From 21a5665d21c50939d3d87f8649bbc1f059c29e67 Mon Sep 17 00:00:00 2001 From: 0xEthamin Date: Sun, 23 Aug 2026 13:56:53 +0200 Subject: [PATCH] ci: fuzz every fuzz crate The fuzz job hardcoded `cd crates/tropic01-driver` before `cargo fuzz list`, in ci.yml and mirrored in ci-local.sh, so 2 of 8 targets had never run in any pipeline: `verify_image`, the firmware-image signature verifier, and `drive_machine`, which drives the update state machine. scripts/fuzz-gate.sh is now the single gate body both callers invoke, per the asm-gate.sh precedent. It walks the tree for fuzz projects and pins the expected crate list, failing both when a crate is discovered but not pinned and when a pinned crate is not found. Adding a fuzz crate means editing that file, which is deliberate: the old shape failed open and in silence. Three defects found on the way, each proven by re-injection. `cargo test --workspace` selected no features, so six `_fuzz` seam guard tests had never run anywhere. They now run in Test and Coverage, which blocks a merge. The `drive_machine` seam armed `MockSeCounter::new(0)`. The secure element counts down, so that pinned the anti-rollback floor at `u32::MAX` and rejected every image regardless of its signature. The driver's `verify_cert_chain` target gates its whole body on a fixed P-521 anchor parsing. One wrong byte and it consumed fuzzer input while doing nothing, coverage flat and the corpus collapsed, and the run still reported success. A test now pins that the anchor is accepted. --- .github/workflows/ci.yml | 18 +- README.md | 2 +- .../fuzz/fuzz_targets/drive_machine.rs | 2 +- crates/fw-update/src/lib.rs | 28 +- crates/fw-update/src/tests.rs | 51 ++++ crates/tropic01-driver/fuzz/Cargo.lock | 2 +- crates/tropic01-driver/src/lib.rs | 59 ++-- scripts/ci-local.sh | 14 +- scripts/fuzz-gate.sh | 264 ++++++++++++++++++ 9 files changed, 382 insertions(+), 58 deletions(-) create mode 100755 scripts/fuzz-gate.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcfc8e0..4ae89b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -260,7 +260,7 @@ jobs: uses: taiki-e/install-action@cargo-llvm-cov - name: Run tests (host, hermetic, whole workspace) - run: cargo test --workspace --locked + run: cargo test --workspace --locked --features fw-update/_fuzz,image-verify/_fuzz,tropic01-driver/_fuzz - name: Set up Python uses: actions/setup-python@v5 @@ -392,7 +392,7 @@ jobs: continue-on-error: true run: cargo +stable outdated --workspace --root-deps-only - # Job 6: Fuzzing (libFuzzer + ASan on the driver's attacker-facing parsers) + # Job 6: Fuzzing (libFuzzer + ASan on every attacker-facing decoder in the tree) fuzz: name: Fuzz Parsers runs-on: ubuntu-latest @@ -409,33 +409,27 @@ jobs: with: tool: cargo-fuzz - # The corpus accumulates across runs: every discovered input makes - # the next run start deeper. - name: Restore fuzz corpus uses: actions/cache@v4 with: - path: crates/tropic01-driver/fuzz/corpus + path: crates/*/fuzz/corpus key: fuzz-corpus-${{ github.run_id }} restore-keys: | fuzz-corpus- # 60 seconds per target on PR/push, 15 minutes on the weekly run. - - name: Fuzz each target + - name: Fuzz every target of every fuzz crate run: | secs=60 if [ "${{ github.event_name }}" = "schedule" ]; then secs=900; fi - cd crates/tropic01-driver - # Enumerate the targets so a newly added one is fuzzed automatically. - for t in $(cargo +nightly fuzz list); do - cargo +nightly fuzz run "$t" --target x86_64-unknown-linux-gnu -- -max_total_time="$secs" -timeout=10 - done + bash scripts/fuzz-gate.sh --secs "$secs" - name: Upload crash artifacts if: failure() uses: actions/upload-artifact@v4 with: name: fuzz-artifacts - path: crates/tropic01-driver/fuzz/artifacts + path: crates/*/fuzz/artifacts # Job 7: SonarQube Analysis sonarqube: diff --git a/README.md b/README.md index d30db00..8774a5a 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ scripts/ci-local.sh --quick # skip coverage and fuzz | Coverage | `cargo-llvm-cov` | library line coverage floor | | Advisories | `cargo deny` | blocks on any RustSec finding, SARIF export | | Dependency policy | `cargo deny` / `cargo udeps` | license allow-list, trusted sources, no yanked or unused crates | -| Fuzz | `cargo fuzz` | the driver's attacker-facing parsers, longer on the weekly schedule | +| Fuzz | `cargo fuzz` | the driver's wire parsers, the signed-image verifier, the update state machine. `scripts/fuzz-gate.sh` walks the tree for `fuzz/` projects and aborts if what it finds differs from the crate list pinned in that script, so adding a fuzz crate needs an edit there. Longer runs on the weekly schedule | | Quality scan | SonarQube | consumes the reports above | See [`.github/workflows/ci.yml`](.github/workflows/ci.yml) for the full pipeline and [`sonar-project.properties`](sonar-project.properties) for the SonarQube configuration. diff --git a/crates/fw-update/fuzz/fuzz_targets/drive_machine.rs b/crates/fw-update/fuzz/fuzz_targets/drive_machine.rs index 4e72e33..7f88bad 100644 --- a/crates/fw-update/fuzz/fuzz_targets/drive_machine.rs +++ b/crates/fw-update/fuzz/fuzz_targets/drive_machine.rs @@ -12,5 +12,5 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { - fw_update::fuzz::drive_machine(data); + let _ = fw_update::fuzz::drive_machine(data); }); diff --git a/crates/fw-update/src/lib.rs b/crates/fw-update/src/lib.rs index c8b6954..77e9dad 100644 --- a/crates/fw-update/src/lib.rs +++ b/crates/fw-update/src/lib.rs @@ -85,6 +85,7 @@ pub mod fuzz { use crate::MockFlash; use crate::MockSeCounter; + use crate::SE_COUNTER_ORIGIN; use crate::UpdateState; use crate::Updater; use image_verify::RootKey; @@ -95,17 +96,18 @@ pub mod fuzz /// from `data`, feeds them through [`Updater::receive_chunk`], then runs /// verify, commit, boot, and confirm. The machine must never panic and must /// never reach [`UpdateState::Committed`] for an image the verifier did not - /// accept. A genuinely valid image is essentially never produced by mutation, - /// so the path under test is the fail-closed rejection across the whole flow. - pub fn drive_machine(data: &[u8]) + /// accept. + /// + /// Returns true when the machine armed the commit. + pub fn drive_machine(data: &[u8]) -> bool { let root = match RootKey::from_bytes(crate::DEV_ROOT_KEY_TEST_ONLY) { Ok(key) => key, - Err(_) => return, + Err(_) => return false, }; let flash = MockFlash::new(0); - let se = MockSeCounter::new(0); + let se = MockSeCounter::new(SE_COUNTER_ORIGIN); let mut up = Updater::new(&root, flash, se); // The first two bytes pick a declared length inside the modelled bank. @@ -122,7 +124,7 @@ pub mod fuzz if up.begin(total_len).is_err() { - return; + return false; } // Each record is a 1-byte length prefix then that many payload bytes, @@ -140,7 +142,7 @@ pub mod fuzz { // A rejected chunk fails closed: the machine must not commit. assert_ne!(up.state(), UpdateState::Committed); - return; + return false; } offset = offset.saturating_add(chunk.len()); rest = tail; @@ -152,15 +154,19 @@ pub mod fuzz // A rejected image must never have armed a swap. assert!(!up.flash().committed()); assert_ne!(up.state(), UpdateState::Committed); - return; + return false; } // The verifier accepted: the commit/confirm path must also stay sound. - if up.commit().is_ok() + if up.commit().is_err() { - let _ = up.on_boot(); - let _ = up.confirm(0); + return false; } + assert_eq!(up.state(), UpdateState::Committed); + assert!(up.flash().committed()); + let _ = up.on_boot(); + let _ = up.confirm(0); + true } } diff --git a/crates/fw-update/src/tests.rs b/crates/fw-update/src/tests.rs index ac11e3b..73b9c9e 100644 --- a/crates/fw-update/src/tests.rs +++ b/crates/fw-update/src/tests.rs @@ -636,3 +636,54 @@ fn page_constants_are_consistent() assert_eq!(PAGE_LEN, 256); assert_eq!(CONFIRM_BOOTS, 1); } + +#[cfg(feature = "_fuzz")] +fn frame_for_fuzz_seam(image: &[u8]) -> std::vec::Vec +{ + let mut framed = std::vec::Vec::new(); + let declared = image.len() as u16; + framed.extend_from_slice(&declared.to_le_bytes()); + for chunk in image.chunks(255) + { + framed.push(chunk.len() as u8); + framed.extend_from_slice(chunk); + } + framed +} + +#[cfg(feature = "_fuzz")] +#[test] +fn the_fuzz_seam_reaches_a_commit_for_a_signed_image() +{ + let image = build_image(DEV_SCALAR, 3, b"the fuzz seam must reach a commit"); + let framed = frame_for_fuzz_seam(&image); + + assert!( + crate::fuzz::drive_machine(&framed), + "the fuzz seam must ARM A COMMIT for an image signed with the dev scalar" + ); +} + +#[cfg(feature = "_fuzz")] +#[test] +fn the_fuzz_seam_rejects_an_image_signed_by_the_wrong_key() +{ + let image = build_image(WRONG_SCALAR, 3, b"the fuzz seam must reject this"); + let framed = frame_for_fuzz_seam(&image); + + assert!( + !crate::fuzz::drive_machine(&framed), + "an image signed by the wrong key must never arm a commit" + ); +} + +#[cfg(feature = "_fuzz")] +#[test] +fn the_fuzz_entry_point_survives_degenerate_inputs() +{ + assert!(!crate::fuzz::drive_machine(&[])); + assert!(!crate::fuzz::drive_machine(&[0x00])); + assert!(!crate::fuzz::drive_machine(&[0xFF, 0xFF])); + assert!(!crate::fuzz::drive_machine(&[0x00, 0x00, 0xFF, 0x01, 0x02])); + assert!(!crate::fuzz::drive_machine(&[0x10, 0x00, 0x02, 0xAA, 0xBB])); +} diff --git a/crates/tropic01-driver/fuzz/Cargo.lock b/crates/tropic01-driver/fuzz/Cargo.lock index cfc8e53..0fa8f77 100644 --- a/crates/tropic01-driver/fuzz/Cargo.lock +++ b/crates/tropic01-driver/fuzz/Cargo.lock @@ -548,7 +548,7 @@ dependencies = [ [[package]] name = "tropic01-driver" -version = "0.0.1" +version = "0.1.1" dependencies = [ "aes-gcm", "ecdsa", diff --git a/crates/tropic01-driver/src/lib.rs b/crates/tropic01-driver/src/lib.rs index 96e45c8..b8c67bc 100644 --- a/crates/tropic01-driver/src/lib.rs +++ b/crates/tropic01-driver/src/lib.rs @@ -228,34 +228,49 @@ pub mod fuzz let _ = crate::device::image_version(data); } - /// Drives the certificate-chain verifier over arbitrary bytes with a fixed - /// pinned anchor. Must never panic. The anchor's exact value is irrelevant: - /// fuzzing targets the bounded DER parsing in front of the crypto, which - /// fails closed on essentially every mutated input. + /// A fixed P-521 SEC1 point (`0x04 || X(66) || Y(66)`), the model TEST root. + /// + /// [`crate::cert::RootAnchor::from_sec1_p521`] validates the point and + /// returns an error for a bad one, and [`verify_cert_chain`] below can only + /// run the parser it fuzzes once that call succeeds. + #[cfg(feature = "attestation")] + pub(crate) const FUZZ_ANCHOR_POINT: [u8; 133] = [ + 0x04, 0x01, 0x35, 0xc7, 0xa2, 0x4d, 0x16, 0xb3, 0x74, 0xb2, 0x07, 0xad, + 0xe8, 0xfe, 0x50, 0xf5, 0x03, 0xad, 0x34, 0xe0, 0xe5, 0x96, 0xc8, 0x3f, + 0xc9, 0x8a, 0xdb, 0x4c, 0x43, 0x88, 0xca, 0x0a, 0xd9, 0xb2, 0x4e, 0x77, + 0xe9, 0x84, 0xb8, 0x97, 0x82, 0x53, 0xa8, 0xe0, 0xd6, 0xfd, 0x68, 0xea, + 0xa8, 0xd9, 0xc9, 0xa9, 0xa6, 0xc8, 0x83, 0x5a, 0x13, 0x8c, 0xcc, 0xff, + 0x51, 0x13, 0x0d, 0xa1, 0x09, 0x86, 0x80, 0x00, 0xcd, 0xf7, 0xfa, 0xd5, + 0xa0, 0x2b, 0xbd, 0x84, 0x45, 0x3c, 0x56, 0x36, 0xf2, 0x5f, 0x1c, 0x39, + 0x5b, 0xdc, 0x22, 0xee, 0x7b, 0x44, 0x1a, 0x81, 0xb5, 0x9f, 0x20, 0x40, + 0x53, 0x89, 0xf4, 0x7d, 0x65, 0xf0, 0x74, 0xa6, 0x02, 0xf9, 0x33, 0x2d, + 0xf1, 0x33, 0x79, 0xf2, 0x7d, 0x65, 0x4f, 0x4e, 0x1b, 0x0f, 0xd4, 0x56, + 0xc1, 0xa9, 0x9f, 0x54, 0x36, 0x64, 0x0f, 0x7e, 0xe0, 0x4e, 0x1b, 0x48, + 0x81, + ]; + + /// Drives the certificate-chain verifier over arbitrary bytes under the fixed + /// anchor above. Must never panic. #[cfg(feature = "attestation")] pub fn verify_cert_chain(data: &[u8]) { - // A fixed, REAL P-521 SEC1 point (0x04 || X(66) || Y(66)). The anchor now - // validates the point at construction, so a real on-curve point is used. - // Its exact value is irrelevant to the fuzz target, which exercises the - // bounded DER parsing in front of the crypto. This is the model TEST root. - const FUZZ_ANCHOR_POINT: [u8; 133] = [ - 0x04, 0x01, 0x35, 0xc7, 0xa2, 0x4d, 0x16, 0xb3, 0x74, 0xb2, 0x07, 0xad, - 0xe8, 0xfe, 0x50, 0xf5, 0x03, 0xad, 0x34, 0xe0, 0xe5, 0x96, 0xc8, 0x3f, - 0xc9, 0x8a, 0xdb, 0x4c, 0x43, 0x88, 0xca, 0x0a, 0xd9, 0xb2, 0x4e, 0x77, - 0xe9, 0x84, 0xb8, 0x97, 0x82, 0x53, 0xa8, 0xe0, 0xd6, 0xfd, 0x68, 0xea, - 0xa8, 0xd9, 0xc9, 0xa9, 0xa6, 0xc8, 0x83, 0x5a, 0x13, 0x8c, 0xcc, 0xff, - 0x51, 0x13, 0x0d, 0xa1, 0x09, 0x86, 0x80, 0x00, 0xcd, 0xf7, 0xfa, 0xd5, - 0xa0, 0x2b, 0xbd, 0x84, 0x45, 0x3c, 0x56, 0x36, 0xf2, 0x5f, 0x1c, 0x39, - 0x5b, 0xdc, 0x22, 0xee, 0x7b, 0x44, 0x1a, 0x81, 0xb5, 0x9f, 0x20, 0x40, - 0x53, 0x89, 0xf4, 0x7d, 0x65, 0xf0, 0x74, 0xa6, 0x02, 0xf9, 0x33, 0x2d, - 0xf1, 0x33, 0x79, 0xf2, 0x7d, 0x65, 0x4f, 0x4e, 0x1b, 0x0f, 0xd4, 0x56, - 0xc1, 0xa9, 0x9f, 0x54, 0x36, 0x64, 0x0f, 0x7e, 0xe0, 0x4e, 0x1b, 0x48, - 0x81, - ]; if let Ok(anchor) = crate::cert::RootAnchor::from_sec1_p521(&FUZZ_ANCHOR_POINT) { let _ = crate::cert::verify_cert_chain(data, &anchor); } } + + #[cfg(all(test, feature = "attestation"))] + mod tests + { + #[test] + fn the_fuzz_anchor_point_is_a_valid_p521_point() + { + assert! + ( + crate::cert::RootAnchor::from_sec1_p521(&super::FUZZ_ANCHOR_POINT).is_ok(), + "the fuzz anchor must be accepted, or verify_cert_chain fuzzes nothing" + ); + } + } } diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh index 6542a68..c455f2f 100755 --- a/scripts/ci-local.sh +++ b/scripts/ci-local.sh @@ -165,14 +165,7 @@ coverage_stage() fuzz_stage() { - ( - cd crates/tropic01-driver || exit 1 - # Enumerate the targets so a newly added one is fuzzed automatically. - for t in $(cargo +nightly fuzz list) - do - cargo +nightly fuzz run "$t" -- -max_total_time="$FUZZ_SECS" -timeout=10 || exit 1 - done - ) + bash scripts/fuzz-gate.sh --secs "$FUZZ_SECS" } embedded_stage() @@ -227,7 +220,8 @@ RUSTFLAGS="-D warnings" run "check (thumbv8m)" \ cargo check -p tropic01-driver --locked --target thumbv8m.main-none-eabihf unset RUSTFLAGS -run "test (host)" cargo test --workspace --locked +run "test (host)" cargo test --workspace --locked \ + --features fw-update/_fuzz,image-verify/_fuzz,tropic01-driver/_fuzz run "clippy (json report + strict)" clippy_reports @@ -274,7 +268,7 @@ then if have cargo-fuzz && rustup toolchain list | grep -q nightly then - run "fuzz (${FUZZ_SECS}s per target)" fuzz_stage + run "fuzz (every fuzz crate, ${FUZZ_SECS}s per target)" fuzz_stage else skip "fuzz" "cargo install cargo-fuzz (and a nightly toolchain)" fi diff --git a/scripts/fuzz-gate.sh b/scripts/fuzz-gate.sh new file mode 100755 index 0000000..9b789ce --- /dev/null +++ b/scripts/fuzz-gate.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +# +# Runs every libFuzzer target of every fuzz crate in the tree. +# +# Called from .github/workflows/ci.yml and from scripts/ci-local.sh. +# +# The crate list is pinned here. Adding or removing a fuzz crate +# means editing this list. +EXPECTED_CRATES=( + crates/fw-update + crates/image-verify + crates/tropic01-driver +) + +# Usage: +# scripts/fuzz-gate.sh [--secs N] [dir ...] +# +# Without a directory argument every pinned crate runs. +# +# Exits 0 when the discovered crate set matches the pinned set and every target +# of every selected crate survives its run. Exits 1 on a mismatch, a crash, a +# build failure or an unreadable fuzz manifest. +set -euo pipefail + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +norm() +{ + grep -v '^[[:space:]]*$' <<< "${1:-}" | LC_ALL=C sort || true +} + +host_triple() +{ + rustc +nightly -vV | sed -n 's/^host: //p' +} + +check_pin_sanity() +{ + if [ "${#EXPECTED_CRATES[@]}" -eq 0 ] + then + echo "ERROR: EXPECTED_CRATES in scripts/fuzz-gate.sh is empty." >&2 + return 1 + fi + + local dupes + dupes=$(printf '%s\n' "${EXPECTED_CRATES[@]}" | LC_ALL=C sort | uniq -d) + if [ -n "$dupes" ] + then + echo "ERROR: EXPECTED_CRATES in scripts/fuzz-gate.sh lists a crate twice:" >&2 + printf ' %s\n' $dupes >&2 + return 1 + fi +} + +fuzz_manifests() +{ + # Prints every fuzz manifest in the repository, sorted. + # + # The walk has no depth bound, because a fuzz project sits wherever its crate + # sits, from `./fuzz` at the repository root down to a nested crate. + # + # Symlinks stay unfollowed. A directory link can list one crate twice, pull + # manifests from outside the repository into discovery, or cycle. + # + # The exclusions drop manifests that are not source: build output under any + # `target/` and git internals under `.git/`, where dependency and submodule + # sources carry their own fuzz projects. A vendored copy elsewhere in the tree + # still shows up, and aborts the gate because it is not in EXPECTED_CRATES. + find . -type f -path '*/fuzz/Cargo.toml' \ + -not -path '*/target/*' \ + -not -path '*/.git/*' \ + -print \ + | sed 's|^\./||' \ + | LC_ALL=C sort +} + +has_fuzz_marker() +{ + awk ' + /^[[:space:]]*\[/ { + section = $0 + sub(/#.*$/, "", section) + gsub(/[[:space:]]/, "", section) + gsub(/"/, "", section) + next + } + section == "[package.metadata]" \ + && /^[[:space:]]*cargo-fuzz[[:space:]]*=[[:space:]]*true/ { found = 1 } + section == "[package]" \ + && /^[[:space:]]*metadata[[:space:]]*=/ \ + && /cargo-fuzz[[:space:]]*=[[:space:]]*true/ { found = 1 } + END { exit(found ? 0 : 1) } + ' "$1" +} + +check_crate_set() +{ + # Stdout only. Folding stderr in here would turn any warning the walk emits + # while still succeeding into a fake manifest path, and the gate would then + # abort blaming a file that does not exist. Letting stderr through to the + # terminal keeps the real message visible and the data clean. + local listing + if ! listing=$(fuzz_manifests) + then + echo "ERROR: the walk for fuzz manifests failed, see the message above." >&2 + return 1 + fi + + local manifests=() + mapfile -t manifests < <(norm "$listing") + if [ "${#manifests[@]}" -eq 0 ] + then + echo "ERROR: no */fuzz/Cargo.toml found anywhere in the tree." >&2 + echo " The gate expects these crates to carry one:" >&2 + printf ' %s\n' "${EXPECTED_CRATES[@]}" >&2 + return 1 + fi + + local found=() manifest + for manifest in "${manifests[@]}" + do + if ! has_fuzz_marker "$manifest" + then + echo "ERROR: $manifest carries no readable 'cargo-fuzz = true' marker." >&2 + echo " The gate refuses to skip it. Either it is a fuzz project and" >&2 + echo " the marker is missing or written in a shape this gate cannot" >&2 + echo " read, or it is not one and does not belong at this path." >&2 + return 1 + fi + found+=("$(dirname "$(dirname "$manifest")")") + done + + local discovered pinned missing extra + discovered=$(norm "$(printf '%s\n' "${found[@]}")") + pinned=$(norm "$(printf '%s\n' "${EXPECTED_CRATES[@]}")") + extra=$(comm -23 <(printf '%s\n' "$discovered") <(printf '%s\n' "$pinned") || true) + missing=$(comm -13 <(printf '%s\n' "$discovered") <(printf '%s\n' "$pinned") || true) + + if [ -n "$extra" ] + then + echo "ERROR: a fuzz crate exists in the tree but is NOT pinned in this gate:" >&2 + printf ' %s\n' $extra >&2 + echo " Add it to EXPECTED_CRATES in scripts/fuzz-gate.sh. Until then it" >&2 + echo " is not fuzzed, and the gate refuses to report success over a" >&2 + echo " crate it does not cover." >&2 + return 1 + fi + if [ -n "$missing" ] + then + echo "ERROR: a fuzz crate is pinned in this gate but was NOT found in the tree:" >&2 + printf ' %s\n' $missing >&2 + echo " Either its fuzz project moved or was deleted, or the marker in its" >&2 + echo " manifest stopped being readable. If the removal was intended, drop" >&2 + echo " it from EXPECTED_CRATES in the same change." >&2 + return 1 + fi + + printf '%s\n' "$discovered" +} + +fuzz_crate() +{ + local dir=$1 secs=$2 + + local list_out rc=0 + list_out=$(cargo +nightly fuzz list --fuzz-dir "$dir/fuzz" 2>&1) || rc=1 + if [ "$rc" -ne 0 ] + then + echo " FAIL $dir: could not LIST the fuzz targets" >&2 + echo "$list_out" >&2 + return 1 + fi + + local targets=() + mapfile -t targets < <(norm "$list_out") + if [ "${#targets[@]}" -eq 0 ] + then + echo " FAIL $dir: the fuzz project declares no target" >&2 + return 1 + fi + + local t + for t in "${targets[@]}" + do + echo ">> fuzz [$dir] $t on $HOST_TRIPLE for ${secs}s" + cargo +nightly fuzz run "$t" --fuzz-dir "$dir/fuzz" --target "$HOST_TRIPLE" \ + -- -max_total_time="$secs" -timeout=10 || return 1 + RAN=$((RAN + 1)) + done +} + +main() +{ + local secs=60 + local dirs=() + while [ $# -gt 0 ] + do + case "$1" in + --secs) shift; secs=${1:?--secs needs a value} ;; + -*) echo "unknown option: $1" >&2; return 2 ;; + *) dirs+=("$1") ;; + esac + shift + done + + if ! [[ "$secs" =~ ^[0-9]+$ ]] || [ "$secs" -lt 1 ] + then + echo "ERROR: --secs must be a whole number of seconds, 1 or more (got '$secs')." >&2 + echo " libFuzzer treats 0 as no limit, which would never return." >&2 + return 1 + fi + + check_pin_sanity || return 1 + + HOST_TRIPLE=$(host_triple) + if [ -z "$HOST_TRIPLE" ] + then + echo "ERROR: could not read the host triple from 'rustc +nightly -vV'." >&2 + echo " The gate needs it to build the fuzz targets for this machine." >&2 + return 1 + fi + + local crate_list + crate_list=$(check_crate_set) || return 1 + local discovered=() + mapfile -t discovered < <(printf '%s\n' "$crate_list") + + if [ "${#dirs[@]}" -eq 0 ] + then + dirs=("${discovered[@]}") + else + local want found d + for want in "${dirs[@]}" + do + found=0 + for d in "${discovered[@]}" + do + [ "$d" = "$want" ] && found=1 + done + if [ "$found" -ne 1 ] + then + echo "ERROR: '$want' is not a fuzz project in this tree." >&2 + echo " Discovered: ${discovered[*]}" >&2 + return 1 + fi + done + fi + + RAN=0 + local d + for d in "${dirs[@]}" + do + fuzz_crate "$d" "$secs" || return 1 + done + + if [ "$RAN" -eq 0 ] + then + echo "ERROR: the fuzz gate ran zero targets." >&2 + return 1 + fi + echo "fuzz gate: targets=${RAN} crates=${#dirs[@]} secs=${secs}" +} + +main "$@"