Skip to content

Commit b4be0b9

Browse files
authored
Merge pull request #700 from FractalFir/fuzz_support
Add support for automatically reducing found fuzz cases.
2 parents acfb046 + e3d4805 commit b4be0b9

File tree

2 files changed

+507
-23
lines changed

2 files changed

+507
-23
lines changed

build_system/src/fuzz.rs

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
use std::ffi::OsStr;
22
use std::path::Path;
33

4+
mod reduce;
5+
46
use crate::utils::run_command_with_output;
57

68
fn show_usage() {
79
println!(
810
r#"
911
`fuzz` command help:
10-
--help : Show this help"#
12+
--reduce : Reduces a file generated by rustlantis
13+
--help : Show this help
14+
--start : Start of the fuzzed range
15+
--count : The number of cases to fuzz
16+
-j --jobs : The number of threads to use during fuzzing"#
1117
);
1218
}
1319

@@ -20,6 +26,16 @@ pub fn run() -> Result<(), String> {
2026
std::thread::available_parallelism().map(|threads| threads.get()).unwrap_or(1);
2127
while let Some(arg) = args.next() {
2228
match arg.as_str() {
29+
"--reduce" => {
30+
let Some(path) = args.next() else {
31+
return Err("--reduce must be provided with a path".into());
32+
};
33+
if !std::fs::exists(&path).unwrap_or(false) {
34+
return Err("--reduce must be provided with a valid path".into());
35+
}
36+
reduce::reduce(&path);
37+
return Ok(());
38+
}
2339
"--help" => {
2440
show_usage();
2541
return Ok(());
@@ -75,16 +91,17 @@ fn fuzz_range(start: u64, end: u64, threads: usize) {
7591
let start = Arc::new(AtomicU64::new(start));
7692
// Count time during fuzzing
7793
let start_time = Instant::now();
94+
let mut workers = Vec::with_capacity(threads);
7895
// Spawn `threads`..
7996
for _ in 0..threads {
8097
let start = start.clone();
8198
// .. which each will ..
82-
std::thread::spawn(move || {
99+
workers.push(std::thread::spawn(move || {
83100
// ... grab the next fuzz seed ...
84101
while start.load(Ordering::Relaxed) < end {
85102
let next = start.fetch_add(1, Ordering::Relaxed);
86103
// .. test that seed .
87-
match test(next) {
104+
match test(next, false) {
88105
Err(err) => {
89106
// If the test failed at compile-time...
90107
println!("test({}) failed because {err:?}", next);
@@ -99,29 +116,38 @@ fn fuzz_range(start: u64, end: u64, threads: usize) {
99116
Ok(Err(err)) => {
100117
// If the test failed at run-time...
101118
println!("The LLVM and GCC results don't match for {err:?}");
102-
// ... copy that file to the directory `target/fuzz/runtime_error`...
119+
// ... generate a new file, which prints temporaries(instead of hashing them)...
103120
let mut out_path: std::path::PathBuf = "target/fuzz/runtime_error".into();
104121
std::fs::create_dir_all(&out_path).unwrap();
105-
// .. into a file named `fuzz{seed}.rs`.
122+
let Ok(Err(tmp_print_err)) = test(next, true) else {
123+
// ... if that file does not reproduce the issue...
124+
// ... save the original sample in a file named `fuzz{seed}.rs`...
125+
out_path.push(&format!("fuzz{next}.rs"));
126+
std::fs::copy(err, &out_path).unwrap();
127+
continue;
128+
};
129+
// ... if that new file still produces the issue, copy it to `fuzz{seed}.rs`..
106130
out_path.push(&format!("fuzz{next}.rs"));
107-
std::fs::copy(err, out_path).unwrap();
131+
std::fs::copy(tmp_print_err, &out_path).unwrap();
132+
// ... and start reducing it, using some properties of `rustlantis` to speed up the process.
133+
reduce::reduce(&out_path);
108134
}
109135
// If the test passed, do nothing
110136
Ok(Ok(())) => (),
111137
}
112138
}
113-
});
139+
}));
114140
}
115141
// The "manager" thread loop.
116-
while start.load(Ordering::Relaxed) < end {
142+
while start.load(Ordering::Relaxed) < end || !workers.iter().all(|t| t.is_finished()) {
117143
// Every 500 ms...
118144
let five_hundred_millis = Duration::from_millis(500);
119145
std::thread::sleep(five_hundred_millis);
120146
// ... calculate the remaining fuzz iters ...
121147
let remaining = end - start.load(Ordering::Relaxed);
122148
// ... fix the count(the start counter counts the cases that
123149
// begun fuzzing, and not only the ones that are done)...
124-
let fuzzed = (total - remaining) - threads as u64;
150+
let fuzzed = (total - remaining).saturating_sub(threads as u64);
125151
// ... and the fuzz speed ...
126152
let iter_per_sec = fuzzed as f64 / start_time.elapsed().as_secs_f64();
127153
// .. and use them to display fuzzing stats.
@@ -131,6 +157,7 @@ fn fuzz_range(start: u64, end: u64, threads: usize) {
131157
(remaining as f64) / iter_per_sec
132158
)
133159
}
160+
drop(workers);
134161
}
135162

136163
/// Builds & runs a file with LLVM.
@@ -198,37 +225,61 @@ fn release_gcc(path: &std::path::Path) -> Result<Vec<u8>, String> {
198225
res.extend(output.stderr);
199226
Ok(res)
200227
}
201-
228+
type ResultCache = Option<(Vec<u8>, Vec<u8>)>;
202229
/// Generates a new rustlantis file, & compares the result of running it with GCC and LLVM.
203-
fn test(seed: u64) -> Result<Result<(), std::path::PathBuf>, String> {
230+
fn test(seed: u64, print_tmp_vars: bool) -> Result<Result<(), std::path::PathBuf>, String> {
204231
// Generate a Rust source...
205-
let source_file = generate(seed)?;
206-
// ... test it with debug LLVM ...
207-
let llvm_res = debug_llvm(&source_file)?;
208-
// ... test it with release GCC ...
232+
let source_file = generate(seed, print_tmp_vars)?;
233+
test_file(&source_file, true)
234+
}
235+
/// Tests a file with a cached LLVM result. Used for reduction, when it is known
236+
/// that a given transformation should not change the execution result.
237+
fn test_cached(
238+
source_file: &Path,
239+
remove_tmps: bool,
240+
cache: &mut ResultCache,
241+
) -> Result<Result<(), std::path::PathBuf>, String> {
242+
// Test `source_file` with release GCC ...
209243
let gcc_res = release_gcc(&source_file)?;
244+
if cache.is_none() {
245+
// ...test `source_file` with debug LLVM ...
246+
*cache = Some((debug_llvm(&source_file)?, gcc_res.clone()));
247+
}
248+
let (llvm_res, old_gcc) = cache.as_ref().unwrap();
210249
// ... compare the results ...
211-
if llvm_res != gcc_res {
250+
if *llvm_res != gcc_res && gcc_res == *old_gcc {
212251
// .. if they don't match, report an error.
213-
Ok(Err(source_file))
252+
Ok(Err(source_file.to_path_buf()))
214253
} else {
215-
std::fs::remove_file(source_file).map_err(|err| format!("{err:?}"))?;
254+
if remove_tmps {
255+
std::fs::remove_file(source_file).map_err(|err| format!("{err:?}"))?;
256+
}
216257
Ok(Ok(()))
217258
}
218259
}
260+
fn test_file(
261+
source_file: &Path,
262+
remove_tmps: bool,
263+
) -> Result<Result<(), std::path::PathBuf>, String> {
264+
let mut uncached = None;
265+
test_cached(source_file, remove_tmps, &mut uncached)
266+
}
219267

220268
/// Generates a new rustlantis file for us to run tests on.
221-
fn generate(seed: u64) -> Result<std::path::PathBuf, String> {
269+
fn generate(seed: u64, print_tmp_vars: bool) -> Result<std::path::PathBuf, String> {
222270
use std::io::Write;
223271
let mut out_path = std::env::temp_dir();
224272
out_path.push(&format!("fuzz{seed}.rs"));
225273
// We need to get the command output here.
226-
let out = std::process::Command::new("cargo")
274+
let mut generate = std::process::Command::new("cargo");
275+
generate
227276
.args(["run", "--release", "--bin", "generate"])
228277
.arg(&format!("{seed}"))
229-
.current_dir("clones/rustlantis")
230-
.output()
231-
.map_err(|err| format!("{err:?}"))?;
278+
.current_dir("clones/rustlantis");
279+
if print_tmp_vars {
280+
generate.arg("--debug");
281+
}
282+
let out = generate.output().map_err(|err| format!("{err:?}"))?;
232283
// Stuff the rustlantis output in a source file.
233284
std::fs::File::create(&out_path)
234285
.map_err(|err| format!("{err:?}"))?

0 commit comments

Comments
 (0)