| 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<OsStr>] = &[&"git", &"pull", &"origin"]; |
| run_command_with_output(cmd, Some(Path::new("clones/rustlantis")))?; |
| |
| // Build the release version of rustlantis |
| let cmd: &[&dyn AsRef<OsStr>] = &[&"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<Vec<u8>, 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<Vec<u8>, 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<u8>, Vec<u8>)>; |
| /// Generates a new rustlantis file, & compares the result of running it with GCC and LLVM. |
| fn test(seed: u64, print_tmp_vars: bool) -> Result<Result<(), std::path::PathBuf>, 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<Result<(), std::path::PathBuf>, 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<Result<(), std::path::PathBuf>, 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<std::path::PathBuf, String> { |
| 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) |
| } |