use std::ffi::OsStr; use std::path::Path; mod reduce; use crate::utils::run_command_with_output; fn show_usage() { println!( r#" `fuzz` command help: --reduce : Reduces a file generated by rustlantis --help : Show this help --start : Start of the fuzzed range --count : The number of cases to fuzz -j --jobs : The number of threads to use during fuzzing"# ); } pub fn run() -> Result<(), String> { // We skip binary name and the `fuzz` command. let mut args = std::env::args().skip(2); let mut start = 0; let mut count = 100; let mut threads = std::thread::available_parallelism().map(|threads| threads.get()).unwrap_or(1); while let Some(arg) = args.next() { match arg.as_str() { "--reduce" => { let Some(path) = args.next() else { return Err("--reduce must be provided with a path".into()); }; if !std::fs::exists(&path).unwrap_or(false) { return Err("--reduce must be provided with a valid path".into()); } reduce::reduce(&path); return Ok(()); } "--help" => { show_usage(); return Ok(()); } "--start" => { start = str::parse(&args.next().ok_or_else(|| "Fuzz start not provided!".to_string())?) .map_err(|err| format!("Fuzz start not a number {err:?}!"))?; } "--count" => { count = str::parse(&args.next().ok_or_else(|| "Fuzz count not provided!".to_string())?) .map_err(|err| format!("Fuzz count not a number {err:?}!"))?; } "-j" | "--jobs" => { threads = str::parse( &args.next().ok_or_else(|| "Fuzz thread count not provided!".to_string())?, ) .map_err(|err| format!("Fuzz thread count not a number {err:?}!"))?; } _ => return Err(format!("Unknown option {arg}")), } } // Ensure that we have a cloned version of rustlantis on hand. crate::utils::git_clone( "https://github.com/cbeuw/rustlantis.git", Some("clones/rustlantis".as_ref()), true, ) .map_err(|err| format!("Git clone failed with message: {err:?}!"))?; // Ensure that we are on the newest rustlantis commit. let cmd: &[&dyn AsRef] = &[&"git", &"pull", &"origin"]; run_command_with_output(cmd, Some(Path::new("clones/rustlantis")))?; // Build the release version of rustlantis let cmd: &[&dyn AsRef] = &[&"cargo", &"build", &"--release"]; run_command_with_output(cmd, Some(Path::new("clones/rustlantis")))?; // Fuzz a given range fuzz_range(start, start + count, threads); Ok(()) } /// Fuzzes a range `start..end` with `threads`. fn fuzz_range(start: u64, end: u64, threads: usize) { use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant}; // Total amount of files to fuzz let total = end - start; // Currently fuzzed element let start = Arc::new(AtomicU64::new(start)); // Count time during fuzzing let start_time = Instant::now(); let mut workers = Vec::with_capacity(threads); // Spawn `threads`.. for _ in 0..threads { let start = start.clone(); // .. which each will .. workers.push(std::thread::spawn(move || { // ... grab the next fuzz seed ... while start.load(Ordering::Relaxed) < end { let next = start.fetch_add(1, Ordering::Relaxed); // .. test that seed . match test(next, false) { Err(err) => { // If the test failed at compile-time... println!("test({next}) failed because {err:?}"); // ... copy that file to the directory `target/fuzz/compiletime_error`... let mut out_path: std::path::PathBuf = "target/fuzz/compiletime_error".into(); std::fs::create_dir_all(&out_path).unwrap(); // .. into a file named `fuzz{seed}.rs`. out_path.push(format!("fuzz{next}.rs")); std::fs::copy(err, out_path).unwrap(); } Ok(Err(err)) => { // If the test failed at run-time... println!("The LLVM and GCC results don't match for {err:?}"); // ... generate a new file, which prints temporaries(instead of hashing them)... let mut out_path: std::path::PathBuf = "target/fuzz/runtime_error".into(); std::fs::create_dir_all(&out_path).unwrap(); let Ok(Err(tmp_print_err)) = test(next, true) else { // ... if that file does not reproduce the issue... // ... save the original sample in a file named `fuzz{seed}.rs`... out_path.push(format!("fuzz{next}.rs")); std::fs::copy(err, &out_path).unwrap(); continue; }; // ... if that new file still produces the issue, copy it to `fuzz{seed}.rs`.. out_path.push(format!("fuzz{next}.rs")); std::fs::copy(tmp_print_err, &out_path).unwrap(); // ... and start reducing it, using some properties of `rustlantis` to speed up the process. reduce::reduce(&out_path); } // If the test passed, do nothing Ok(Ok(())) => (), } } })); } // The "manager" thread loop. while start.load(Ordering::Relaxed) < end || !workers.iter().all(|t| t.is_finished()) { // Every 500 ms... let five_hundred_millis = Duration::from_millis(500); std::thread::sleep(five_hundred_millis); // ... calculate the remaining fuzz iters ... let remaining = end - start.load(Ordering::Relaxed); // ... fix the count(the start counter counts the cases that // begun fuzzing, and not only the ones that are done)... let fuzzed = (total - remaining).saturating_sub(threads as u64); // ... and the fuzz speed ... let iter_per_sec = fuzzed as f64 / start_time.elapsed().as_secs_f64(); // .. and use them to display fuzzing stats. println!( "fuzzed {fuzzed} cases({}%), at rate {iter_per_sec} iter/s, remaining ~{}s", (100 * fuzzed) as f64 / total as f64, (remaining as f64) / iter_per_sec ) } drop(workers); } /// Builds & runs a file with LLVM. fn debug_llvm(path: &std::path::Path) -> Result, String> { // Build a file named `llvm_elf`... let exe_path = path.with_extension("llvm_elf"); // ... using the LLVM backend ... let output = std::process::Command::new("rustc") .arg(path) .arg("-o") .arg(&exe_path) .output() .map_err(|err| format!("{err:?}"))?; // ... check that the compilation succeeded ... if !output.status.success() { return Err(format!("LLVM compilation failed:{output:?}")); } // ... run the resulting executable ... let output = std::process::Command::new(&exe_path).output().map_err(|err| format!("{err:?}"))?; // ... check it run normally ... if !output.status.success() { return Err(format!( "The program at {path:?}, compiled with LLVM, exited unsuccessfully:{output:?}" )); } // ... cleanup that executable ... std::fs::remove_file(exe_path).map_err(|err| format!("{err:?}"))?; // ... and return the output(stdout + stderr - this allows UB checks to fire). let mut res = output.stdout; res.extend(output.stderr); Ok(res) } /// Builds & runs a file with GCC. fn release_gcc(path: &std::path::Path) -> Result, String> { // Build a file named `gcc_elf`... let exe_path = path.with_extension("gcc_elf"); // ... using the GCC backend ... let output = std::process::Command::new("./y.sh") .arg("rustc") .arg(path) .arg("-O") .arg("-o") .arg(&exe_path) .output() .map_err(|err| format!("{err:?}"))?; // ... check that the compilation succeeded ... if !output.status.success() { return Err(format!("GCC compilation failed:{output:?}")); } // ... run the resulting executable .. let output = std::process::Command::new(&exe_path).output().map_err(|err| format!("{err:?}"))?; // ... check it run normally ... if !output.status.success() { return Err(format!( "The program at {path:?}, compiled with GCC, exited unsuccessfully:{output:?}" )); } // ... cleanup that executable ... std::fs::remove_file(exe_path).map_err(|err| format!("{err:?}"))?; // ... and return the output(stdout + stderr - this allows UB checks to fire). let mut res = output.stdout; res.extend(output.stderr); Ok(res) } type ResultCache = Option<(Vec, Vec)>; /// Generates a new rustlantis file, & compares the result of running it with GCC and LLVM. fn test(seed: u64, print_tmp_vars: bool) -> Result, String> { // Generate a Rust source... let source_file = generate(seed, print_tmp_vars)?; test_file(&source_file, true) } /// Tests a file with a cached LLVM result. Used for reduction, when it is known /// that a given transformation should not change the execution result. fn test_cached( source_file: &Path, remove_tmps: bool, cache: &mut ResultCache, ) -> Result, String> { // Test `source_file` with release GCC ... let gcc_res = release_gcc(source_file)?; if cache.is_none() { // ...test `source_file` with debug LLVM ... *cache = Some((debug_llvm(source_file)?, gcc_res.clone())); } let (llvm_res, old_gcc) = cache.as_ref().unwrap(); // ... compare the results ... if *llvm_res != gcc_res && gcc_res == *old_gcc { // .. if they don't match, report an error. Ok(Err(source_file.to_path_buf())) } else { if remove_tmps { std::fs::remove_file(source_file).map_err(|err| format!("{err:?}"))?; } Ok(Ok(())) } } fn test_file( source_file: &Path, remove_tmps: bool, ) -> Result, String> { let mut uncached = None; test_cached(source_file, remove_tmps, &mut uncached) } /// Generates a new rustlantis file for us to run tests on. fn generate(seed: u64, print_tmp_vars: bool) -> Result { use std::io::Write; let mut out_path = std::env::temp_dir(); out_path.push(format!("fuzz{seed}.rs")); // We need to get the command output here. let mut generate = std::process::Command::new("cargo"); generate .args(["run", "--release", "--bin", "generate"]) .arg(format!("{seed}")) .current_dir("clones/rustlantis"); if print_tmp_vars { generate.arg("--debug"); } let out = generate.output().map_err(|err| format!("{err:?}"))?; // Stuff the rustlantis output in a source file. std::fs::File::create(&out_path) .map_err(|err| format!("{err:?}"))? .write_all(&out.stdout) .map_err(|err| format!("{err:?}"))?; Ok(out_path) }