1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
|
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)
}
|