Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

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(&not_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");
}