Skip to content

Commit d4baa8b

Browse files
committed
fuzz: Add a subcommand for generating corpus input
1 parent 0c708b6 commit d4baa8b

6 files changed

Lines changed: 183 additions & 15 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ smallvec = { version = "1.11.0", features = ["const_generics", "union"] }
2323
[dev-dependencies]
2424
criterion = { version = "0.5.1", features = ["html_reports"] }
2525

26+
# Speed up some things like decoding and corpus generation
27+
[profile.dev.package.rustc_apfloat-fuzz]
28+
opt-level = 1
29+
2630
[[bench]]
2731
name = "decimal"
2832
harness = false

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ on other platforms, or even some Linux distros, though it mostly assumes UNIX.
8787
There is a justfile that makes this easy:
8888

8989
```sh
90+
# Create the corpus
91+
just gen
9092
# Build and run fuzzing
9193
just fuzz
9294
# Do the same thing but use more cores
@@ -104,12 +106,14 @@ cargo install cargo-afl
104106
# Build the fuzzing binary (`target/release/rustc_apfloat-fuzz`).
105107
cargo afl build -p rustc_apfloat-fuzz --release
106108

107-
# Seed the inputs for a run `foo` (while not ideal, even this one minimal input works).
108-
mkdir fuzz/in-foo && echo > fuzz/in-foo/empty
109+
# Seed the inputs for a run `foo`
110+
cargo run -p rustc_apfloat-fuzz -- corpus fuzz/runs/in-unmin
111+
# Minimize the results (optional but recommended)
112+
cargo afl cmin -i fuzz/runs/in-unmin -o fuzz/runs/in -T "$(nproc)" target/release/rustc_apfloat-fuzz
109113

110114
# Start the fuzzing run `foo`, which should bring up the AFL++ progress TUI
111115
# (see also `cargo run -p rustc_apfloat-fuzz` for extra flags available).
112-
cargo afl fuzz -i fuzz/run/in-foo -o fuzz/run/out-foo target/release/rustc_apfloat-fuzz
116+
cargo afl fuzz -i fuzz/runs/in -o fuzz/runs/out-foo target/release/rustc_apfloat-fuzz
113117
```
114118

115119
To visualize the fuzzing testcases, you can use the `decode` subcommand:

fuzz/src/corpus.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use std::fmt::{self, Write as _};
2+
use std::fs;
3+
use std::io::Write;
4+
use std::path::Path;
5+
6+
use num_traits::ToPrimitive;
7+
use rustc_apfloat::{Float, Round};
8+
9+
use crate::{Args, Error, FloatRepr, Op, decode_eval_check, for_each_repr, round_to_u8};
10+
11+
const ROUND_ALL: &[Round] = &[
12+
Round::NearestTiesToEven,
13+
Round::TowardPositive,
14+
Round::TowardNegative,
15+
Round::TowardZero,
16+
Round::NearestTiesToAway,
17+
];
18+
19+
/// Create baseline inputs for the fuzzer to start with. It will start applying mutations to these
20+
/// inputs.
21+
///
22+
/// This creates the cartesian product of the following:
23+
///
24+
/// * All float types
25+
/// * All operations
26+
/// * All rounding modes
27+
/// * A small list of possible inputs, applied to each argument.
28+
///
29+
/// This creates a _lot_ of files, so running `cmin` after helps give the fuzzer less input to
30+
/// work with.
31+
pub fn generate(dir: &Path) {
32+
fs::create_dir_all(&dir).unwrap();
33+
let mut total = 0;
34+
35+
for_each_repr!(for F in all_reprs!() {
36+
let count = gen_for_f::<F>(dir);
37+
total += count;
38+
});
39+
40+
eprintln!("wrote {total} total files to `{}`", dir.display());
41+
eprintln!("note that it is recommended to run `cargo afl cmin` (`just gen` handles this)");
42+
}
43+
44+
fn gen_for_f<F: FloatRepr>(dir: &Path) -> u64 {
45+
let mut buf = Vec::new();
46+
let mut name = String::new();
47+
48+
// There is no `ONE` constant, so this works.
49+
let one = (F::RustcApFloat::SMALLEST / F::RustcApFloat::SMALLEST).value;
50+
let inputs = [
51+
F::RustcApFloat::ZERO,
52+
F::RustcApFloat::INFINITY,
53+
-F::RustcApFloat::ZERO,
54+
-F::RustcApFloat::INFINITY,
55+
F::RustcApFloat::qnan(None),
56+
F::RustcApFloat::snan(None),
57+
F::RustcApFloat::largest(),
58+
F::RustcApFloat::SMALLEST,
59+
F::RustcApFloat::smallest_normalized(),
60+
one,
61+
];
62+
let mut count = 0;
63+
let flt_name = F::short_lowercase_name();
64+
65+
// We don't need to test anything here, just use `cli_args` for config.
66+
let mut cli_args = Args::default();
67+
cli_args.ignore_cxx = true;
68+
cli_args.ignore_hard = true;
69+
70+
for op in Op::ALL.iter().copied() {
71+
for rm in ROUND_ALL.iter().copied() {
72+
let mut write_one = |a: F, b: F, c: F, input_desc: fmt::Arguments| {
73+
buf.clear();
74+
name.clear();
75+
76+
write!(name, "{flt_name}-{op:?}-{rm:?}-{input_desc}").unwrap();
77+
78+
buf.push(F::KIND.to_u8().unwrap());
79+
buf.push(op.to_u8().unwrap());
80+
buf.push(round_to_u8(rm));
81+
82+
for arg in [a, b, c].iter().take(op.airity() as usize) {
83+
arg.write_as_le_bytes_into(&mut buf);
84+
}
85+
86+
// Verify that the created input parses correctly. We don't need to do the
87+
// evaluation check, running the fuzzer will handle that.
88+
match decode_eval_check(&buf, &cli_args, false) {
89+
Ok(()) | Err(Error::Check(_)) => (),
90+
Err(Error::Decode(e)) => panic!("error decoding: {e}"),
91+
}
92+
93+
let mut f = fs::OpenOptions::new()
94+
.create(true)
95+
.write(true)
96+
.truncate(true)
97+
.open(dir.join(&name))
98+
.unwrap();
99+
f.write_all(&mut buf).unwrap();
100+
count += 1;
101+
};
102+
103+
let airity = op.airity() as u8;
104+
for (ai, a) in inputs.iter().enumerate() {
105+
if airity == 1 {
106+
write_one(
107+
F::from_ap(*a),
108+
F::from_bits_u128(0),
109+
F::from_bits_u128(0),
110+
format_args!("{ai}"),
111+
)
112+
} else {
113+
for (bi, b) in inputs.iter().enumerate() {
114+
if airity == 2 {
115+
write_one(
116+
F::from_ap(*a),
117+
F::from_ap(*b),
118+
F::from_bits_u128(0),
119+
format_args!("{ai}-{bi}"),
120+
)
121+
} else {
122+
assert_eq!(airity, 3);
123+
for (ci, c) in inputs.iter().enumerate() {
124+
write_one(
125+
F::from_ap(*a),
126+
F::from_ap(*b),
127+
F::from_ap(*c),
128+
format_args!("{ai}-{bi}-{ci}"),
129+
)
130+
}
131+
}
132+
}
133+
}
134+
}
135+
}
136+
}
137+
138+
eprintln!("{flt_name}: wrote {count} files");
139+
count
140+
}

fuzz/src/exhaustive.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::for_each_repr;
1818

1919
pub fn run_for_all_floats(cli_args: &Args) {
2020
let mut any_mismatches = false;
21-
for_each_repr!(for F in all_floats!() {
21+
for_each_repr!(for F in all_reprs!() {
2222
any_mismatches |= run_exhaustive::<F>(&cli_args).is_err();
2323
});
2424

fuzz/src/main.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#![allow(internal_features)] // for the below config
44
#![feature(cfg_target_has_reliable_f16_f128)]
55

6+
mod corpus;
67
mod exhaustive;
78
mod host;
89

@@ -20,7 +21,7 @@ use rustc_apfloat::{Float, FloatConvert, Round, Status, StatusAnd, ieee};
2021

2122
use crate::host::HostFloat;
2223

23-
#[derive(Clone, Parser, Debug)]
24+
#[derive(Clone, Parser, Debug, Default)]
2425
struct Args {
2526
/// Disable comparison with C++ (LLVM's original) APFloat
2627
#[arg(long)]
@@ -57,6 +58,11 @@ enum Commands {
5758
/// The file to check. If unspecified or `-`, read from stdin.
5859
file: Option<PathBuf>,
5960
},
61+
/// Construct a test corpus in the specified directory
62+
Corpus {
63+
/// The file to check. If unspecified or `-`, read from stdin.
64+
outdir: PathBuf,
65+
},
6066
/// Decode fuzzing in/out testcases (binary serialized `FuzzOp`s)
6167
Decode { files: Vec<PathBuf> },
6268

@@ -106,6 +112,7 @@ fn main() {
106112
reader.read_to_end(&mut buf).unwrap();
107113
fuzz_check(&buf, true);
108114
}
115+
Commands::Corpus { outdir } => corpus::generate(&outdir),
109116
Commands::Decode { files } => run_decode_subcmd(files, &cli_args),
110117
Commands::Bruteforce { .. } => exhaustive::run_for_all_floats(&cli_args),
111118
}
@@ -176,6 +183,7 @@ trait FloatRepr: Copy + Default + Eq + fmt::Display + fmt::Debug {
176183

177184
// FIXME(const) `[u8; Self::BYTE_LEN]` would be better but requires MGCA.
178185
fn from_le_bytes(bytes: &[u8]) -> Self;
186+
fn write_as_le_bytes_into(self, out_bytes: &mut Vec<u8>);
179187

180188
fn to_bits_u128(self) -> u128;
181189
fn from_bits_u128(bits: u128) -> Self;
@@ -197,7 +205,7 @@ macro_rules! float_reprs {
197205
$(type HardFloat = $hard_float_ty:ty;)?
198206
})+) => {
199207
macro_rules! for_each_repr {
200-
(for $ty_var:ident in all_floats!() $block:block) => {
208+
(for $ty_var:ident in all_reprs!() $block:block) => {
201209
$({
202210
type $ty_var = $crate::$name;
203211
$block
@@ -233,6 +241,9 @@ macro_rules! float_reprs {
233241
);
234242
Self(<$repr>::from_le_bytes(repr_bytes))
235243
}
244+
fn write_as_le_bytes_into(self, out_bytes: &mut Vec<u8>) {
245+
out_bytes.extend(&self.0.to_le_bytes()[..Self::BYTE_LEN]);
246+
}
236247

237248
fn to_bits_u128(self) -> u128 {
238249
self.0.into()
@@ -339,7 +350,7 @@ float_reprs! {
339350

340351
pub(crate) use for_each_repr;
341352

342-
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, FromPrimitive)]
353+
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive)]
343354
pub enum FpKind {
344355
// The tag is based on the bit count. These are specified so corpus inputs are stable.
345356
Ieee16 = 16,
@@ -365,10 +376,6 @@ impl FpKind {
365376
Self::BrainF16,
366377
Self::X87_F80,
367378
];
368-
369-
pub fn to_u8(self) -> u8 {
370-
self as u8
371-
}
372379
}
373380

374381
/// A testable operation, which can be encoded as a byte.

justfile

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# Allow overriding the fuzz directories
2+
fuzz_in_unmin := env("FUZZ_IN_UNMIN", "fuzz/runs/in-unmin")
23
fuzz_in := env("FUZZ_IN", "fuzz/runs/in")
34
fuzz_out := env("FUZZ_OUT", "fuzz/runs/out")
5+
fuzz_bin := env("FUZZ_BIN", "target/release/rustc_apfloat-fuzz")
46

57
alias f := fuzz
8+
alias fb := fuzz-build
69
alias fp := fuzz-parallel
710
alias fa := fuzz-attach
811
alias fq := fuzz-parallel-quit
@@ -17,17 +20,27 @@ test:
1720
cargo test --workspace
1821

1922
# Create directories and build the executable, but don't start fuzzing.
20-
_fuzz-setup:
23+
fuzz-build:
2124
mkdir -p "{{ fuzz_in }}"
2225
echo > "{{ fuzz_in }}/empty"
2326
cargo afl build -p rustc_apfloat-fuzz --release
2427

28+
# Generate a corpus for fuzzing then run `cmin`
29+
gen: fuzz-build
30+
rm -rf "{{ fuzz_in_unmin }}" "{{ fuzz_in }}"
31+
cargo run -p rustc_apfloat-fuzz -- corpus "{{ fuzz_in_unmin }}"
32+
cargo afl cmin \
33+
-i "{{ fuzz_in_unmin }}" \
34+
-o "{{ fuzz_in }}" \
35+
-T "{{ num_cpus() }}" \
36+
"{{ fuzz_bin }}"
37+
2538
# Build the instrumented executable and fuzz it. See also: `fuzz-parallel`.
26-
fuzz: _fuzz-setup
27-
cargo afl fuzz -i "{{ fuzz_in }}" -o "{{ fuzz_out }}" target/release/rustc_apfloat-fuzz
39+
fuzz: fuzz-build
40+
cargo afl fuzz -i "{{ fuzz_in }}" -o "{{ fuzz_out }}" "{{ fuzz_bin }}"
2841

2942
# Start fuzzing in parallel. Note this must be stopped with fuzz-parallel-quit (see fuzz-parallel.sh).
30-
fuzz-parallel *args: _fuzz-setup
43+
fuzz-parallel *args: fuzz-build
3144
etc/fuzz-parallel.sh {{ args }}
3245

3346
# Attach to a running parallel fuzz session

0 commit comments

Comments
 (0)