use std::panic::{self, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use super::corpus::Corpus;
const CASE_TIMEOUT: Duration = Duration::from_secs(20);
const ARTIFACT_DIR: &str = "fuzz-artifacts";
pub struct Target {
pub name: &'static str,
pub extension: &'static str,
pub default_seconds: u64,
pub regressions: fn(),
pub generate: fn(&Corpus, &mut StdRng) -> Vec<u8>,
pub run: fn(&[u8]),
}
pub fn targets() -> Vec<Target> {
vec![
super::file::random::TARGET,
super::tree::random::TARGET,
super::vm::random::TARGET,
super::libs::random::TARGET,
super::memory::random::TARGET,
]
}
const USAGE: &str =
"Usage: neyuki fuzz [<target>[,<target>...]|all] [--seconds N] [--seed N] [--keep-going]
neyuki fuzz <target> --replay <file>
Without a target, runs the fixed regression cases of every target. With
--keep-going, a crash is saved and fuzzing continues; the run still fails at
the end, after reporting each distinct crash once.
Targets: file (bytecode loader and verifier), tree (parser, checker, compiler,
formatter), vm (generated and mutated programs), libs (standard library
parsers), memory (buffers and the garbage collector).";
static PANICS: AtomicUsize = AtomicUsize::new(0);
static IN_CASE: AtomicBool = AtomicBool::new(false);
static LAST_PANIC: Mutex<Option<String>> = Mutex::new(None);
fn install_panic_hook() {
panic::set_hook(Box::new(|info| {
PANICS.fetch_add(1, Ordering::SeqCst);
let backtrace = std::backtrace::Backtrace::force_capture().to_string();
let frames: Vec<&str> = backtrace
.lines()
.map(str::trim)
.filter(|l| l.starts_with("at ./src/") && !l.contains("/fuzz/driver.rs"))
.collect();
let msg = format!("{}\n {}", info, frames.join("\n "));
if !IN_CASE.load(Ordering::SeqCst) {
eprintln!("{}", msg);
}
if let Ok(mut last) = LAST_PANIC.lock() {
*last = Some(msg);
}
}));
}
pub fn run_cli(args: &[String]) -> Result<(), String> {
let mut names: Option<String> = None;
let mut seconds: Option<u64> = None;
let mut seed: Option<u64> = None;
let mut replay: Option<String> = None;
let mut keep_going = false;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
let mut value = |flag: &str| {
iter.next()
.cloned()
.ok_or_else(|| format!("{} needs a value\n\n{}", flag, USAGE))
};
match arg.as_str() {
"--seconds" => {
let v = value("--seconds")?;
seconds = Some(
v.parse()
.map_err(|_| format!("invalid --seconds '{}'", v))?,
);
}
"--seed" => {
let v = value("--seed")?;
seed = Some(v.parse().map_err(|_| format!("invalid --seed '{}'", v))?);
}
"--replay" => replay = Some(value("--replay")?),
"--keep-going" => keep_going = true,
"-h" | "--help" => {
println!("{}", USAGE);
return Ok(());
}
other if other.starts_with('-') => {
return Err(format!("unknown option '{}'\n\n{}", other, USAGE));
}
other => names = Some(other.to_string()),
}
}
let Some(names) = names else {
super::run_all_fuzz_tests();
return Ok(());
};
let all = targets();
let selected: Vec<&Target> = if names == "all" {
all.iter().collect()
} else {
names
.split(',')
.map(|name| {
all.iter()
.find(|t| t.name == name)
.ok_or_else(|| format!("unknown fuzz target '{}'\n\n{}", name, USAGE))
})
.collect::<Result<_, _>>()?
};
install_panic_hook();
if let Some(path) = replay {
let [target] = selected[..] else {
return Err("--replay takes exactly one target".to_string());
};
let input = std::fs::read(&path).map_err(|e| format!("failed to read {}: {}", path, e))?;
return run_case(target, &input)
.map_err(|msg| format!("{} crashed on {}:\n{}", target.name, path, msg));
}
let seed = seed.unwrap_or_else(|| rand::thread_rng().r#gen());
println!(
"fuzz seed {} (pass --seed {} to repeat this run)",
seed, seed
);
let corpus = Corpus::load();
let mut crashes = Vec::new();
for target in selected {
let seconds = seconds.unwrap_or(target.default_seconds);
crashes.extend(fuzz_target(target, &corpus, seed, seconds, keep_going)?);
}
if crashes.is_empty() {
return Ok(());
}
for crash in &crashes {
eprintln!("\n{}", crash);
}
Err(format!("{} distinct crash(es) found", crashes.len()))
}
fn panic_location(msg: &str) -> &str {
msg.lines()
.next()
.and_then(|line| line.strip_prefix("panicked at "))
.unwrap_or(msg)
}
fn run_case(target: &Target, input: &[u8]) -> Result<(), String> {
let before = PANICS.load(Ordering::SeqCst);
IN_CASE.store(true, Ordering::SeqCst);
let result = panic::catch_unwind(AssertUnwindSafe(|| (target.run)(input)));
IN_CASE.store(false, Ordering::SeqCst);
if result.is_err() || PANICS.load(Ordering::SeqCst) != before {
let msg = LAST_PANIC
.lock()
.ok()
.and_then(|mut last| last.take())
.unwrap_or_else(|| "panic".to_string());
return Err(msg);
}
Ok(())
}
fn fuzz_target(
target: &Target,
corpus: &Corpus,
seed: u64,
seconds: u64,
keep_going: bool,
) -> Result<Vec<String>, String> {
println!("-> {}: regression cases", target.name);
let before = PANICS.load(Ordering::SeqCst);
if panic::catch_unwind(target.regressions).is_err() || PANICS.load(Ordering::SeqCst) != before {
let msg = LAST_PANIC
.lock()
.ok()
.and_then(|mut l| l.take())
.unwrap_or_default();
return Err(format!(
"{}: a regression case failed:\n{}",
target.name, msg
));
}
println!("-> {}: random cases for {}s", target.name, seconds);
let current: Arc<Mutex<(u64, Vec<u8>)>> = Arc::new(Mutex::new((0, Vec::new())));
let started = Arc::new(AtomicU64::new(0));
let done = Arc::new(AtomicBool::new(false));
let clock = Instant::now();
let watchdog = spawn_watchdog(
target,
Arc::clone(¤t),
Arc::clone(&started),
Arc::clone(&done),
clock,
);
let mut rng = StdRng::seed_from_u64(seed ^ fnv(target.name));
let deadline = clock + Duration::from_secs(seconds);
let mut cases = 0u64;
let mut crashes: Vec<String> = Vec::new();
let mut seen = std::collections::HashSet::new();
let in_flight = format!(
"{}/{}-current.{}",
ARTIFACT_DIR, target.name, target.extension
);
let _ = std::fs::create_dir_all(ARTIFACT_DIR);
let outcome = loop {
if Instant::now() >= deadline {
break Ok(());
}
let input = (target.generate)(corpus, &mut rng);
if let Ok(mut cur) = current.lock() {
*cur = (cases, input.clone());
}
let _ = std::fs::write(&in_flight, &input);
started.store(clock.elapsed().as_millis() as u64 + 1, Ordering::SeqCst);
let result = run_case(target, &input);
started.store(0, Ordering::SeqCst);
cases += 1;
if let Err(msg) = result {
if !seen.insert(panic_location(&msg).to_string()) {
continue;
}
let path = save_artifact(target, &input);
let report = format!(
"{} crashed on case {} (seed {}), input saved to {}\n\
replay with: neyuki fuzz {} --replay {}\n{}",
target.name,
cases - 1,
seed,
path,
target.name,
path,
msg
);
if !keep_going {
break Err(report);
}
eprintln!(" crash at {}, saved to {}", panic_location(&msg), path);
crashes.push(report);
}
};
done.store(true, Ordering::SeqCst);
let _ = watchdog.join();
let _ = std::fs::remove_file(&in_flight);
let _ = std::fs::remove_dir(ARTIFACT_DIR);
outcome?;
println!(
" {} cases in {:.1}s, {} distinct crash(es)",
cases,
clock.elapsed().as_secs_f64(),
crashes.len()
);
Ok(crashes)
}
fn spawn_watchdog(
target: &Target,
current: Arc<Mutex<(u64, Vec<u8>)>>,
started: Arc<AtomicU64>,
done: Arc<AtomicBool>,
clock: Instant,
) -> std::thread::JoinHandle<()> {
let name = target.name;
let extension = target.extension;
std::thread::spawn(move || {
while !done.load(Ordering::SeqCst) {
std::thread::sleep(Duration::from_millis(250));
let since = started.load(Ordering::SeqCst);
if since == 0 {
continue;
}
let running = clock.elapsed().as_millis() as u64 + 1 - since;
if running > CASE_TIMEOUT.as_millis() as u64 && !done.load(Ordering::SeqCst) {
let (case, input) = current.lock().map(|c| c.clone()).unwrap_or_default();
let path = write_artifact(name, extension, &input);
eprintln!(
"{} hung on case {} for over {}s, input saved to {}\n\
replay with: neyuki fuzz {} --replay {}",
name,
case,
CASE_TIMEOUT.as_secs(),
path,
name,
path
);
std::process::exit(1);
}
}
})
}
fn save_artifact(target: &Target, input: &[u8]) -> String {
write_artifact(target.name, target.extension, input)
}
fn write_artifact(name: &str, extension: &str, input: &[u8]) -> String {
let path = format!(
"{}/{}-{:016x}.{}",
ARTIFACT_DIR,
name,
fnv_bytes(input),
extension
);
if let Err(err) =
std::fs::create_dir_all(ARTIFACT_DIR).and_then(|_| std::fs::write(&path, input))
{
eprintln!("failed to save {}: {}", path, err);
}
path
}
fn fnv(s: &str) -> u64 {
fnv_bytes(s.as_bytes())
}
fn fnv_bytes(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf29ce484222325u64, |h, b| {
(h ^ *b as u64).wrapping_mul(0x100000001b3)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_target_runs_random_cases() {
let corpus = Corpus::load();
for target in targets() {
let mut rng = StdRng::seed_from_u64(7);
for _ in 0..40 {
let input = (target.generate)(&corpus, &mut rng);
let result = panic::catch_unwind(AssertUnwindSafe(|| (target.run)(&input)));
assert!(result.is_ok(), "{} panicked", target.name);
}
}
}
}