diff --git a/Cargo.lock b/Cargo.lock index da59462..4e03bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1145,6 +1145,8 @@ dependencies = [ "libc", "rayon", "semver", + "serde", + "serde_json", "signal-hook", "strip-ansi-escapes", "target-triple", diff --git a/Cargo.toml b/Cargo.toml index ce59d1c..83abd14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ cli = [ "cargo_metadata", "clap", "console", + "dep:serde", + "dep:serde_json", "glob", "indicatif", "libc", @@ -60,6 +62,8 @@ indicatif = { version = "0.18.4", optional = true } libc = { version = "0.2.183", optional = true } rayon = { version = "1.11.0", optional = true } semver = { version = "1.0.27", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } signal-hook = { version = "0.4.3", optional = true } strip-ansi-escapes = { version = "0.2.1", optional = true } target-triple = { version = "1.0.0", optional = true } diff --git a/examples/asan/src/main.rs b/examples/asan/src/main.rs index 301f4aa..5dcf982 100644 --- a/examples/asan/src/main.rs +++ b/examples/asan/src/main.rs @@ -4,7 +4,7 @@ fn main() { return; } if data[0] == b'f' && data[1] == b'u' && data[2] == b'z' && data[3] == b'z' { - let xs = [0, 1, 2, 3]; + let xs = std::hint::black_box([0, 1, 2, 3]); let _y = std::hint::black_box(unsafe { *xs.as_ptr().add(4) }); } }); diff --git a/src/bin/cargo-ziggy/build.rs b/src/bin/cargo-ziggy/build.rs index 288121f..b5eacca 100644 --- a/src/bin/cargo-ziggy/build.rs +++ b/src/bin/cargo-ziggy/build.rs @@ -111,7 +111,14 @@ impl Build { let run = common .cargo() .args(["hfuzz", "build", "--bin", &cx.bin_target]) - .env("CARGO_TARGET_DIR", cx.target_dir.join("honggfuzz")) + .env( + "CARGO_TARGET_DIR", + cx.target_dir.join(if self.asan { + "honggfuzz-asan" + } else { + "honggfuzz" + }), + ) .envs(honggfuzz_envs(self.asan)) .stdout(process::Stdio::piped()) .spawn()? diff --git a/src/bin/cargo-ziggy/clean.rs b/src/bin/cargo-ziggy/clean.rs index dba3967..8d2c01f 100644 --- a/src/bin/cargo-ziggy/clean.rs +++ b/src/bin/cargo-ziggy/clean.rs @@ -35,6 +35,7 @@ impl Clean { clean("afl", Some(target_triple::TARGET), false)?; // honggfuzz uses --target=host clean("honggfuzz", Some(target_triple::TARGET), true)?; + clean("honggfuzz-asan", Some(target_triple::TARGET), true)?; // coverage (from ziggy cover) clean("coverage", None, false)?; // runner (from ziggy run) diff --git a/src/bin/cargo-ziggy/fuzz.rs b/src/bin/cargo-ziggy/fuzz.rs index 459cba7..2a7704a 100644 --- a/src/bin/cargo-ziggy/fuzz.rs +++ b/src/bin/cargo-ziggy/fuzz.rs @@ -1,6 +1,6 @@ use crate::{ Build, Common, Cover, Fuzz, FuzzingEngines, Minimize, - util::{Context, ContextView}, + util::{Context, ContextView, TimeoutArg}, }; use anyhow::{Context as _, Error, Result, anyhow, bail}; use console::{Term, style}; @@ -100,6 +100,12 @@ impl Fuzz { }; let cx_view = cx.view(common); + if self.asan && self.memory_limit.is_some() { + bail!( + "--memory-limit and --asan are incompatible, because ASan reserves a large shadow mapping" + ); + } + if self.binary.is_none() { let build = Build { no_afl: !self.afl(), @@ -518,11 +524,6 @@ impl Fuzz { 22 => "-l3at", _ => "-c-", // disable Cmplog, needs AFL++ 4.08a }; - // AFL timeout is in ms so we convert the value - let timeout_option_afl = match self.timeout { - Some(t) => format!("-t{}", t * 1000), - None => String::new(), - }; let memory_option_afl = match &self.memory_limit { Some(m) => format!("-m{m}"), None => String::new(), @@ -589,6 +590,7 @@ impl Fuzz { &format!("-o{}/afl", paths.output_target), &format!("-g{}", self.min_length), &format!("-G{}", self.max_length), + &TimeoutArg::from(self.timeout).afl_arg(), &use_shared_corpus, &use_initial_corpus_dir, old_queue_cycling, @@ -596,7 +598,6 @@ impl Fuzz { mopt_mutator, mutation_option, input_format_option, - &timeout_option_afl, &memory_option_afl, &dictionary_option, ] @@ -641,8 +642,9 @@ impl Fuzz { run_args.push_str(&format!(" -F{}", self.max_length)); 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}")); + { + run_args.push(' '); + run_args.push_str(&TimeoutArg::from(self.timeout).honggfuzz_arg()); } if let Some(d) = &self.dictionary { run_args.push_str(&format!(" -w{}", d.display())); @@ -653,6 +655,11 @@ impl Fuzz { run_args }; + let hfuzz_target = if self.asan { + "honggfuzz-asan" + } else { + "honggfuzz" + }; let log = File::create(format!("{}/logs/honggfuzz.log", paths.output_target))?; @@ -670,7 +677,7 @@ impl Fuzz { ), "/dev/null", ]) - .env("CARGO_TARGET_DIR", cx.target_dir().join("honggfuzz")) + .env("CARGO_TARGET_DIR", cx.target_dir().join(hfuzz_target)) .envs(crate::build::honggfuzz_envs(self.asan)) .env( "HFUZZ_WORKSPACE", @@ -745,8 +752,9 @@ impl Fuzz { output_corpus: PathBuf::from(minimized_corpus), ziggy_output: self.ziggy_output.clone(), jobs: self.jobs, - timeout: self.timeout.unwrap_or(5000), + timeout: self.timeout, engine, + release: self.release, }; match minimization_args.minimize(common) { Ok(()) => { @@ -769,8 +777,8 @@ impl Fuzz { new_corpus_size ))?; } - Err(_) => { - bail!("Please check the logs, this might be an oom error"); + Err(e) => { + return Err(e).context("Please check the logs, this might be an oom error"); } } Ok(()) diff --git a/src/bin/cargo-ziggy/main.rs b/src/bin/cargo-ziggy/main.rs index 9bb3064..4c63ef9 100644 --- a/src/bin/cargo-ziggy/main.rs +++ b/src/bin/cargo-ziggy/main.rs @@ -12,7 +12,9 @@ mod util; use crate::fuzz::FuzzingConfig; use anyhow::{Context, Result, anyhow, bail}; -use clap::{Args, Parser, Subcommand, ValueEnum}; +use clap::{ + Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum, parser::ValueSource, +}; use std::{ path::PathBuf, sync::OnceLock, @@ -270,11 +272,15 @@ pub struct Minimize { jobs: u32, /// Timeout for a single run - #[clap(short, long, value_name = "MILLI_SECS", default_value_t = 5000)] - timeout: u32, + #[clap(short, long, value_name = "SECS")] + timeout: Option, #[clap(short, long, value_enum, default_value_t = FuzzingEngines::All)] engine: FuzzingEngines, + + /// Compile in release mode (--release) + #[clap(long = "release", action)] + release: bool, } #[derive(Args)] @@ -352,8 +358,8 @@ pub struct Triage { ziggy_output: PathBuf, /// Terminate runner after x seconds - #[clap(short, long, value_name = "SECS")] - timeout: Option, + #[clap(short, long, value_name = "SECS", default_value_t = 0)] + timeout: u32, /* future feature, wait for casr /// Crash directory to be sourced from #[clap(short, long, value_parser, value_name = "DIR", default_value = DEFAULT_CRASHES_DIR)] @@ -490,7 +496,7 @@ impl Common { .ok_or_else(|| anyhow!("not in a Cargo workspace")) } - fn guess_bin(&self) -> Result { + fn guess_bin(&self) -> Result<(String, &cargo_metadata::Package)> { let meta = self .metadata() .ok_or_else(|| anyhow!("failed running cargo metadata"))?; @@ -498,27 +504,33 @@ impl Common { if meta.workspace_default_members.is_missing() { bail!("please specify a target") } - let bins: Vec<(&str, &str)> = meta + let bins: Vec<_> = meta .workspace_default_packages() .into_iter() - .flat_map(|p| p.targets.iter().filter(|t| t.is_bin())) - .map(|t| (t.name.as_str(), t.src_path.as_str())) + .flat_map(|p| { + p.targets + .iter() + .filter_map(move |t| t.is_bin().then_some((t, p))) + }) + .map(|(t, p)| (t.name.as_str(), t.src_path.as_str(), p)) .collect(); // if there is only one bin, we use it - if let [(name, _)] = bins.as_slice() { - return Ok((*name).to_owned()); + if let [(name, _, p)] = bins.as_slice() { + return Ok(((*name).to_owned(), p)); } // otherwise fallback to `main.rs` - let main_bins: Vec<&str> = bins + let main_bins: Vec<_> = bins .iter() - .filter_map(|(name, path)| path.ends_with("main.rs").then_some(*name)) + .filter_map(|(name, path, package)| { + path.ends_with("main.rs").then_some((*name, package)) + }) .collect(); - if let [name] = main_bins.as_slice() { - return Ok((*name).to_owned()); + if let [(name, package)] = main_bins.as_slice() { + return Ok(((*name).to_owned(), package)); } // otherwise we ask the user to choose let mut targets = String::new(); - for (name, _) in bins { + for (name, _, _) in bins { targets.push_str("\n\t"); targets.push_str(name); } @@ -526,94 +538,173 @@ impl Common { } fn resolve_bin(&self, target: Option) -> Result { - target.ok_or(()).or_else(|()| self.guess_bin()) + target + .ok_or(()) + .or_else(|()| self.guess_bin().map(|(bin_name, _)| bin_name)) } - fn compatible_fuzzers(&self, bin_name: &str) -> Option> { - let meta = self.metadata()?; - if meta.workspace_default_members.is_missing() { - return None; + fn compatible_fuzzers(&self, target: Option) -> Result>> { + let package = if let Some(bin_name) = target { + // resolve in workspace + let Some(meta) = self.metadata() else { + return Ok(None); + }; + if meta.workspace_default_members.is_missing() { + return Ok(None); + } + let candidates: Vec<_> = meta + .workspace_default_packages() + .into_iter() + .filter(|p| p.targets.iter().any(|t| t.is_bin() && t.name == bin_name)) + .collect(); + match candidates.len() { + 0 => return Ok(None), + 1 => candidates[0], + _ => bail!("{bin_name} is included in multiple workspace default packages"), + } + } else { + self.guess_bin()?.1 + }; + + let meta: Option = { + let meta = serde_json::from_value(package.metadata.clone())?; + #[cfg(debug_assertions)] + match std::env::var("ZIGGY_TEST_METADATA_OVERRIDE") { + Ok(ref s) => serde_json::from_str::>(s)?, + Err(std::env::VarError::NotPresent) => meta, + Err(e) => return Err(e.into()), + } + #[cfg(not(debug_assertions))] + meta + }; + + return Ok(meta.and_then(|m| m.ziggy.map(|z| z.compat))); + + #[derive(Debug, serde::Deserialize)] + struct Metadata { + ziggy: Option, } - let compat = meta - .workspace_default_packages() - .into_iter() - .find(|p| p.targets.iter().any(|t| t.is_bin() && t.name == bin_name))? - .metadata - .get("ziggy")? - .get("compat")? - .as_array()? - .iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect(); - Some(compat) + #[derive(Debug, serde::Deserialize)] + struct InnerMeta { + compat: Vec, + } } } -fn main() -> Result<(), anyhow::Error> { - let mut common = Common::new(); - common.shutdown_immediate(); - common.setup_signal_handling()?; +fn parse_args(common: &Common) -> Result { + let matches = Cargo::command().get_matches(); + let mut default_engine = false; + if let Some(("ziggy", ziggy)) = matches.subcommand() + && let Some(subcmd) = ziggy.subcommand() + { + #[expect(clippy::single_match)] + match subcmd { + ("minimize", fuzz) => { + default_engine = + matches!(fuzz.value_source("engine"), Some(ValueSource::DefaultValue)); + } + _ => {} + } + } + + let Cargo::Ziggy(mut command) = match Cargo::from_arg_matches(&matches) { + Ok(cmd) => cmd, + Err(e) => e.exit(), + }; + + apply_restrictions(&mut command, common, default_engine) + .context("reading [package.metadata.ziggy] in Cargo.toml")?; + Ok(command) +} - let Cargo::Ziggy(mut command) = Cargo::parse(); +/// apply package metadata for ziggy +fn apply_restrictions( + command: &mut Ziggy, + common: &Common, + default_engine: bool, +) -> Result<(), anyhow::Error> { + // skip for binary target fuzzing + if matches!( + command, + Ziggy::Fuzz(Fuzz { + binary: Some(_), + .. + }) + ) { + return Ok(()); + } - let bin_target = match &command { - Ziggy::Build(build) => Some(build.target.clone()), - Ziggy::Fuzz(fuzz) => Some(fuzz.target.clone()), - Ziggy::Minimize(minimize) => Some(minimize.target.clone()), - _ => None, + let bin_target = match command { + Ziggy::Build(Build { target, .. }) + | Ziggy::Fuzz(Fuzz { target, .. }) + | Ziggy::Minimize(Minimize { target, .. }) => target.clone(), + _ => { + // no need to restrict anything + return Ok(()); + } }; - if let Some(bin_target) = bin_target - && let Ok(harness) = common.resolve_bin(bin_target) - && let Some(compat) = common.compatible_fuzzers(&harness) - { - let mut with_afl = false; - let mut with_honggfuzz = false; - for fuzzer in compat { - match fuzzer.to_lowercase().as_str() { - "afl" => { - with_afl = true; - } - "honggfuzz" => { - with_honggfuzz = true; - } - other => bail!("unknown fuzzer {other} in [package.metadata.ziggy]"), + let Some(compatible_fuzzers) = common.compatible_fuzzers(bin_target)? else { + return Ok(()); + }; + + let mut afl_compatible = false; + let mut honggfuzz_compatible = false; + for fuzzer in compatible_fuzzers { + match fuzzer.to_lowercase().as_str() { + "afl" => { + afl_compatible = true; + } + "honggfuzz" => { + honggfuzz_compatible = true; } + other => bail!("unknown fuzzer {other}"), } + } - match &mut command { - Ziggy::Build(build) => { - build.no_afl |= !with_afl; - build.no_honggfuzz |= !with_honggfuzz; - } - Ziggy::Fuzz(fuzz) => { - fuzz.no_afl |= !with_afl; - fuzz.no_honggfuzz |= !with_honggfuzz; + match command { + Ziggy::Build(build) => { + build.no_afl |= !afl_compatible; + build.no_honggfuzz |= !honggfuzz_compatible; + } + Ziggy::Fuzz(fuzz) => { + fuzz.no_afl |= !afl_compatible; + fuzz.no_honggfuzz |= !honggfuzz_compatible; + } + Ziggy::Minimize(minimize) => { + let cli_afl = matches!( + minimize.engine, + FuzzingEngines::AFLPlusPlus | FuzzingEngines::All + ); + let cli_honggfuzz = matches!( + minimize.engine, + FuzzingEngines::Honggfuzz | FuzzingEngines::All + ); + let res_afl = cli_afl && afl_compatible; + let res_honggfuzz = cli_honggfuzz && honggfuzz_compatible; + if !default_engine && (cli_afl, cli_honggfuzz) != (res_afl, res_honggfuzz) { + bail!("requested fuzzer is incompatible with harness"); } - Ziggy::Minimize(minimize) => { - let use_afl = matches!( - minimize.engine, - FuzzingEngines::AFLPlusPlus | FuzzingEngines::All - ) && with_afl; - let use_honggfuzz = matches!( - minimize.engine, - FuzzingEngines::Honggfuzz | FuzzingEngines::All - ) && with_honggfuzz; - - minimize.engine = match (use_afl, use_honggfuzz) { - (true, true) => FuzzingEngines::All, - (true, false) => FuzzingEngines::AFLPlusPlus, - (false, true) => FuzzingEngines::Honggfuzz, - (false, false) => bail!("harness is not compatible with any requested fuzzer"), - } + minimize.engine = match (res_afl, res_honggfuzz) { + (true, true) => FuzzingEngines::All, + (true, false) => FuzzingEngines::AFLPlusPlus, + (false, true) => FuzzingEngines::Honggfuzz, + (false, false) => bail!("harness is not compatible with any requested fuzzer"), } - _ => {} } + _ => {} } + Ok(()) +} - match command { - Ziggy::Build(args) => args.build(&common).context("Failed to build the fuzzers"), +fn main() -> Result<(), anyhow::Error> { + let mut common = Common::new(); + common.shutdown_immediate(); + common.setup_signal_handling()?; + + match parse_args(&common)? { + Ziggy::Build(args) => args.build(&common).context("Failure building 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(args) => args diff --git a/src/bin/cargo-ziggy/minimize.rs b/src/bin/cargo-ziggy/minimize.rs index 4792086..dd3f6c7 100644 --- a/src/bin/cargo-ziggy/minimize.rs +++ b/src/bin/cargo-ziggy/minimize.rs @@ -1,6 +1,6 @@ use crate::{ Build, Common, FuzzingEngines, Minimize, - util::{Context, ContextView, hash_file}, + util::{Context, ContextView, TimeoutArg, hash_file}, }; use anyhow::{Context as _, Result, bail}; use std::{ @@ -17,7 +17,8 @@ impl Minimize { let build = Build { no_afl: self.engine == FuzzingEngines::Honggfuzz, no_honggfuzz: self.engine == FuzzingEngines::AFLPlusPlus, - release: false, + release: self.release, + // deliberately do not use ASan build for minimization asan: false, target: Some(cx.bin_target.clone()), }; @@ -46,7 +47,7 @@ impl Minimize { FuzzingEngines::All => { std::thread::scope(|s| -> Result<()> { let handle_afl = { s.spawn(move || self.minimize_afl(cx_view)) }; - thread::sleep(Duration::from_millis(1000)); + thread::sleep(Duration::from_millis(2000)); let handle_honggfuzz = { s.spawn(move || self.minimize_honggfuzz(cx_view)) }; handle_afl @@ -107,11 +108,16 @@ impl Minimize { 0 | 1 => String::from("all"), t => format!("{t}"), }; - let target_dir = cx.target_dir().join("afl/debug").join(cx.bin_target()); + let target_bin = cx + .target_dir() + .join("afl") + .join(if self.release { "release" } else { "debug" }) + .join(cx.bin_target()); // AFL++ minimization let log_file = File::create(self.log_dir(cx.as_ref()).join("minimization_afl.log"))?; - cx.common() + let status = cx + .common() .cargo() .args([ "afl", @@ -122,16 +128,19 @@ impl Minimize { &self.output_corpus(cx.as_ref()), "-T", &jobs_option, - "-t", - &format!("{}", self.timeout), + &TimeoutArg::from(self.timeout).afl_arg(), "--", - target_dir.as_str(), + target_bin.as_str(), ]) .stderr(log_file.try_clone()?) .stdout(log_file) .spawn()? .wait()?; - Ok(()) + if status.success() { + Ok(()) + } else { + bail!("afl cmin with {status}") + } } // HONGGFUZZ minimization @@ -139,11 +148,12 @@ impl Minimize { println!("Minimizing with honggfuzz"); let log_file = File::create(self.log_dir(cx.as_ref()).join("minimization_honggfuzz.log"))?; - cx.common() + let status = 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") + .envs(crate::build::honggfuzz_envs(false)) .env( "HFUZZ_WORKSPACE", format!( @@ -155,16 +165,20 @@ impl Minimize { .env( "HFUZZ_RUN_ARGS", format!( - "-i{} -M -o{} -t{}", + "-i{} -M -o{} {}", self.input_corpus(cx.as_ref()), self.output_corpus(cx.as_ref()), - self.timeout + TimeoutArg::from(self.timeout).honggfuzz_arg(), ), ) .stderr(log_file.try_clone()?) .stdout(log_file) .spawn()? .wait()?; - Ok(()) + if status.success() { + Ok(()) + } else { + bail!("honggfuzz minimization with {status}") + } } } diff --git a/src/bin/cargo-ziggy/triage.rs b/src/bin/cargo-ziggy/triage.rs index 2e0e23f..44814ed 100644 --- a/src/bin/cargo-ziggy/triage.rs +++ b/src/bin/cargo-ziggy/triage.rs @@ -37,7 +37,7 @@ impl Triage { "-o", &triage_dir, &format!("-j{}", self.jobs), - &format!("-t{}", self.timeout.unwrap_or(0)), // future: add option for crashes directory and use runner + &format!("-t{}", self.timeout), // future: add option for crashes directory and use runner ]) .spawn() .context("Running casr failed, try `cargo install casr`")? diff --git a/src/bin/cargo-ziggy/util.rs b/src/bin/cargo-ziggy/util.rs index 27321a9..7aab71e 100644 --- a/src/bin/cargo-ziggy/util.rs +++ b/src/bin/cargo-ziggy/util.rs @@ -84,3 +84,34 @@ impl AsRef for ContextView<'_> { self.cx } } + +/// Flag handling for per seed timeout (in secs) +pub struct TimeoutArg(Option); + +impl From> for TimeoutArg { + fn from(value: Option) -> Self { + Self(value) + } +} + +impl TimeoutArg { + /// Complete afl flag with default 5s + pub fn afl_arg(&self) -> String { + if let Some(s) = self.0 { + // uses milli-secs + format!("-t{}", s.saturating_mul(1000)) + } else { + "-t5000".to_owned() + } + } + + /// Complete honggfuzz flag with default 5s + pub fn honggfuzz_arg(&self) -> String { + if let Some(s) = self.0 { + // uses secs + format!("-t{s}") + } else { + "-t5".to_owned() + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a28207f..4959778 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,8 +82,7 @@ pub use afl::fuzz as afl_fuzz; #[cfg(feature = "afl")] macro_rules! fuzz { ( $($x:tt)* ) => { - static USE_ARGS: std::sync::LazyLock = std::sync::LazyLock::new(|| std::env::args().len() > 1); - if *USE_ARGS { + if ::std::env::args().len() > 1 { $crate::inner_fuzz!($($x)*); } else { $crate::afl_fuzz!($($x)*); @@ -99,8 +98,12 @@ pub use honggfuzz::fuzz as honggfuzz_fuzz; #[cfg(all(feature = "honggfuzz", not(feature = "afl")))] macro_rules! fuzz { ( $($x:tt)* ) => { - loop { - $crate::honggfuzz_fuzz!($($x)*); + if ::std::env::args().len() > 1 { + $crate::inner_fuzz!($($x)*); + } else { + loop { + $crate::honggfuzz_fuzz!($($x)*); + } } }; } diff --git a/tests/asan_fuzz.rs b/tests/asan_fuzz.rs index f485928..c17783f 100644 --- a/tests/asan_fuzz.rs +++ b/tests/asan_fuzz.rs @@ -307,3 +307,98 @@ fn honggfuzz_builds_once() { kill_subprocesses_recursively(&format!("{}", fuzzer.id())); assert!(hfuzz_dir.is_dir()); } + +#[allow(clippy::zombie_processes)] +#[test] +fn compat_metadata() { + let _guard = exclusive_guard(); + let temp_dir = tempfile::tempdir().unwrap(); + let output_dir = temp_dir.path().join("output"); + let target_dir = temp_dir.path().join("target"); + let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap(); + let workspace_root: PathBuf = metadata.workspace_root.into(); + let cargo_ziggy = metadata.target_directory.join("debug/cargo-ziggy"); + let fuzzer_directory = workspace_root.join("examples/asan"); + let hfuzz_dir = output_dir.join("asan-fuzz/honggfuzz"); + + let restrict = |fuzzers| r#"{"ziggy":{"compat":[PATTERN]}}"#.replace("PATTERN", fuzzers); + + // cargo ziggy build (restrict to afl) + let build_status = process::Command::new(&cargo_ziggy) + .arg("ziggy") + .arg("build") + .env("ZIGGY_TEST_METADATA_OVERRIDE", restrict("\"afl\"")) + .env("CARGO_TARGET_DIR", &target_dir) + .current_dir(&fuzzer_directory) + .status() + .expect("failed to run `cargo ziggy build`"); + assert!(build_status.success(), "`cargo ziggy build` failed"); + assert!(target_dir.join("afl/debug/asan-fuzz").is_file()); + assert!(!target_dir.join("honggfuzz").is_dir()); + + let fuzzer = process::Command::new(&cargo_ziggy) + .arg("ziggy") + .arg("fuzz") + .arg("-j2") + .env("ZIGGY_TEST_METADATA_OVERRIDE", restrict("\"afl\", \"afl\"")) + .env("CARGO_TARGET_DIR", &target_dir) + .env("ZIGGY_OUTPUT", &output_dir) + .env("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES", "1") + .env("AFL_SKIP_CPUFREQ", "1") + .current_dir(&fuzzer_directory) + .spawn() + .expect("failed to run `cargo ziggy fuzz`"); + thread::sleep(Duration::from_secs(10)); + kill_subprocesses_recursively(&format!("{}", fuzzer.id())); + thread::sleep(Duration::from_secs(2)); + assert!(!hfuzz_dir.is_dir()); + + // restricted minimization + let minimize = process::Command::new(&cargo_ziggy) + .arg("ziggy") + .arg("minimize") + .arg("-ehonggfuzz") + .env("ZIGGY_TEST_METADATA_OVERRIDE", restrict("\"afl\"")) + .env("CARGO_TARGET_DIR", &target_dir) + .env("ZIGGY_OUTPUT", &output_dir) + .current_dir(&fuzzer_directory) + .output() + .expect("failed to run `cargo ziggy minimize`"); + assert!(!minimize.status.success()); + assert!( + std::str::from_utf8(&minimize.stderr) + .unwrap() + .contains(" incompatible ") + ); + + let minimize = process::Command::new(&cargo_ziggy) + .arg("ziggy") + .arg("minimize") + .arg("-eall") + .env("ZIGGY_TEST_METADATA_OVERRIDE", restrict("\"afl\"")) + .env("CARGO_TARGET_DIR", &target_dir) + .env("ZIGGY_OUTPUT", &output_dir) + .current_dir(&fuzzer_directory) + .output() + .expect("failed to run `cargo ziggy minimize`"); + assert!(!minimize.status.success()); + assert!( + std::str::from_utf8(&minimize.stderr) + .unwrap() + .contains(" incompatible ") + ); + + let minimize = process::Command::new(&cargo_ziggy) + .arg("ziggy") + .arg("minimize") + .arg("-eafl-plus-plus") + .env("ZIGGY_TEST_METADATA_OVERRIDE", restrict("\"afl\"")) + .env("CARGO_TARGET_DIR", &target_dir) + .env("ZIGGY_OUTPUT", &output_dir) + .env("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES", "1") + .env("AFL_SKIP_CPUFREQ", "1") + .current_dir(&fuzzer_directory) + .status() + .expect("failed to run `cargo ziggy minimize`"); + assert!(minimize.success()); +}