ci/verbs/tests/fuzz_verb.rs
//! The nightly fuzz step (OSSF-NEXT): `ci-verbs fuzz` builds a checkout's cargo-fuzz
//! crate (`fuzz/`, the layout OpenSSF Scorecard's Fuzzing check finds) with plain STABLE
//! cargo, runs every target for a bounded time over a corpus kept between nights, and
//! goes red by name when a target crashes, keeping the input that crashed it.
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use ci_verbs::fuzz;
use serde_json::json;
fn tmp(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("ci-verbs-fuzz-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
/// A stand-in for a libFuzzer binary: a shell script that answers the way libFuzzer does.
fn fake(dir: &Path, name: &str, body: &str) -> PathBuf {
let p = dir.join(name);
std::fs::write(&p, format!("#!/bin/sh\n{body}\n")).unwrap();
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
p
}
#[test]
fn the_targets_are_the_bins_of_a_cargo_fuzz_crate_and_nothing_else() {
let meta = json!({
"packages": [{
"name": "gunnar-fuzz",
"manifest_path": "/src/gunnar/fuzz/Cargo.toml",
"metadata": { "cargo-fuzz": true },
"targets": [
{ "name": "pack_index", "kind": ["bin"], "src_path": "/src/gunnar/fuzz/fuzz_targets/pack_index.rs" },
{ "name": "crypt_open", "kind": ["bin"], "src_path": "/src/gunnar/fuzz/fuzz_targets/crypt_open.rs" },
{ "name": "gunnar_fuzz", "kind": ["lib"], "src_path": "/src/gunnar/fuzz/src/lib.rs" }
]
}]
});
let t = fuzz::targets_from_metadata(&meta).unwrap();
assert_eq!(t, ["crypt_open", "pack_index"], "the bins, sorted; the lib is not a target");
let not_fuzz = json!({ "packages": [{ "name": "x", "metadata": null, "targets": [{ "name": "x", "kind": ["bin"] }] }] });
let e = fuzz::targets_from_metadata(¬_fuzz).unwrap_err();
assert!(e.contains("cargo-fuzz"), "a crate without `[package.metadata] cargo-fuzz = true` is refused by name: {e}");
let none = json!({ "packages": [{ "name": "f", "metadata": { "cargo-fuzz": true }, "targets": [] }] });
assert!(fuzz::targets_from_metadata(&none).unwrap_err().contains("no fuzz target"));
}
#[test]
fn the_build_is_stable_cargo_with_sancov_for_the_host_triple_only() {
// the flags cargo-fuzz passes with `--sanitizer none`: no -Z, so ci4's stable 1.97 builds it
for f in [
"--cfg fuzzing",
"-Cpasses=sancov-module",
"-Cllvm-args=-sanitizer-coverage-level=4",
"-Cllvm-args=-sanitizer-coverage-inline-8bit-counters",
"-Cllvm-args=-sanitizer-coverage-pc-table",
"-Cllvm-args=-sanitizer-coverage-trace-compares",
"-Cdebug-assertions",
"-Coverflow-checks",
] {
assert!(fuzz::RUSTFLAGS.contains(f), "{f} missing from {}", fuzz::RUSTFLAGS);
}
assert!(!fuzz::RUSTFLAGS.contains("-Z"), "stable only: {}", fuzz::RUSTFLAGS);
let args = fuzz::build_args(Path::new("/src/gunnar/fuzz/Cargo.toml"), "x86_64-unknown-linux-gnu");
let line = args.join(" ");
// `--target` keeps RUSTFLAGS off build scripts and proc-macros (they would not link)
assert!(line.contains("--target x86_64-unknown-linux-gnu"), "{line}");
assert!(line.contains("--release") && line.contains("--bins") && line.contains("--locked"), "{line}");
assert!(line.contains("--manifest-path /src/gunnar/fuzz/Cargo.toml"), "{line}");
assert_eq!(fuzz::bin_path(Path::new("/t"), "x86_64-unknown-linux-gnu", "pack_index"), Path::new("/t/x86_64-unknown-linux-gnu/release/pack_index"));
}
#[test]
fn libfuzzer_stats_are_read_from_its_last_lines() {
let err = "INFO: Seed: 1\n#2\tINITED cov: 10 ft: 11 corp: 1/1b exec/s: 0 rss: 30Mb\n#4096\tpulse cov: 120 ft: 300 corp: 20/1Kb\n#81234\tDONE cov: 187 ft: 512 corp: 41/3210b lim: 4096 exec/s: 1353 rss: 61Mb\nDone 81234 runs in 61 second(s)\n";
let s = fuzz::parse_stats(err);
assert_eq!(s.runs, Some(81234));
assert_eq!(s.cov, Some(187));
assert_eq!(s.corpus, Some(41));
let none = fuzz::parse_stats("nothing useful");
assert_eq!((none.runs, none.cov, none.corpus), (None, None, None));
}
#[test]
fn a_clean_target_is_green_and_its_corpus_grows_in_the_kept_directory() {
let d = tmp("clean");
let seeds = d.join("seeds");
std::fs::create_dir_all(&seeds).unwrap();
std::fs::write(seeds.join("one"), b"x").unwrap();
// libFuzzer writes new inputs into the FIRST corpus directory: the kept one
let bin = fake(
&d,
"clean",
r##"for a in "$@"; do case "$a" in -max_total_time=*) T=$a;; -artifact_prefix=*) ;; -*) ;; *) [ -z "$KEEP" ] && KEEP=$a;; esac; done
echo "seen $T" > "$KEEP/new-input"
echo "#500 DONE cov: 42 ft: 50 corp: 3/10b exec/s: 500 rss: 30Mb" >&2
echo "Done 500 runs in 1 second(s)" >&2
exit 0"##,
);
let o = fuzz::run_target("clean", &bin, 7, &d.join("corpus"), &seeds, &d.join("crashes"));
assert!(o.is_clean(), "{o:?}");
assert_eq!(o.stats.runs, Some(500));
let kept = std::fs::read_to_string(d.join("corpus/clean/new-input")).unwrap();
assert_eq!(kept.trim(), "seen -max_total_time=7", "the bound reaches libFuzzer");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn a_crash_is_red_by_name_and_keeps_the_input_that_crashed() {
let d = tmp("crash");
let seeds = d.join("seeds");
std::fs::create_dir_all(&seeds).unwrap();
let bin = fake(
&d,
"boom",
r##"for a in "$@"; do case "$a" in -artifact_prefix=*) P=${a#-artifact_prefix=};; esac; done
printf 'boom' > "${P}crash-0123abcd"
echo "==1== ERROR: libFuzzer: deadly signal" >&2
echo "artifact_prefix='$P'; Test unit written to ${P}crash-0123abcd" >&2
exit 77"##,
);
let o = fuzz::run_target("boom", &bin, 5, &d.join("corpus"), &seeds, &d.join("crashes"));
assert!(!o.is_clean());
let a = o.artifacts.first().expect("the crashing input is kept");
assert_eq!(std::fs::read(a).unwrap(), b"boom");
assert!(a.starts_with(d.join("crashes/boom")), "{}", a.display());
let md = fuzz::summary("gunnar", "abcdef0123456789", std::slice::from_ref(&o));
assert!(md.contains("| boom | CRASH |"), "{md}");
assert!(md.contains("crash-0123abcd"), "the page names the input: {md}");
assert!(md.contains("626f6f6d"), "the page shows the input's first bytes in hex: {md}");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn a_binary_that_cannot_start_is_red_too() {
let d = tmp("nobin");
let o = fuzz::run_target("gone", &d.join("no-such-bin"), 5, &d.join("corpus"), &d.join("seeds"), &d.join("crashes"));
assert!(!o.is_clean());
assert!(o.error.as_deref().unwrap_or("").contains("no-such-bin"), "{o:?}");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn the_verb_says_usage_and_refuses_a_checkout_without_a_fuzz_crate() {
assert_eq!(fuzz::verb(&[]), 2, "usage without a checkout");
let d = tmp("nofuzz");
let args: Vec<String> = ["--checkout", d.to_str().unwrap(), "--out", d.join("out").to_str().unwrap()].iter().map(|s| s.to_string()).collect();
assert_eq!(fuzz::verb(&args), 1, "a checkout without fuzz/Cargo.toml is a red step, by name");
let md = std::fs::read_to_string(d.join("out/summary.md")).unwrap();
assert!(md.contains("fuzz/Cargo.toml"), "{md}");
let _ = std::fs::remove_dir_all(d);
}
/// MON-OSSF (2026-10-31): `--private <name>` fuzzes the private sibling the forge mounts
/// read-only beside the run checkout (`<checkout>/../<name>`, monetize-impl for monetize), so a
/// verdict-only pipeline fuzzes the private vendors without the private code leaving dwarf. The
/// name is one plain directory name: a path, `..` or an empty name is refused by name.
#[test]
fn a_private_sibling_is_fuzzed_beside_the_checkout_and_only_by_a_plain_name() {
let co = Some(PathBuf::from("/w/monetize"));
assert_eq!(fuzz::checkout_of(None, None, co.clone()).unwrap(), Path::new("/w/monetize"));
assert_eq!(fuzz::checkout_of(Some("/x/gunnar".into()), None, co.clone()).unwrap(), Path::new("/x/gunnar"), "--checkout wins over the env");
assert_eq!(fuzz::checkout_of(None, Some("monetize-impl".into()), co.clone()).unwrap(), Path::new("/w/monetize/../monetize-impl"));
for bad in ["", "..", "a/b", "../etc", ".hidden"] {
let e = fuzz::checkout_of(None, Some(bad.into()), co.clone()).unwrap_err();
assert!(e.contains("--private"), "{bad:?}: {e}");
}
assert!(fuzz::checkout_of(None, None, None).is_err(), "no checkout at all");
}