From 143ef978d4924bb5408c2aeec00d26f7de0077bc Mon Sep 17 00:00:00 2001 From: Christoph Gehrke Date: Thu, 9 Apr 2026 10:51:50 +0200 Subject: [PATCH] feat(build): add target argument Adapt target resolving, such that guessing fails when bin target is non-unique. Additionally, cache `cargo_metadata` in `Common` struct (now an argument for every subcmd). --- src/bin/cargo-ziggy/add_seeds.rs | 12 +- src/bin/cargo-ziggy/build.rs | 33 +- src/bin/cargo-ziggy/clean.rs | 15 +- src/bin/cargo-ziggy/coverage.rs | 55 ++- src/bin/cargo-ziggy/fuzz.rs | 581 ++++++++++++++++--------------- src/bin/cargo-ziggy/main.rs | 148 ++++---- src/bin/cargo-ziggy/minimize.rs | 139 ++++---- src/bin/cargo-ziggy/plot.rs | 24 +- src/bin/cargo-ziggy/run.rs | 21 +- src/bin/cargo-ziggy/triage.rs | 16 +- src/bin/cargo-ziggy/util.rs | 53 +++ tests/url_fuzz.rs | 9 +- 12 files changed, 600 insertions(+), 506 deletions(-) diff --git a/src/bin/cargo-ziggy/add_seeds.rs b/src/bin/cargo-ziggy/add_seeds.rs index cdfe09b..c6f87d4 100644 --- a/src/bin/cargo-ziggy/add_seeds.rs +++ b/src/bin/cargo-ziggy/add_seeds.rs @@ -1,9 +1,9 @@ -use crate::*; -use anyhow::bail; +use crate::{AddSeeds, Common}; +use anyhow::{Context, bail}; use std::{env, process}; impl AddSeeds { - pub fn add_seeds(&self) -> Result<(), anyhow::Error> { + pub fn add_seeds(&self, common: &Common) -> Result<(), anyhow::Error> { eprintln!("Adding seeds to AFL"); let req = semver::VersionReq::parse(">=0.14.5").unwrap(); @@ -27,11 +27,11 @@ impl AddSeeds { ); } - let target = find_target(&self.target)?; + let target = common.resolve_bin(self.target.clone())?; let input = self.input.display().to_string(); - let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); - process::Command::new(&cargo) + common + .cargo() .args([ "afl", "addseeds", diff --git a/src/bin/cargo-ziggy/build.rs b/src/bin/cargo-ziggy/build.rs index a106361..9185178 100644 --- a/src/bin/cargo-ziggy/build.rs +++ b/src/bin/cargo-ziggy/build.rs @@ -1,23 +1,29 @@ -use crate::Build; -use anyhow::{Context, Result, bail}; +use crate::{Build, Common, util::Context}; +use anyhow::{Context as _, Result, bail}; use console::style; use std::{env, process}; impl Build { /// Build the fuzzers - pub fn build(&self) -> Result<(), anyhow::Error> { + pub fn build(&self, common: &Common) -> Result<(), anyhow::Error> { // No fuzzers for you if self.no_afl && self.no_honggfuzz { bail!("Pick at least one fuzzer"); } - // The cargo executable - let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); + let cx = Context::new(common, self.target.clone())?; if !self.no_afl { eprintln!(" {} afl", style("Building").red().bold()); - let target_dir = format!("--target-dir={}", super::target_dir().join("afl")); - let mut afl_args = vec!["afl", "build", "--features=ziggy/afl", &target_dir]; + let target_dir = format!("--target-dir={}", cx.target_dir.join("afl")); + let mut afl_args = vec![ + "afl", + "build", + "--features=ziggy/afl", + &target_dir, + "--bin", + &cx.bin_target, + ]; // Add the --release argument if self.release is true if self.release { @@ -29,7 +35,8 @@ impl Build { let mut rust_doc_flags = env::var("RUSTDOCFLAGS").unwrap_or_default(); // First fuzzer we build: AFL++ - let run = process::Command::new(&cargo) + let run = common + .cargo() .args(&afl_args) .env("AFL_QUIET", "1") .env("AFL_LLVM_CMPLOG", "1") // for afl.rs feature "plugins" @@ -57,7 +64,8 @@ impl Build { rust_flags.push_str("-Copt-level=0"); rust_doc_flags.push_str(" -Zsanitizer=address "); - let run = process::Command::new(&cargo) + let run = common + .cargo() .args(afl_args) .env("AFL_QUIET", "1") // need to specify for afl.rs so that we build with -Copt-level=0 @@ -85,9 +93,10 @@ impl Build { eprintln!(" {} honggfuzz", style("Building").red().bold()); // Second fuzzer we build: Honggfuzz - let run = process::Command::new(&cargo) - .args(["hfuzz", "build"]) - .env("CARGO_TARGET_DIR", super::target_dir().join("honggfuzz")) + let run = common + .cargo() + .args(["hfuzz", "build", "--bin", &cx.bin_target]) + .env("CARGO_TARGET_DIR", cx.target_dir.join("honggfuzz")) .env("HFUZZ_BUILD_ARGS", "--features=ziggy/honggfuzz") .env("RUSTFLAGS", env::var("RUSTFLAGS").unwrap_or_default()) .stdout(process::Stdio::piped()) diff --git a/src/bin/cargo-ziggy/clean.rs b/src/bin/cargo-ziggy/clean.rs index 3bfc8fe..b592be1 100644 --- a/src/bin/cargo-ziggy/clean.rs +++ b/src/bin/cargo-ziggy/clean.rs @@ -1,23 +1,24 @@ -use crate::Clean; +use crate::{Clean, Common}; use anyhow::{Error, bail}; -use std::{env, process::Command}; impl Clean { - pub fn clean(&self) -> Result<(), Error> { - let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); - + pub fn clean(&self, common: &Common) -> Result<(), Error> { + let Ok(target_dir) = common.target_dir() else { + return Ok(()); + }; let clean = |target, target_triple: Option<&str>, try_release| -> Result<(), Error> { let already_profile = self .args .iter() .any(|arg| arg.as_encoded_bytes().starts_with(b"--profile") || arg == "--release"); - let status = Command::new(&cargo) + let status = common + .cargo() .arg("clean") .arg("-q") .args(&self.args) .args(target_triple.map(|triple| format!("--target={triple}"))) .args((!already_profile && try_release).then_some("--release")) - .env("CARGO_TARGET_DIR", super::target_dir().join(target)) + .env("CARGO_TARGET_DIR", target_dir.join(target)) .status() .expect("Error running cargo clean command"); if !status.success() { diff --git a/src/bin/cargo-ziggy/coverage.rs b/src/bin/cargo-ziggy/coverage.rs index e88f1d1..d307c9d 100644 --- a/src/bin/cargo-ziggy/coverage.rs +++ b/src/bin/cargo-ziggy/coverage.rs @@ -1,6 +1,5 @@ -use crate::{Cover, find_target}; -use anyhow::{Context, Result, anyhow, bail}; -use cargo_metadata::camino::Utf8PathBuf; +use crate::{Common, Cover, util::Context, util::Utf8PathBuf}; +use anyhow::{Context as _, Result, anyhow, bail}; use glob::glob; use indicatif::{ProgressBar, ProgressStyle}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; @@ -16,7 +15,8 @@ thread_local! { } impl Cover { - pub fn generate_coverage(&mut self) -> Result<(), anyhow::Error> { + pub fn generate_coverage(&self, common: &Common) -> Result<(), anyhow::Error> { + let cx = Context::new(common, self.target.clone())?; process::Command::new("grcov") .arg("--version") .output() @@ -40,9 +40,6 @@ impl Cover { eprintln!("Generating coverage"); - self.target = - find_target(&self.target).context("⚠️ couldn't find the target to start coverage")?; - if let Some(path) = &self.source && !path.try_exists()? { @@ -50,11 +47,11 @@ impl Cover { } // build the runner - Self::build_runner()?; + Self::build_runner(common)?; if !self.keep { // We remove the previous coverage files - Self::clean_old_cov()?; + Self::clean_old_cov(&cx)?; } let input_path = PathBuf::from( @@ -62,7 +59,7 @@ impl Cover { .display() .to_string() .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) - .replace("{target_name}", &self.target), + .replace("{target_name}", &cx.bin_target), ); let coverage_corpus = if input_path.is_dir() { @@ -87,16 +84,16 @@ impl Cover { .display() .to_string() .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) - .replace("{target_name}", &self.target); + .replace("{target_name}", &cx.bin_target); delete_dir_or_file(&coverage_dir)?; // Get the absolute path for the coverage directory to ensure .profraw files // are created in the correct location, even in workspace scenarios - let base_dir = super::target_dir().join("coverage/debug"); + let base_dir = cx.target_dir.join("coverage/debug"); let coverage_target_dir = base_dir.join("deps"); let cfg = Cfg::new( - base_dir.join(&self.target), + base_dir.join(&cx.bin_target), coverage_target_dir.join("coverage-%p-%m.profraw"), ); @@ -109,7 +106,7 @@ impl Cover { .unwrap() .progress_chars("#>-"), ); - let log_dir = self.ziggy_output.join(format!("{}/logs", &self.target)); + let log_dir = self.ziggy_output.join(format!("{}/logs", &cx.bin_target)); fs::create_dir_all(&log_dir)?; let log_file = std::sync::Mutex::new(std::fs::File::create(log_dir.join("coverage.log"))?); coverage_corpus.into_par_iter().for_each(|file| { @@ -137,7 +134,7 @@ impl Cover { // We generate the code coverage report eprintln!("\n Generating coverage report"); Self::run_grcov( - &self.target, + &cx, output_types, &coverage_dir, &source_or_workspace_root, @@ -146,22 +143,25 @@ impl Cover { } /// Build the runner with the appropriate flags for coverage - pub fn build_runner() -> Result<(), anyhow::Error> { - // The cargo executable - let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); + pub fn build_runner(common: &Common) -> Result<(), anyhow::Error> { + let target_dir = common.target_dir()?; let mut coverage_rustflags = env::var("COVERAGE_RUSTFLAGS").unwrap_or_else(|_| "-Cinstrument-coverage".to_string()); coverage_rustflags.push(' '); coverage_rustflags.push_str(&env::var("RUSTFLAGS").unwrap_or_default()); - let target_dir = format!("--target-dir={}", super::target_dir().join("coverage")); - let build = process::Command::new(&cargo) - .args(["rustc", "--features=ziggy/coverage", &target_dir]) + let build = common + .cargo() + .args([ + "rustc", + "--features=ziggy/coverage", + &format!("--target-dir={}", target_dir.join("coverage")), + ]) .env("RUSTFLAGS", coverage_rustflags) .env( "LLVM_PROFILE_FILE", - super::target_dir().join("coverage/debug/deps/build-%p-%m.profraw"), + target_dir.join("coverage/debug/deps/build-%p-%m.profraw"), ) .spawn() .context("⚠️ couldn't spawn rustc for coverage")? @@ -174,7 +174,7 @@ impl Cover { } pub fn run_grcov( - target: &str, + cx: &Context, output_types: &str, coverage_dir: &str, source_or_workspace_root: &str, @@ -182,8 +182,8 @@ impl Cover { ) -> Result<(), anyhow::Error> { let coverage = process::Command::new("grcov") .args([ - crate::target_dir().join("coverage/debug/deps").as_str(), - &format!("-b={}/coverage/debug/{target}", super::target_dir()), + cx.target_dir.join("coverage/debug/deps").as_str(), + &format!("-b={}/coverage/debug/{}", cx.target_dir, cx.bin_target), &format!("-s={source_or_workspace_root}"), &format!("-t={output_types}"), "--llvm", @@ -202,10 +202,9 @@ impl Cover { Ok(()) } - pub fn clean_old_cov() -> Result<(), anyhow::Error> { + pub fn clean_old_cov(cx: &Context) -> Result<(), anyhow::Error> { // Use absolute path to ensure we clean the correct location in workspaces - let coverage_deps_dir = super::target_dir().join("coverage/debug/deps"); - let pattern = coverage_deps_dir.join("*.profraw"); + let pattern = cx.target_dir.join("*.profraw"); if let Ok(profile_files) = glob(pattern.as_str()) { for file in profile_files.flatten() { diff --git a/src/bin/cargo-ziggy/fuzz.rs b/src/bin/cargo-ziggy/fuzz.rs index 0e6d549..33b6cf5 100644 --- a/src/bin/cargo-ziggy/fuzz.rs +++ b/src/bin/cargo-ziggy/fuzz.rs @@ -1,11 +1,15 @@ -use crate::*; -use anyhow::{Error, anyhow, bail}; +use crate::{ + Build, Common, Cover, Fuzz, FuzzingEngines, Minimize, + util::{Context, ContextView}, +}; +use anyhow::{Context as _, Error, Result, anyhow, bail}; use console::{Term, style}; use glob::glob; use std::{ - fs::File, + fmt, + fs::{self, File}, io::Write, - path::Path, + path::{Path, PathBuf}, process::{self, Stdio}, sync::{Arc, Mutex}, thread, @@ -31,30 +35,29 @@ use strip_ansi_escapes::strip_str; /// ``` /// The `all_afl_corpora` directory corresponds to the `output/target_name/afl/**/queue/` directories. impl Fuzz { - const HFUZZ_NO_CRASH: [&str; 3] = ["README.txt", "HONGGFUZZ.REPORT.TXT", "input"]; - const HFUZZ_TIMEOUT_PREFIX: &str = "SIGVTALRM"; - - pub fn corpus(&self) -> String { - self.corpus + fn output_paths(&self, cx: &Context) -> OutputPaths { + let output_target = if let Some(path) = self.binary.as_ref() { + if let Some(name) = path.file_prefix() { + format!("{}/{}", self.ziggy_output.display(), name.display()) + } else { + self.ziggy_output.display().to_string() + } + } else { + format!("{}/{}", self.ziggy_output.display(), cx.bin_target) + }; + let corpus = self + .corpus .display() .to_string() .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) - .replace("{target_name}", &self.target) - } - - pub fn corpus_tmp(&self) -> String { - format!("{}/corpus_tmp/", self.output_target()) - } - - pub fn corpus_minimized(&self) -> String { - format!("{}/corpus_minimized/", self.output_target(),) - } - - pub fn output_target(&self) -> String { - if self.fuzz_binary() { - self.ziggy_output.display().to_string() - } else { - format!("{}/{}", self.ziggy_output.display(), &self.target) + .replace("{target_name}", &cx.bin_target); + let corpus_tmp = format!("{output_target}/corpus_tmp/"); + let corpus_minimized = format!("{output_target}/corpus_minimized/"); + OutputPaths { + corpus, + corpus_tmp, + corpus_minimized, + output_target, } } @@ -66,7 +69,7 @@ impl Fuzz { /// Returns true if Honggfuzz is enabled // This definition could be a one-liner but it was expanded for clarity pub fn honggfuzz(&self) -> bool { - if self.fuzz_binary() { + if self.binary.is_some() { // We cannot use honggfuzz in binary mode false } else if self.no_afl { @@ -78,77 +81,75 @@ impl Fuzz { } } - fn fuzz_binary(&self) -> bool { - self.binary.is_some() - } - - fn check_bin_target(&self, path: &std::path::Path) -> Result<(), Error> { - if !path.is_file() { - if let Some(path) = self.binary.as_ref() { - bail!("file not found `{}`", path.display()); - } - bail!("no bin target named `{}`", self.target) - } - Ok(()) - } - // Manages the continuous running of fuzzers pub fn fuzz(&mut self, common: &Common) -> Result<(), anyhow::Error> { - if !self.fuzz_binary() { + let cx = if let Some(binary) = self.binary.as_ref() { + self.coverage_worker = false; + if !binary.is_file() { + bail!("file not found `{}`", binary.display()); + } + Context { + target_dir: "target".into(), + bin_target: binary.display().to_string(), + } + } else { + Context::new(common, self.target.clone())? + }; + let cx_view = cx.view(common); + + if self.binary.is_none() { let build = Build { no_afl: !self.afl(), no_honggfuzz: !self.honggfuzz(), release: self.release, asan: self.asan, + target: self.target.clone(), }; - build.build().context("Failed to build the fuzzers")?; + build.build(common).context("Failed to build the fuzzers")?; } - self.target = if let Some(binary) = self.binary.as_ref() { - binary.display().to_string() - } else { - find_target(&self.target).context("⚠️ couldn't find target when fuzzing")? - }; + let paths = self.output_paths(&cx); let time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis(); - let crash_dir = format!("{}/crashes/{time}", self.output_target()); + let crash_dir = format!("{}/crashes/{time}", paths.output_target); let crash_path = Path::new(&crash_dir); - let timeouts_dir = format!("{}/timeouts/{time}", self.output_target()); + let timeouts_dir = format!("{}/timeouts/{time}", paths.output_target); let timeouts_path = Path::new(&timeouts_dir); fs::create_dir_all(crash_path)?; fs::create_dir_all(timeouts_path)?; - fs::create_dir_all(format!("{}/logs", self.output_target()))?; - fs::create_dir_all(format!("{}/queue", self.output_target()))?; + fs::create_dir_all(format!("{}/logs", paths.output_target))?; + fs::create_dir_all(format!("{}/queue", paths.output_target))?; - if Path::new(&self.corpus()).exists() { + if Path::new(&paths.corpus).exists() { if self.minimize { - fs::create_dir_all(self.corpus_tmp()) + fs::create_dir_all(&paths.corpus_tmp) .context("Could not create temporary corpus")?; - self.copy_corpora() + paths + .copy_corpora() .context("Could not move all seeds to temporary corpus")?; - let _ = fs::remove_dir_all(self.corpus_minimized()); - self.run_minimization() + let _ = fs::remove_dir_all(&paths.corpus_minimized); + self.run_minimization(common, &paths) .context("Failure while minimizing")?; - fs::remove_dir_all(self.corpus()).context("Could not remove shared corpus")?; - fs::rename(self.corpus_minimized(), self.corpus()) + fs::remove_dir_all(&paths.corpus).context("Could not remove shared corpus")?; + fs::rename(&paths.corpus_minimized, &paths.corpus) .context("Could not move minimized corpus over")?; - fs::remove_dir_all(self.corpus_tmp()) + fs::remove_dir_all(&paths.corpus_tmp) .context("Could not remove temporary corpus")?; } } else { - fs::create_dir_all(self.corpus())?; + fs::create_dir_all(&paths.corpus)?; } // We create an initial corpus file, so that AFL++ starts up properly if corpus is empty - let is_empty = fs::read_dir(self.corpus())?.next().is_none(); // check if corpus has some seeds + let is_empty = fs::read_dir(&paths.corpus)?.next().is_none(); // check if corpus has some seeds if is_empty { - let mut initial_corpus = File::create(self.corpus() + "/init")?; + let mut initial_corpus = File::create(format!("{}/init", &paths.corpus))?; writeln!(&mut initial_corpus, "00000000")?; drop(initial_corpus); } - let mut processes = self.spawn_new_fuzzers(common)?; + let mut processes = self.spawn_new_fuzzers(cx_view, &paths)?; self.start_time = Instant::now(); @@ -162,23 +163,22 @@ impl Fuzz { // We prepare builds for the coverage worker if self.coverage_worker { - Cover::clean_old_cov()?; - Cover::build_runner()?; + Cover::clean_old_cov(&cx)?; + Cover::build_runner(common)?; } let cov_start_time = Arc::new(Mutex::new(None)); let cov_end_time = Arc::new(Mutex::new(Instant::now())); let coverage_now_running = Arc::new(Mutex::new(false)); - let workspace_root = if !self.fuzz_binary() && self.coverage_worker { - cargo_metadata::MetadataCommand::new() - .exec()? - .workspace_root - .to_string() + let workspace_root = if self.binary.is_none() && self.coverage_worker { + common + .metadata() + .map(|m| m.workspace_root.to_string()) + .unwrap_or_default() } else { String::default() }; - let target = self.target.clone(); - let main_corpus = self.corpus(); - let output_target = self.output_target(); + let main_corpus = &paths.corpus; + let output_target = &paths.output_target; let mut stats = Stats::default(); common.shutdown_deferred(); // handle termination signals gracefully @@ -190,9 +190,9 @@ impl Fuzz { eprintln!("\rShutting down..."); let res = [ stop_fuzzers(&processes), - self.sync_corpora(sync_after).map(|_| ()), - self.sync_crashes(crash_path), - self.sync_timeouts(timeouts_path), + self.sync_corpora(&paths, sync_after).map(|_| ()), + paths.sync_crashes(&cx, crash_path), + paths.sync_timeouts(&cx, timeouts_path), ]; return res.into_iter().fold(Ok(()), std::result::Result::and); } @@ -210,69 +210,72 @@ impl Fuzz { (false, _, _) => String::from("disabled"), }; - let current_stats = self.print_stats(common, &coverage_status); + let current_stats = self.print_stats(cx_view, &paths, &coverage_status); if coverage_status.as_str() == "starting" { *coverage_now_running.lock().unwrap() = true; - let main_corpus = main_corpus.clone(); - let target = target.clone(); - let workspace_root = workspace_root.clone(); - let output_target = output_target.clone(); - let cov_start_time = Arc::clone(&cov_start_time); - let cov_end_time = Arc::clone(&cov_end_time); - let coverage_now_running = Arc::clone(&coverage_now_running); - - thread::spawn(move || { - let mut seen_new_entry = false; - let prev_start_time = { - let unlocked = cov_start_time.lock().unwrap(); - *unlocked - }; - *cov_start_time.lock().unwrap() = Some(Instant::now()); - let profile_bin = super::target_dir().join(format!("coverage/debug/{target}")); - let profile_file = - super::target_dir().join("coverage/debug/deps/coverage-%p-%m.profraw"); - let entries = std::fs::read_dir(&main_corpus).unwrap(); - for entry in entries.flatten().map(|e| e.path()) { - // We only want to run corpus entries created since the last time we ran. - let created = entry - .metadata() - .unwrap() - .created() - .ok() - .and_then(|c| c.elapsed().ok()) - .unwrap_or_default(); - if prev_start_time.map_or(Duration::MAX, |s| s.elapsed()) >= created { - let _ = process::Command::new(&profile_bin) - .arg(entry) - .env("LLVM_PROFILE_FILE", &profile_file) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - seen_new_entry = true; + { + let main_corpus = main_corpus.clone(); + let target = cx.bin_target.clone(); + let workspace_root = workspace_root.clone(); + let output_target = output_target.clone(); + let cov_start_time = Arc::clone(&cov_start_time); + let cov_end_time = Arc::clone(&cov_end_time); + let coverage_now_running = Arc::clone(&coverage_now_running); + let cx = cx.clone(); + thread::spawn(move || { + let mut seen_new_entry = false; + let prev_start_time = { + let unlocked = cov_start_time.lock().unwrap(); + *unlocked + }; + *cov_start_time.lock().unwrap() = Some(Instant::now()); + let profile_bin = cx.target_dir.join(format!("coverage/debug/{target}")); + let profile_file = cx + .target_dir + .join("coverage/debug/deps/coverage-%p-%m.profraw"); + let entries = std::fs::read_dir(&main_corpus).unwrap(); + for entry in entries.flatten().map(|e| e.path()) { + // We only want to run corpus entries created since the last time we ran. + let created = entry + .metadata() + .unwrap() + .created() + .ok() + .and_then(|c| c.elapsed().ok()) + .unwrap_or_default(); + if prev_start_time.map_or(Duration::MAX, |s| s.elapsed()) >= created { + let _ = process::Command::new(&profile_bin) + .arg(entry) + .env("LLVM_PROFILE_FILE", &profile_file) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + seen_new_entry = true; + } } - } - let res = if seen_new_entry { - let coverage_dir = output_target + "/coverage"; - let _ = fs::remove_dir_all(&coverage_dir); - Cover::run_grcov(&target, "html", &coverage_dir, &workspace_root, Some(1)) - } else { - Ok(()) - }; - - { - let mut guard = coverage_now_running.lock().unwrap(); - res.unwrap(); - *guard = false; - } - *cov_end_time.lock().unwrap() = Instant::now(); - }); + let res = if seen_new_entry { + let coverage_dir = output_target + "/coverage"; + let _ = fs::remove_dir_all(&coverage_dir); + Cover::run_grcov(&cx, "html", &coverage_dir, &workspace_root, Some(1)) + } else { + Ok(()) + }; + + { + let mut guard = coverage_now_running.lock().unwrap(); + res.unwrap(); + *guard = false; + } + *cov_end_time.lock().unwrap() = Instant::now(); + }); + } } if !afl_output_ok && let Ok(afl_log) = - fs::read_to_string(format!("{}/logs/afl.log", self.output_target())) + fs::read_to_string(format!("{}/logs/afl.log", &paths.output_target)) { if afl_log.contains("ready to roll") { afl_output_ok = true; @@ -293,16 +296,16 @@ impl Fuzz { // Copy crash files from AFL++ and Honggfuzz's outputs if current_stats.crashes != stats.crashes { - self.sync_crashes(crash_path)?; + paths.sync_crashes(&cx, crash_path)?; } if current_stats.timeouts != stats.timeouts { - self.sync_timeouts(timeouts_path)?; + paths.sync_timeouts(&cx, timeouts_path)?; } stats = current_stats; // Sync corpus dirs if last_sync_time.elapsed() > Duration::from_mins(self.corpus_sync_interval) { - sync_after.replace(self.sync_corpora(sync_after)?); + sync_after.replace(self.sync_corpora(&paths, sync_after)?); last_sync_time = Instant::now(); } @@ -316,72 +319,23 @@ impl Fuzz { } } - /// Copy crashes from AFL++ or Honggfuzz's outputs into `target_dir` - fn sync_crashes(&self, target_dir: &Path) -> Result<(), anyhow::Error> { - // afl - for dir in glob(&format!("{}/afl/*/crashes", self.output_target())) - .map_err(|_| anyhow!("Failed to read crashes glob pattern"))? - .flatten() - { - copy_from_dir(dir, target_dir, |_| true)?; - } - - // honggfuzz - copy_from_dir( - PathBuf::from(format!( - "{}/honggfuzz/{}", - self.output_target(), - self.target - )), - target_dir, - |file_name| { - let no_timeout = !BStr(file_name).starts_with(Self::HFUZZ_TIMEOUT_PREFIX); - no_timeout && Self::HFUZZ_NO_CRASH.iter().all(|name| *name != file_name) - }, - )?; - - Ok(()) - } - - fn sync_timeouts(&self, target_dir: &Path) -> Result<(), anyhow::Error> { - // afl - for dir in glob(&format!("{}/afl/*/hangs", self.output_target())) - .map_err(|_| anyhow!("Failed to read timeouts glob pattern"))? - .flatten() - { - copy_from_dir(dir, target_dir, |_| true)?; - } - - // honggfuzz - copy_from_dir( - PathBuf::from(format!( - "{}/honggfuzz/{}", - self.output_target(), - self.target - )), - target_dir, - |file_name| { - let is_timeout = BStr(file_name).starts_with(Self::HFUZZ_TIMEOUT_PREFIX); - is_timeout && Self::HFUZZ_NO_CRASH.iter().all(|name| *name != file_name) - }, - )?; - - Ok(()) - } - /// Sync shared corpora /// /// Copy-over each live corpus to the shared corpus directory, where each file name is usually its hash. /// If both fuzzers are running, copy over AFL++'s queue for consumption by Honggfuzz. - fn sync_corpora(&self, newer_then: Option) -> Result { + fn sync_corpora( + &self, + paths: &OutputPaths, + newer_then: Option, + ) -> Result { let now = SystemTime::now(); let afl_files = glob(&format!( "{}/afl/mainaflfuzzer/queue/*", - self.output_target(), + paths.output_target, ))? .flatten() .zip(std::iter::repeat(Fuzzer::Afl)); - let hfuzz_files = glob(&format!("{}/honggfuzz/corpus/*", self.output_target()))? + let hfuzz_files = glob(&format!("{}/honggfuzz/corpus/*", paths.output_target))? .flatten() .zip(std::iter::repeat(Fuzzer::Honggfuzz)); @@ -397,8 +351,8 @@ impl Fuzz { false }); - let queue_path = PathBuf::from(format!("{}/queue", self.output_target())); - let corpus_path = PathBuf::from(format!("{}/corpus", self.output_target())); + let queue_path = PathBuf::from(format!("{}/queue", paths.output_target)); + let corpus_path = PathBuf::from(format!("{}/corpus", paths.output_target)); for (file, fuzzer) in new_files { if matches!(fuzzer, Fuzzer::Afl) && self.honggfuzz() @@ -458,7 +412,11 @@ impl Fuzz { } // Spawns new fuzzers - pub fn spawn_new_fuzzers(&self, common: &Common) -> Result, anyhow::Error> { + pub fn spawn_new_fuzzers( + &self, + cx: ContextView, + paths: &OutputPaths, + ) -> Result, anyhow::Error> { // No fuzzers for you if self.no_afl && self.no_honggfuzz { bail!("Pick at least one fuzzer.\nNote: -b/--binary implies --no-honggfuzz"); @@ -471,7 +429,7 @@ impl Fuzz { let (afl_jobs, honggfuzz_jobs) = { if self.no_afl { (0, self.jobs) - } else if self.no_honggfuzz || self.fuzz_binary() { + } else if self.no_honggfuzz || self.binary.is_some() { (self.jobs, 0) } else { // we assign roughly 2/3 to AFL++, 1/3 to honggfuzz, however do @@ -486,7 +444,7 @@ impl Fuzz { } if afl_jobs > 0 { - std::fs::create_dir_all(format!("{}/afl", self.output_target()))?; + std::fs::create_dir_all(format!("{}/afl", paths.output_target))?; // https://aflplus.plus/docs/fuzzing_in_depth/#c-using-multiple-cores let afl_modes = [ @@ -503,7 +461,7 @@ impl Fuzz { }; // We only sync to the shared corpus if Honggfuzz is also running let use_shared_corpus = match (self.no_honggfuzz, job_num) { - (false, 0) => format!("-F{}", &self.corpus()), + (false, 0) => format!("-F{}", &paths.corpus), _ => String::new(), }; let use_initial_corpus_dir = match (&self.initial_corpus, job_num) { @@ -553,14 +511,16 @@ impl Fuzz { _ => "-Pexploit", }; let input_format_option = self.config.input_format_flag(); - let log_destination = || match job_num { - 0 => File::create(format!("{}/logs/afl.log", self.output_target())) - .unwrap() - .into(), - 1 => File::create(format!("{}/logs/afl_1.log", self.output_target())) - .unwrap() - .into(), - _ => process::Stdio::null(), + let log_destination = match job_num { + 0 => Some(File::create(format!( + "{}/logs/afl.log", + paths.output_target + ))?), + 1 => Some(File::create(format!( + "{}/logs/afl_1.log", + paths.output_target + ))?), + _ => None, }; let final_sync = match job_num { 0 => "AFL_FINAL_SYNC", @@ -568,24 +528,23 @@ impl Fuzz { }; let target_path = self.binary.clone().unwrap_or_else(|| { if self.release { - super::target_dir() - .join(format!("afl/release/{}", self.target)) + cx.target_dir() + .join(format!("afl/release/{}", cx.bin_target())) .into_std_path_buf() } else if self.asan && job_num == 0 { - super::target_dir() + cx.target_dir() .join(format!( "afl/{}/debug/{}", target_triple::TARGET, - self.target + cx.bin_target() )) .into_std_path_buf() } else { - super::target_dir() - .join(format!("afl/debug/{}", self.target)) + cx.target_dir() + .join(format!("afl/debug/{}", cx.bin_target())) .into_std_path_buf() } }); - self.check_bin_target(target_path.as_path())?; let mut afl_flags = self.afl_flags.clone(); if is_main_instance { @@ -595,16 +554,16 @@ impl Fuzz { } fuzzer_handles.push( - common + cx.common() .cargo() .args( [ "afl", "fuzz", &fuzzer_name, - &format!("-i{}", self.corpus()), + &format!("-i{}", paths.corpus), &format!("-p{power_schedule}"), - &format!("-o{}/afl", self.output_target()), + &format!("-o{}/afl", paths.output_target), &format!("-g{}", self.min_length), &format!("-G{}", self.max_length), &use_shared_corpus, @@ -636,8 +595,14 @@ impl Fuzz { .env(final_sync, "1") .env("AFL_IGNORE_SEED_PROBLEMS", "1") .env("AFL_PIZZA_MODE", "-1") - .stdout(log_destination()) - .stderr(log_destination()) + .stdout( + log_destination + .as_ref() + .map(std::fs::File::try_clone) + .transpose()? + .map_or_else(process::Stdio::null, Into::into), + ) + .stderr(log_destination.map_or_else(process::Stdio::null, Into::into)) .spawn()?, ); } @@ -645,22 +610,13 @@ impl Fuzz { } if honggfuzz_jobs > 0 { - self.check_bin_target( - super::target_dir() - .join("honggfuzz") - .join(target_triple::TARGET) - .join("release") - .join(&self.target) - .as_std_path(), - )?; - let run_args = { let mut run_args = String::new(); - run_args.push_str(&format!(" --input={}", self.corpus())); - run_args.push_str(&format!(" -o{}/honggfuzz/corpus", self.output_target())); + run_args.push_str(&format!(" --input={}", paths.corpus)); + run_args.push_str(&format!(" -o{}/honggfuzz/corpus", paths.output_target)); run_args.push_str(&format!(" -n{honggfuzz_jobs}")); run_args.push_str(&format!(" -F{}", self.max_length)); - run_args.push_str(&format!(" --dynamic_input={}/queue", self.output_target())); + run_args.push_str(&format!(" --dynamic_input={}/queue", paths.output_target)); run_args.push_str(" --tmout_sigvtalrm"); if let Some(t) = self.timeout { run_args.push_str(&format!(" -t{t}")); @@ -675,7 +631,7 @@ impl Fuzz { run_args }; - let log = File::create(format!("{}/logs/honggfuzz.log", self.output_target()))?; + let log = File::create(format!("{}/logs/honggfuzz.log", paths.output_target))?; // The `script` invocation is a trick to get the correct TTY output for honggfuzz fuzzer_handles.push( @@ -684,14 +640,18 @@ impl Fuzz { "--flush", "--quiet", "-c", - &format!("{} hfuzz run {}", common.cargo_path.display(), &self.target), + &format!( + "{} hfuzz run {}", + cx.common().cargo_path.display(), + &cx.bin_target() + ), "/dev/null", ]) .env("HFUZZ_BUILD_ARGS", "--features=ziggy/honggfuzz") - .env("CARGO_TARGET_DIR", super::target_dir().join("honggfuzz")) + .env("CARGO_TARGET_DIR", cx.target_dir().join("honggfuzz")) .env( "HFUZZ_WORKSPACE", - format!("{}/honggfuzz", self.output_target()), + format!("{}/honggfuzz", paths.output_target), ) .env("HFUZZ_RUN_ARGS", &run_args) .stdin(std::process::Stdio::null()) @@ -709,13 +669,13 @@ impl Fuzz { if afl_jobs > 0 { eprintln!( " {}", - style(format!("tail -f {}/logs/afl.log", self.output_target())).bold() + style(format!("tail -f {}/logs/afl.log", paths.output_target)).bold() ); } if afl_jobs > 1 { eprintln!( " {}", - style(format!("tail -f {}/logs/afl_1.log", self.output_target())).bold() + style(format!("tail -f {}/logs/afl_1.log", paths.output_target)).bold() ); } if honggfuzz_jobs > 0 { @@ -723,7 +683,7 @@ impl Fuzz { " {}", style(format!( "tail -f {}/logs/honggfuzz.log", - self.output_target() + paths.output_target )) .bold() ); @@ -732,37 +692,7 @@ impl Fuzz { Ok(fuzzer_handles) } - fn all_seeds(&self) -> Result> { - Ok(glob(&format!("{}/afl/*/queue/*", self.output_target())) - .map_err(|_| anyhow!("Failed to read AFL++ queue glob pattern"))? - .chain( - glob(&format!("{}/*", self.corpus())) - .map_err(|_| anyhow!("Failed to read Honggfuzz corpus glob pattern"))?, - ) - .flatten() - .filter(|f| f.is_file()) - .collect()) - } - - // Copy all corpora into `corpus` - pub fn copy_corpora(&self) -> Result<()> { - self.all_seeds()?.iter().for_each(|s| { - let _ = fs::copy( - s.to_str().unwrap_or_default(), - format!( - "{}/{}", - &self.corpus_tmp(), - s.file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default(), - ), - ); - }); - Ok(()) - } - - pub fn run_minimization(&self) -> Result<()> { + pub fn run_minimization(&self, common: &Common, paths: &OutputPaths) -> Result<()> { let term = Term::stdout(); term.write_line(&format!( @@ -770,8 +700,8 @@ impl Fuzz { &style("Running minimization").magenta().bold() ))?; - let input_corpus = &self.corpus_tmp(); - let minimized_corpus = &self.corpus_minimized(); + let input_corpus = &paths.corpus_tmp; + let minimized_corpus = &paths.corpus_minimized; let old_corpus_size = fs::read_dir(input_corpus).map_or_else( |_| String::from("err"), @@ -786,7 +716,7 @@ impl Fuzz { (true, true, _) => bail!("Pick at least one fuzzer"), }; - let mut minimization_args = Minimize { + let minimization_args = Minimize { target: self.target.clone(), input_corpus: PathBuf::from(input_corpus), output_corpus: PathBuf::from(minimized_corpus), @@ -795,7 +725,7 @@ impl Fuzz { timeout: self.timeout.unwrap_or(5000), engine, }; - match minimization_args.minimize() { + match minimization_args.minimize(common) { Ok(()) => { let new_corpus_size = fs::read_dir(minimized_corpus).map_or_else( |_| String::from("err"), @@ -823,8 +753,13 @@ impl Fuzz { Ok(()) } - pub fn print_stats(&self, common: &Common, cov_worker_status: &str) -> Stats { - let fuzzer_name = format!(" {} ", self.target); + pub fn print_stats( + &self, + cx: ContextView, + paths: &OutputPaths, + cov_worker_status: &str, + ) -> Stats { + let fuzzer_name = format!(" {} ", cx.bin_target()); let reset = "\x1b[0m"; let gray = "\x1b[1;90m"; @@ -848,13 +783,14 @@ impl Fuzz { if !self.afl() { afl_status = format!("{yellow}disabled{reset} "); } else { - let afl_stats_process = common + let afl_stats_process = cx + .common() .cargo() .args([ "afl", "whatsup", "-s", - &format!("{}/afl", self.output_target()), + &format!("{}/afl", paths.output_target), ]) .output(); @@ -909,7 +845,7 @@ impl Fuzz { let hf_stats_process = process::Command::new("tail") .args([ "-n300", - &format!("{}/logs/honggfuzz.log", self.output_target()), + &format!("{}/logs/honggfuzz.log", paths.output_target), ]) .output(); if let Ok(process) = hf_stats_process { @@ -1068,7 +1004,7 @@ impl Fuzz { } } -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, Debug)] pub enum FuzzingConfig { Generic, Binary, @@ -1086,8 +1022,6 @@ impl FuzzingConfig { } } -use std::fmt; - impl fmt::Display for FuzzingConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") @@ -1152,3 +1086,96 @@ impl BStr<'_> { pat.len() <= self.0.len() && &self.0.as_encoded_bytes()[..pat.len()] == pat.as_bytes() } } + +pub struct OutputPaths { + corpus: String, + corpus_tmp: String, + corpus_minimized: String, + output_target: String, +} + +impl OutputPaths { + const HFUZZ_NO_CRASH: [&str; 3] = ["README.txt", "HONGGFUZZ.REPORT.TXT", "input"]; + const HFUZZ_TIMEOUT_PREFIX: &str = "SIGVTALRM"; + + fn all_seeds(&self) -> Result> { + Ok(glob(&format!("{}/afl/*/queue/*", self.output_target)) + .map_err(|_| anyhow!("Failed to read AFL++ queue glob pattern"))? + .chain( + glob(&format!("{}/*", self.corpus)) + .map_err(|_| anyhow!("Failed to read Honggfuzz corpus glob pattern"))?, + ) + .flatten() + .filter(|f| f.is_file()) + .collect()) + } + + // Copy all corpora into `corpus` + pub fn copy_corpora(&self) -> Result<()> { + self.all_seeds()?.iter().for_each(|s| { + let _ = fs::copy( + s.to_str().unwrap_or_default(), + format!( + "{}/{}", + &self.corpus_tmp, + s.file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + ), + ); + }); + Ok(()) + } + + /// Copy crashes from AFL++ or Honggfuzz's outputs into `target_dir` + fn sync_crashes(&self, cx: &Context, target_dir: &Path) -> Result<(), anyhow::Error> { + // afl + for dir in glob(&format!("{}/afl/*/crashes", self.output_target)) + .map_err(|_| anyhow!("Failed to read crashes glob pattern"))? + .flatten() + { + copy_from_dir(dir, target_dir, |_| true)?; + } + + // honggfuzz + copy_from_dir( + PathBuf::from(format!( + "{}/honggfuzz/{}", + self.output_target, cx.bin_target + )), + target_dir, + |file_name| { + let no_timeout = !BStr(file_name).starts_with(Self::HFUZZ_TIMEOUT_PREFIX); + no_timeout && Self::HFUZZ_NO_CRASH.iter().all(|name| *name != file_name) + }, + )?; + + Ok(()) + } + + fn sync_timeouts(&self, cx: &Context, target_dir: &Path) -> Result<(), anyhow::Error> { + // afl + for dir in glob(&format!("{}/afl/*/hangs", self.output_target)) + .map_err(|_| anyhow!("Failed to read timeouts glob pattern"))? + .flatten() + { + copy_from_dir(dir, target_dir, |_| true)?; + } + + // honggfuzz + copy_from_dir( + PathBuf::from(format!( + "{}/honggfuzz/{}", + self.output_target, cx.bin_target + )), + target_dir, + |file_name| { + let is_timeout = BStr(file_name).starts_with(Self::HFUZZ_TIMEOUT_PREFIX); + is_timeout && Self::HFUZZ_NO_CRASH.iter().all(|name| *name != file_name) + }, + )?; + + Ok(()) + } +} diff --git a/src/bin/cargo-ziggy/main.rs b/src/bin/cargo-ziggy/main.rs index 5af6458..dd74b44 100644 --- a/src/bin/cargo-ziggy/main.rs +++ b/src/bin/cargo-ziggy/main.rs @@ -10,10 +10,9 @@ mod triage; mod util; use crate::fuzz::FuzzingConfig; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result, anyhow, bail}; use clap::{Args, Parser, Subcommand, ValueEnum}; use std::{ - fs, path::PathBuf, sync::OnceLock, sync::{Arc, atomic::AtomicBool}, @@ -87,6 +86,10 @@ pub enum Ziggy { #[derive(Args)] pub struct Build { + /// Target to build + #[clap(value_name = "TARGET")] + target: Option, + /// No AFL++ (Fuzz only with honggfuzz) #[clap(long = "no-afl", action)] no_afl: bool, @@ -107,8 +110,8 @@ pub struct Build { #[derive(Args)] pub struct Fuzz { /// Target to fuzz - #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] - target: String, + #[clap(value_name = "TARGET")] + target: Option, /// Shared corpus directory #[clap(short, long, value_parser, value_name = "DIR", default_value = DEFAULT_CORPUS_DIR)] @@ -204,8 +207,8 @@ pub struct Fuzz { #[derive(Args)] pub struct Run { /// Target to use - #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] - target: String, + #[clap(value_name = "TARGET")] + target: Option, /// Input directories and/or files to run #[clap(short, long, value_name = "DIR", default_value = DEFAULT_CORPUS_DIR)] @@ -241,8 +244,8 @@ pub struct Run { #[derive(Args, Clone)] pub struct Minimize { /// Target to use - #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] - target: String, + #[clap(value_name = "TARGET")] + target: Option, /// Corpus directory to minimize #[clap(short, long, default_value = DEFAULT_CORPUS_DIR)] @@ -273,8 +276,8 @@ pub struct Minimize { #[derive(Args)] pub struct Cover { /// Target to generate coverage for - #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] - target: String, + #[clap(value_name = "TARGET")] + target: Option, /// Output directory for code coverage report #[clap(short, long, value_parser, value_name = "DIR", default_value = DEFAULT_COVERAGE_DIR)] @@ -310,8 +313,8 @@ pub struct Cover { #[derive(Args)] pub struct Plot { /// Target to generate plot for - #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] - target: String, + #[clap(value_name = "TARGET")] + target: Option, /// Name of AFL++ fuzzer to use as data source #[clap(short, long, value_name = "NAME", default_value = "mainaflfuzzer")] @@ -331,8 +334,8 @@ pub struct Plot { #[derive(Args)] pub struct Triage { /// Target to use - #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] - target: String, + #[clap(value_name = "TARGET")] + target: Option, /// Triage output directory to be written to (will be overwritten) #[clap(short, long, value_name = "DIR", default_value = DEFAULT_TRIAGE_DIR)] @@ -361,8 +364,8 @@ pub struct Triage { #[derive(Args)] pub struct AddSeeds { /// Target to use - #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] - target: String, + #[clap(value_name = "TARGET")] + target: Option, /// Seeds directory to be added #[clap(short, long, value_parser, value_name = "DIR")] @@ -388,6 +391,7 @@ pub struct Common { sigs_done: Option<()>, pub cargo_path: PathBuf, runtime: OnceLock, + metadata: OnceLock>, } impl Common { @@ -399,6 +403,7 @@ impl Common { .unwrap_or_else(|_| String::from("cargo")) .into(), runtime: OnceLock::new(), + metadata: OnceLock::new(), } } fn is_terminated(&self) -> bool { @@ -445,6 +450,49 @@ impl Common { .expect("Failed building tokio runtime") }) } + + /// Cached `cargo metadata` + fn metadata(&self) -> Option<&cargo_metadata::Metadata> { + self.metadata + .get_or_init(|| cargo_metadata::MetadataCommand::new().exec().ok()) + .as_ref() + } + + fn target_dir(&self) -> Result<&util::Utf8PathBuf> { + self.metadata() + .map(|metadata| &metadata.target_directory) + .ok_or_else(|| anyhow!("not in a Cargo workspace")) + } + + fn guess_bin(&self) -> Result { + let meta = self + .metadata() + .ok_or_else(|| anyhow!("failed running cargo metadata"))?; + + if meta.workspace_default_members.is_missing() { + bail!("please specify a target") + } + let bins: Vec<&str> = meta + .workspace_default_packages() + .into_iter() + .flat_map(|p| { + p.targets + .iter() + .filter_map(|t| t.is_bin().then_some(t.name.as_str())) + }) + .collect(); + if bins.len() == 1 { + return Ok(bins[0].to_owned()); + } + bail!( + "please specify a target\nhelp: available targets:\n\t{}", + bins.join("\n\t") + ); + } + + fn resolve_bin(&self, target: Option) -> Result { + target.ok_or(()).or_else(|()| self.guess_bin()) + } } fn main() -> Result<(), anyhow::Error> { @@ -454,60 +502,24 @@ fn main() -> Result<(), anyhow::Error> { let Cargo::Ziggy(command) = Cargo::parse(); match command { - Ziggy::Build(args) => args.build().context("Failed to build the fuzzers"), + Ziggy::Build(args) => args.build(&common).context("Failed to build the fuzzers"), Ziggy::Fuzz(mut args) => args.fuzz(&common).context("Failure running fuzzers"), Ziggy::Run(mut args) => args.run(&common).context("Failure running inputs"), - Ziggy::Minimize(mut args) => args.minimize().context("Failure running minimization"), - Ziggy::Cover(mut args) => args - .generate_coverage() + Ziggy::Minimize(args) => args + .minimize(&common) + .context("Failure running minimization"), + Ziggy::Cover(args) => args + .generate_coverage(&common) .context("Failure generating coverage"), - Ziggy::Plot(mut args) => args.generate_plot().context("Failure generating plot"), - Ziggy::AddSeeds(args) => args.add_seeds().context("Failure adding seeds to AFL"), - Ziggy::Triage(args) => args.triage().context("Failure triaging with casr"), - Ziggy::Clean(args) => args.clean().context("Failure cleaning build artifacts"), + Ziggy::Plot(args) => args + .generate_plot(&common) + .context("Failure generating plot"), + Ziggy::AddSeeds(args) => args + .add_seeds(&common) + .context("Failure adding seeds to AFL"), + Ziggy::Triage(args) => args.triage(&common).context("Failure triaging with casr"), + Ziggy::Clean(args) => args + .clean(&common) + .context("Failure cleaning build artifacts"), } } - -pub fn find_target(target: &String) -> Result { - // If the target is already set, we're done here - if target != DEFAULT_UNMODIFIED_TARGET { - return Ok(target.into()); - } - - let new_target_result = guess_target(); - - new_target_result.context("Target is not obvious") -} - -fn guess_target() -> Result { - let metadata = cargo_metadata::MetadataCommand::new().exec()?; - let default_bin = metadata - .workspace_default_members - .iter() - .find_map(|default_pkg| { - metadata - .packages - .iter() - .find(|p| p.id == *default_pkg) - .and_then(|p| { - p.targets - .iter() - .find_map(|target| target.is_bin().then(|| target.name.clone())) - }) - }); - - default_bin.ok_or_else(|| anyhow!("Please specify a target")) -} - -fn target_dir() -> &'static cargo_metadata::camino::Utf8PathBuf { - use std::sync::LazyLock; - - static TARGET_DIR: LazyLock = LazyLock::new(|| { - cargo_metadata::MetadataCommand::new().exec().map_or_else( - |_| cargo_metadata::camino::Utf8PathBuf::from("target"), - |metadata| metadata.target_directory, - ) - }); - - &TARGET_DIR -} diff --git a/src/bin/cargo-ziggy/minimize.rs b/src/bin/cargo-ziggy/minimize.rs index fc8efa5..19f3e42 100644 --- a/src/bin/cargo-ziggy/minimize.rs +++ b/src/bin/cargo-ziggy/minimize.rs @@ -1,113 +1,116 @@ -use crate::{Build, FuzzingEngines, Minimize, find_target}; -use anyhow::{Context, Result, bail}; +use crate::{ + Build, Common, FuzzingEngines, Minimize, + util::{Context, ContextView, hash_file}, +}; +use anyhow::{Context as _, Result, bail}; use std::{ - env, fs::{self, File}, - process, thread, + thread, time::Duration, }; -use twox_hash::XxHash64; impl Minimize { - pub fn minimize(&mut self) -> Result<(), anyhow::Error> { + pub fn minimize(&self, common: &Common) -> Result<(), anyhow::Error> { + let cx = Context::new(common, self.target.clone())?; + let cx_view = cx.view(common); let build = Build { no_afl: self.engine == FuzzingEngines::Honggfuzz, no_honggfuzz: self.engine == FuzzingEngines::AFLPlusPlus, release: false, asan: false, + target: Some(cx.bin_target.clone()), }; - build.build().context("Failed to build the fuzzers")?; - - self.target = - find_target(&self.target).context("⚠️ couldn't find target when minimizing")?; + build.build(common).context("Failed to build the fuzzers")?; - if fs::read_dir(self.output_corpus()).is_ok() { + if fs::read_dir(self.output_corpus(&cx)).is_ok() { bail!( "Directory {} exists, please move it before running minimization", - self.output_corpus() + self.output_corpus(&cx) ); } - let entries = fs::read_dir(self.input_corpus())?; + let entries = fs::read_dir(self.input_corpus(&cx))?; let original_count = entries.flatten().count(); println!("Running minimization on a corpus of {original_count} files"); match self.engine { FuzzingEngines::All => { - let min_afl = self.clone(); - let handle_afl = thread::spawn(move || { - min_afl.minimize_afl().unwrap(); - }); - thread::sleep(Duration::from_millis(1000)); - - let min_honggfuzz = self.clone(); - let handle_honggfuzz = thread::spawn(move || { - min_honggfuzz.minimize_honggfuzz().unwrap(); - }); - - handle_afl.join().unwrap(); - handle_honggfuzz.join().unwrap(); + std::thread::scope(|s| -> Result<()> { + let handle_afl = { s.spawn(move || self.minimize_afl(cx_view)) }; + thread::sleep(Duration::from_millis(1000)); + let handle_honggfuzz = { s.spawn(move || self.minimize_honggfuzz(cx_view)) }; + + handle_afl + .join() + .unwrap() + .and_then(|()| handle_honggfuzz.join().unwrap()) + })?; } FuzzingEngines::AFLPlusPlus => { - self.minimize_afl()?; + self.minimize_afl(cx_view)?; } FuzzingEngines::Honggfuzz => { - self.minimize_honggfuzz()?; + self.minimize_honggfuzz(cx_view)?; } } - // We rename every file to its md5 hash - let min_entries = fs::read_dir(self.output_corpus())?; + // We rename every file to its hash + let out_dir = self.output_corpus(&cx); + let min_entries = fs::read_dir(self.output_corpus(&cx))?; for file in min_entries.flatten() { - let bytes = fs::read(file.path()).unwrap_or_default(); - let hash = XxHash64::oneshot(0, &bytes); - let _ = fs::rename(file.path(), format!("{}/{hash:x}", self.output_corpus())); + if let Ok(hash) = hash_file(file.path().as_path()) { + let _ = fs::rename(file.path(), format!("{out_dir}/{hash:x}")); + } } - let min_entries_hashed = fs::read_dir(self.output_corpus())?; + let min_entries_hashed = fs::read_dir(self.output_corpus(&cx))?; let minimized_count = min_entries_hashed.flatten().count(); println!("Minimized corpus contains {minimized_count} files"); Ok(()) } - fn input_corpus(&self) -> String { + fn input_corpus(&self, cx: &Context) -> String { self.input_corpus .display() .to_string() .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) - .replace("{target_name}", &self.target) + .replace("{target_name}", &cx.bin_target) } - fn output_corpus(&self) -> String { + fn output_corpus(&self, cx: &Context) -> String { self.output_corpus .display() .to_string() .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) - .replace("{target_name}", &self.target) + .replace("{target_name}", &cx.bin_target) } // AFL++ minimization - fn minimize_afl(&self) -> Result<(), anyhow::Error> { + fn minimize_afl(&self, cx: ContextView) -> Result<(), anyhow::Error> { println!("Minimizing with AFL++"); - // The cargo executable - let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); let jobs_option = match self.jobs { 0 | 1 => String::from("all"), t => format!("{t}"), }; - let target_dir = super::target_dir().join("afl/debug").join(&self.target); + let target_dir = cx.target_dir().join("afl/debug").join(cx.bin_target()); // AFL++ minimization - process::Command::new(&cargo) + let log_file = File::create(format!( + "{}/{}/logs/minimization_afl.log", + &self.ziggy_output.display(), + cx.bin_target(), + ))?; + cx.common() + .cargo() .args([ "afl", "cmin", "-i", - &self.input_corpus(), + &self.input_corpus(cx.as_ref()), "-o", - &self.output_corpus(), + &self.output_corpus(cx.as_ref()), "-T", &jobs_option, "-t", @@ -115,58 +118,46 @@ impl Minimize { "--", target_dir.as_str(), ]) - .stderr(File::create(format!( - "{}/{}/logs/minimization_afl.log", - &self.ziggy_output.display(), - &self.target, - ))?) - .stdout(File::create(format!( - "{}/{}/logs/minimization_afl.log", - &self.ziggy_output.display(), - &self.target, - ))?) + .stderr(log_file.try_clone()?) + .stdout(log_file) .spawn()? .wait()?; Ok(()) } // HONGGFUZZ minimization - fn minimize_honggfuzz(&self) -> Result<(), anyhow::Error> { + fn minimize_honggfuzz(&self, cx: ContextView) -> Result<(), anyhow::Error> { println!("Minimizing with honggfuzz"); - // The cargo executable - let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); - process::Command::new(&cargo) - .args(["hfuzz", "run", &self.target]) - .env("CARGO_TARGET_DIR", super::target_dir().join("honggfuzz")) + let log_file = File::create(format!( + "{}/{}/logs/minimization_honggfuzz.log", + &self.ziggy_output.display(), + cx.bin_target(), + ))?; + cx.common() + .cargo() + .args(["hfuzz", "run", cx.bin_target()]) + .env("CARGO_TARGET_DIR", cx.target_dir().join("honggfuzz")) .env("HFUZZ_BUILD_ARGS", "--features=ziggy/honggfuzz") .env( "HFUZZ_WORKSPACE", format!( "{}/{}/honggfuzz", &self.ziggy_output.display(), - &self.target + cx.bin_target() ), ) .env( "HFUZZ_RUN_ARGS", format!( "-i{} -M -o{} -t{}", - &self.input_corpus(), - &self.output_corpus(), + &self.input_corpus(cx.as_ref()), + &self.output_corpus(cx.as_ref()), self.timeout ), ) - .stderr(File::create(format!( - "{}/{}/logs/minimization_honggfuzz.log", - &self.ziggy_output.display(), - &self.target, - ))?) - .stdout(File::create(format!( - "{}/{}/logs/minimization_honggfuzz.log", - &self.ziggy_output.display(), - &self.target, - ))?) + .stderr(log_file.try_clone()?) + .stdout(log_file) .spawn()? .wait()?; Ok(()) diff --git a/src/bin/cargo-ziggy/plot.rs b/src/bin/cargo-ziggy/plot.rs index d292f91..790ccfe 100644 --- a/src/bin/cargo-ziggy/plot.rs +++ b/src/bin/cargo-ziggy/plot.rs @@ -1,21 +1,16 @@ -use crate::{Plot, find_target}; -use anyhow::{Context, Result}; -use std::{env, process}; +use crate::{Common, Plot, util::Context}; +use anyhow::{Context as _, Result}; impl Plot { - pub fn generate_plot(&mut self) -> Result<(), anyhow::Error> { + pub fn generate_plot(&self, common: &Common) -> Result<(), anyhow::Error> { eprintln!("Generating plot"); - self.target = - find_target(&self.target).context("⚠️ couldn't find the target for plotting")?; - - // The cargo executable - let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); + let cx = Context::new(common, self.target.clone())?; let fuzzer_data_dir = format!( "{}/{}/afl/{}/", - &self.ziggy_output.display(), - &self.target, + self.ziggy_output.display(), + cx.bin_target, &self.input ); @@ -24,12 +19,13 @@ impl Plot { .display() .to_string() .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) - .replace("{target_name}", &self.target); + .replace("{target_name}", &cx.bin_target); println!("{plot_dir}"); - println!("{}", self.target); + println!("{}", cx.bin_target); // We run the afl-plot command - process::Command::new(&cargo) + common + .cargo() .args(["afl", "plot", &fuzzer_data_dir, &plot_dir]) .spawn() .context("⚠️ couldn't spawn afl plot")? diff --git a/src/bin/cargo-ziggy/run.rs b/src/bin/cargo-ziggy/run.rs index d905a73..f8288a0 100644 --- a/src/bin/cargo-ziggy/run.rs +++ b/src/bin/cargo-ziggy/run.rs @@ -1,5 +1,5 @@ -use crate::{Common, Run, find_target}; -use anyhow::{Context, Result, bail}; +use crate::{Common, Run, util::Context}; +use anyhow::{Context as _, Result, bail}; use console::style; use std::{ collections::HashSet, @@ -11,10 +11,10 @@ use std::{ impl Run { // Run inputs pub fn run(&mut self, common: &Common) -> Result<(), anyhow::Error> { - let target = find_target(&self.target)?; - let target_dir = format!("--target-dir={}", super::target_dir().join("runner")); + let cx = Context::new(common, self.target.clone())?; + let target_arg = format!("--target-dir={}", cx.target_dir.join("runner")); - let mut args = vec!["rustc", &target_dir]; + let mut args = vec!["rustc", &target_arg]; let asan_target_str = format!("--target={}", target_triple::TARGET); let mut rust_flags = env::var("RUSTFLAGS").unwrap_or_default(); let mut rust_doc_flags = env::var("RUSTDOCFLAGS").unwrap_or_default(); @@ -72,7 +72,7 @@ impl Run { .display() .to_string() .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) - .replace("{target_name}", &target); + .replace("{target_name}", &cx.bin_target); // For each directory we read, we get all files in that directory let path = PathBuf::from(canonical_name); if path.is_dir() { @@ -89,9 +89,14 @@ impl Run { .collect(); let runner_path = if self.asan { - super::target_dir().join(format!("runner/{}/debug/{target}", target_triple::TARGET)) + cx.target_dir.join(format!( + "runner/{}/debug/{}", + target_triple::TARGET, + cx.bin_target + )) } else { - super::target_dir().join(format!("runner/debug/{target}")) + cx.target_dir + .join(format!("runner/debug/{}", cx.bin_target)) }; let runner = Runner::new( diff --git a/src/bin/cargo-ziggy/triage.rs b/src/bin/cargo-ziggy/triage.rs index 2d351d2..dbf97f8 100644 --- a/src/bin/cargo-ziggy/triage.rs +++ b/src/bin/cargo-ziggy/triage.rs @@ -1,20 +1,20 @@ -use crate::*; -use anyhow::bail; -use std::process; +use crate::{Common, Triage, util::Context}; +use anyhow::{Context as _, bail}; +use std::{fs, process}; impl Triage { - pub fn triage(&self) -> Result<(), anyhow::Error> { + pub fn triage(&self, common: &Common) -> Result<(), anyhow::Error> { eprintln!("Running CASR triage on crashes"); - let target = find_target(&self.target)?; - let input_dir = format!("{}/{target}/afl", self.ziggy_output.display()); + let cx = Context::new(common, self.target.clone())?; + let input_dir = cx.target_dir.join("afl"); let triage_dir = self .output .display() .to_string() .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) - .replace("{target_name}", &target); + .replace("{target_name}", &cx.bin_target); fs::remove_dir_all(&triage_dir).ok(); if !fs::metadata(&input_dir) @@ -34,7 +34,7 @@ impl Triage { process::Command::new("casr-afl") .args([ "-i", - &input_dir, + input_dir.as_ref(), "-o", &triage_dir, &format!("-j{}", self.jobs), diff --git a/src/bin/cargo-ziggy/util.rs b/src/bin/cargo-ziggy/util.rs index 618001d..2adcaae 100644 --- a/src/bin/cargo-ziggy/util.rs +++ b/src/bin/cargo-ziggy/util.rs @@ -1,6 +1,10 @@ +use crate::Common; use anyhow::Result; use std::{fs::File, hash::Hasher, io, io::Read, path::Path}; +pub use cargo_metadata::camino::Utf8PathBuf; + +#[inline] pub fn hash_file(path: &Path) -> Result { let mut hasher = twox_hash::XxHash64::with_seed(0); let mut file = File::open(path)?; @@ -15,3 +19,52 @@ pub fn hash_file(path: &Path) -> Result { } } } + +#[derive(Debug, Clone)] +pub struct Context { + pub target_dir: Utf8PathBuf, + pub bin_target: String, +} + +impl Context { + pub fn new(common: &Common, target: Option) -> Result { + Ok(Self { + target_dir: common.target_dir().cloned()?, + bin_target: common.resolve_bin(target)?, + }) + } + + pub fn view<'a>(&'a self, common: &'a Common) -> ContextView<'a> { + ContextView { common, cx: self } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ContextView<'a> { + common: &'a Common, + cx: &'a Context, +} + +impl<'a> ContextView<'a> { + #[inline] + pub fn common(&self) -> &'a Common { + self.common + } + + #[inline] + pub fn target_dir(&self) -> &'a Utf8PathBuf { + &self.cx.target_dir + } + + #[inline] + pub fn bin_target(&self) -> &'a str { + &self.cx.bin_target + } +} + +impl AsRef for ContextView<'_> { + #[inline] + fn as_ref(&self) -> &Context { + self.cx + } +} diff --git a/tests/url_fuzz.rs b/tests/url_fuzz.rs index 13defcf..db3832b 100644 --- a/tests/url_fuzz.rs +++ b/tests/url_fuzz.rs @@ -271,6 +271,7 @@ fn fuzz_binary() { let cargo_ziggy = target_directory.join("debug/cargo-ziggy"); let fuzzer_directory = workspace_root.join("examples/url"); let binary_path = temp_dir_path.join("binary"); + let output_path = temp_dir_path.join("output"); // cargo ziggy build let build_status = process::Command::new(&cargo_ziggy) @@ -297,7 +298,7 @@ fn fuzz_binary() { .arg("-j2") .arg("-t5") .arg("-G100") - .env("ZIGGY_OUTPUT", temp_dir_path) + .env("ZIGGY_OUTPUT", &output_path) .env("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES", "1") .env("AFL_SKIP_CPUFREQ", "1") .current_dir(temp_dir_path) @@ -307,8 +308,8 @@ fn fuzz_binary() { kill_subprocesses_recursively(&format!("{}", fuzzer.id())); assert!( - temp_dir_path - .join("afl/mainaflfuzzer/fuzzer_stats") + output_path + .join("binary/afl/mainaflfuzzer/fuzzer_stats") .is_file() ); @@ -323,7 +324,7 @@ fn fuzz_binary() { .arg("-j2") .arg("-t5") .arg("-G100") - .env("ZIGGY_OUTPUT", temp_dir_path) + .env("ZIGGY_OUTPUT", &output_path) .env("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES", "1") .env("AFL_SKIP_CPUFREQ", "1") .current_dir(temp_dir_path)