From 97f47025e3840e3ea724707e0b377bed7ad4186c Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 21 May 2026 17:40:00 +0800 Subject: [PATCH] feat: afl fuzzing with preinstalled binary --- .github/workflows/fuzz-afl.yml | 226 +++++++++++++ Justfile | 310 +++++++++++++++++- docs/fuzzing.md | 306 +++++++++++++++-- fuzz/Cargo.lock | 29 ++ fuzz/Cargo.toml | 14 +- fuzz/build.rs | 25 ++ fuzz/build_stubs/sys_alloc_aligned.c | 26 ++ fuzz/fuzz_targets/_template.rs | 28 +- .../fuzz_apply_state_diff_split_path.rs | 26 +- fuzz/fuzz_targets/fuzz_block_verification.rs | 5 +- fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs | 5 +- .../fuzz_multi_block_state_sequence.rs | 26 +- .../fuzz_program_deployment_lifecycle.rs | 5 +- fuzz/fuzz_targets/fuzz_replay_prevention.rs | 32 +- .../fuzz_sequencer_vs_replayer.rs | 5 +- .../fuzz_signature_verification.rs | 5 +- .../fuzz_state_diff_computation.rs | 5 +- fuzz/fuzz_targets/fuzz_state_serialization.rs | 5 +- fuzz/fuzz_targets/fuzz_state_transition.rs | 32 +- .../fuzz_stateless_verification.rs | 5 +- .../fuzz_targets/fuzz_transaction_decoding.rs | 5 +- .../fuzz_validate_execute_consistency.rs | 24 +- .../fuzz_witness_set_verification.rs | 5 +- fuzz_props/Cargo.toml | 4 + fuzz_props/src/generators.rs | 24 ++ fuzz_props/src/lib.rs | 20 ++ scripts/add_fuzz_target.py | 29 +- 27 files changed, 1034 insertions(+), 197 deletions(-) create mode 100644 .github/workflows/fuzz-afl.yml create mode 100644 fuzz/build.rs create mode 100644 fuzz/build_stubs/sys_alloc_aligned.c diff --git a/.github/workflows/fuzz-afl.yml b/.github/workflows/fuzz-afl.yml new file mode 100644 index 0000000..069dfe8 --- /dev/null +++ b/.github/workflows/fuzz-afl.yml @@ -0,0 +1,226 @@ +name: AFL++ Fuzzing + +on: + schedule: + - cron: "0 2 * * *" # nightly at 02:00 UTC + workflow_dispatch: # manual trigger + +env: + RISC0_DEV_MODE: "1" + +jobs: + # ──────────────────────────────────────────────────────────────────────────── + # afl-smoke — 120-second campaign for 7 priority targets + # ──────────────────────────────────────────────────────────────────────────── + afl-smoke: + name: "AFL++ smoke — ${{ matrix.target }}" + runs-on: ubuntu-latest + container: + image: aflplusplus/aflplusplus:v4.40c + + strategy: + fail-fast: false + matrix: + target: + - fuzz_transaction_decoding + - fuzz_encoding_roundtrip + - fuzz_state_serialization + - fuzz_stateless_verification + - fuzz_state_transition + - fuzz_apply_state_diff_split_path + - fuzz_sequencer_vs_replayer + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Cache fuzz/target + uses: actions/cache@v4 + with: + path: fuzz/target + key: afl-fuzz-target-${{ matrix.target }}-${{ hashFiles('fuzz/Cargo.lock') }} + restore-keys: | + afl-fuzz-target-${{ matrix.target }}- + + - name: Install cargo-afl + run: cargo install cargo-afl --locked + + - name: Build fuzz target + run: | + cd fuzz && cargo afl build \ + --no-default-features \ + --features fuzzer-afl \ + --release \ + --bin ${{ matrix.target }} + + - name: Create corpus directory + run: mkdir -p fuzz/corpus/${{ matrix.target }} + + - name: Run AFL++ for 120 seconds + run: | + mkdir -p afl-output/${{ matrix.target }} + timeout 120 \ + afl-fuzz \ + -i fuzz/corpus/${{ matrix.target }} \ + -o afl-output/${{ matrix.target }} \ + -- fuzz/target/release/${{ matrix.target }} \ + || true # timeout exit code 124 is expected + + - name: Sync queue entries to shared corpus + run: | + TARGET="${{ matrix.target }}" + DEST="fuzz/corpus/${TARGET}" + mkdir -p "$DEST" + count=0 + for instance_dir in afl-output/${TARGET}/*/; do + QUEUE="${instance_dir}queue" + [ -d "$QUEUE" ] || continue + for f in "$QUEUE"/id:*; do + [ -f "$f" ] || continue + HASH=$(sha1sum "$f" | cut -d' ' -f1) + DEST_FILE="${DEST}/${HASH}" + if [ ! -f "$DEST_FILE" ]; then + cp "$f" "$DEST_FILE" + count=$((count + 1)) + fi + done + done + echo "Synced ${count} new input(s) to ${DEST}" + + - name: Open corpus PR (if new inputs found) + uses: peter-evans/create-pull-request@v6 + with: + commit-message: "chore: add AFL++ corpus entries for ${{ matrix.target }}" + title: "AFL++ corpus update — ${{ matrix.target }}" + body: | + Automated corpus update from the nightly AFL++ smoke run. + Target: `${{ matrix.target }}` + branch: "afl-corpus/${{ matrix.target }}" + add-paths: "fuzz/corpus/${{ matrix.target }}/" + delete-branch: true + + - name: Upload crashes and hangs artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: afl-findings-${{ matrix.target }} + path: | + afl-output/${{ matrix.target }}/*/crashes/ + afl-output/${{ matrix.target }}/*/hangs/ + if-no-files-found: ignore + + # ──────────────────────────────────────────────────────────────────────────── + # afl-coverage — LLVM coverage report for 3 key targets + # ──────────────────────────────────────────────────────────────────────────── + afl-coverage: + name: "AFL++ coverage — ${{ matrix.target }}" + runs-on: ubuntu-latest + needs: afl-smoke + + strategy: + fail-fast: false + matrix: + target: + - fuzz_state_transition + - fuzz_sequencer_vs_replayer + - fuzz_apply_state_diff_split_path + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust nightly + llvm-tools-preview + uses: dtolnay/rust-toolchain@nightly + with: + components: llvm-tools-preview + + - name: Install cargo-afl + run: cargo install cargo-afl --locked + + - name: Download smoke findings for ${{ matrix.target }} + uses: actions/download-artifact@v4 + with: + name: afl-findings-${{ matrix.target }} + path: afl-output/${{ matrix.target }} + continue-on-error: true # no crashes/hangs is fine + + - name: Build with LLVM instrumented coverage + env: + RUSTFLAGS: "-C instrument-coverage" + RISC0_DEV_MODE: "1" + run: | + cargo build \ + --manifest-path fuzz/Cargo.toml \ + --no-default-features \ + --features fuzzer-afl \ + --release \ + --bin ${{ matrix.target }} + + - name: Run corpus + queue entries through instrumented binary + run: | + TARGET="${{ matrix.target }}" + BINARY="fuzz/target/release/${TARGET}" + PROFRAW_DIR="coverage/afl/${TARGET}/profraw" + mkdir -p "$PROFRAW_DIR" + idx=0 + + # Shared corpus + for f in fuzz/corpus/${TARGET}/id:* fuzz/corpus/${TARGET}/*; do + [ -f "$f" ] || continue + LLVM_PROFILE_FILE="${PROFRAW_DIR}/${idx}.profraw" "$BINARY" < "$f" 2>/dev/null || true + idx=$((idx + 1)) + done + + # AFL++ queue entries (if available from the smoke job) + for instance_dir in afl-output/${TARGET}/*/; do + QUEUE="${instance_dir}queue" + [ -d "$QUEUE" ] || continue + for f in "$QUEUE"/id:*; do + [ -f "$f" ] || continue + LLVM_PROFILE_FILE="${PROFRAW_DIR}/${idx}.profraw" "$BINARY" < "$f" 2>/dev/null || true + idx=$((idx + 1)) + done + done + echo "Ran ${idx} inputs through ${TARGET}" + + - name: Merge raw profiles + run: | + TARGET="${{ matrix.target }}" + PROFRAW_DIR="coverage/afl/${TARGET}/profraw" + PROFDATA="coverage/afl/${TARGET}/merged.profdata" + shopt -s nullglob + files=("${PROFRAW_DIR}"/*.profraw) + if [ ${#files[@]} -eq 0 ]; then + echo "No .profraw files found — skipping merge." + exit 0 + fi + llvm-profdata merge -sparse "${files[@]}" -o "$PROFDATA" + + - name: Generate HTML coverage report + run: | + TARGET="${{ matrix.target }}" + BINARY="fuzz/target/release/${TARGET}" + PROFDATA="coverage/afl/${TARGET}/merged.profdata" + HTML_DIR="coverage/afl/${TARGET}/html" + if [ ! -f "$PROFDATA" ]; then + echo "No profdata — skipping HTML report." + exit 0 + fi + mkdir -p "$HTML_DIR" + llvm-cov show \ + "$BINARY" \ + --instr-profile="$PROFDATA" \ + --format=html \ + --output-dir="$HTML_DIR" \ + --ignore-filename-regex='\.cargo|rustc' + echo "Coverage report: ${HTML_DIR}/index.html" + + - name: Upload coverage report artifact + uses: actions/upload-artifact@v4 + with: + name: afl-coverage-${{ matrix.target }} + path: coverage/afl/${{ matrix.target }}/html/ + if-no-files-found: ignore diff --git a/Justfile b/Justfile index 2dc0ac9..279dcba 100644 --- a/Justfile +++ b/Justfile @@ -90,8 +90,304 @@ new-target NAME: # ── 3 & 4. Update Cargo.toml and fuzz.yml automatically ────────────────── python3 scripts/add_fuzz_target.py "$TARGET" echo "" - echo "Done! Verify the build with:" + echo "Done! Verify the libFuzzer build with:" echo " RISC0_DEV_MODE=1 cargo fuzz build ${TARGET}" + echo "" + echo "Verify the AFL++ build with:" + echo " cd fuzz && cargo afl build --no-default-features --features fuzzer-afl --release --bin ${TARGET}" + +# ── AFL++ fuzzing ────────────────────────────────────────────────────────────── +# Prerequisites (install once): +# macOS: brew install afl-fuzz && cargo install cargo-afl +# Linux: Build AFL++ from source (recommended — Debian/Ubuntu apt packages are +# several major versions behind; see https://github.com/AFLplusplus/AFLplusplus): +# git clone https://github.com/AFLplusplus/AFLplusplus +# cd AFLplusplus && make distrib && sudo make install +# Then: cargo install cargo-afl + +# Build ALL fuzz targets for AFL++ (output: fuzz/target/release/) +afl-build: + cd fuzz && cargo afl build --no-default-features --features fuzzer-afl --release + +# Build a SINGLE fuzz target for AFL++ +# Usage: just afl-build-target fuzz_state_transition +afl-build-target TARGET: + cd fuzz && cargo afl build --no-default-features --features fuzzer-afl --release --bin {{TARGET}} + +# Disable the macOS crash reporter daemon so AFL++ can detect crashes reliably. +# This is a macOS-only requirement; on Linux this is a no-op. +# The `fuzz-afl` recipe calls this automatically; run it manually if you want +# to keep the reporter disabled across multiple just invocations. +# +# Re-enable with: just afl-macos-teardown +afl-macos-setup: + #!/bin/bash + if [ "$(uname)" != "Darwin" ]; then echo "Not macOS — nothing to do."; exit 0; fi + SL=/System/Library; PL=com.apple.ReportCrash + echo "Disabling macOS crash reporter (required by AFL++)…" + launchctl unload -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true + sudo launchctl unload -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + echo "Done. Re-enable with: just afl-macos-teardown" + +# Re-enable the macOS crash reporter after an AFL++ session. +afl-macos-teardown: + #!/bin/bash + if [ "$(uname)" != "Darwin" ]; then echo "Not macOS — nothing to do."; exit 0; fi + SL=/System/Library; PL=com.apple.ReportCrash + echo "Re-enabling macOS crash reporter…" + launchctl load -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true + sudo launchctl load -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + echo "Done." + +# Run AFL++ on one target or ALL targets when no target is supplied. +# Builds binaries as needed; syncs the queue to the shared corpus when done. +# +# On macOS the crash reporter is disabled automatically for the duration of the +# run and re-enabled when the script exits (via a shell trap). +# +# Requires afl-fuzz and cargo-afl to be installed locally: +# macOS: brew install afl-fuzz && cargo install cargo-afl +# Linux: Build AFL++ from source (apt packages are several major versions +# behind): see https://github.com/AFLplusplus/AFLplusplus +# +# Usage: just fuzz-afl # all targets, 120 s each +# just fuzz-afl "" 60 # all targets, 60 s each +# just fuzz-afl fuzz_state_transition # single target, 120 s +# just fuzz-afl fuzz_state_transition 300 # single target, 300 s +fuzz-afl TARGET="" TIME="120": + #!/bin/bash + set -euo pipefail + TARGET="{{TARGET}}" + TIME="{{TIME}}" + + # ── Collect targets to run ──────────────────────────────────────────────── + if [ -z "$TARGET" ]; then + TARGETS=($(cargo fuzz list 2>/dev/null)) + else + TARGETS=("$TARGET") + fi + + # ── Require local AFL++ installation ───────────────────────────────────── + if ! command -v afl-fuzz &>/dev/null; then + echo "ERROR: afl-fuzz not found in PATH." + echo "" + echo "Install AFL++ before running this recipe:" + echo "" + echo " macOS : brew install afl-fuzz" + echo "" + echo " Linux : Build from source (apt packages are several major versions behind):" + echo " git clone https://github.com/AFLplusplus/AFLplusplus" + echo " cd AFLplusplus && make distrib && sudo make install" + echo "" + echo "Also install the cargo-afl build wrapper:" + echo " cargo install cargo-afl" + echo "" + exit 1 + fi + if ! command -v cargo-afl &>/dev/null && ! cargo afl --version &>/dev/null 2>&1; then + echo "ERROR: cargo-afl not found." + echo " cargo install cargo-afl" + exit 1 + fi + + # ── macOS: disable crash reporter for the duration of this run ─────────── + if [ "$(uname)" = "Darwin" ]; then + SL=/System/Library; PL=com.apple.ReportCrash + echo "macOS: disabling crash reporter (AFL++ requirement)…" + launchctl unload -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true + sudo launchctl unload -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + # Re-enable on any exit — normal, error, or Ctrl-C + trap ' + echo "Re-enabling macOS crash reporter…" + SL=/System/Library; PL=com.apple.ReportCrash + launchctl load -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true + sudo launchctl load -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + ' EXIT + fi + + # ── Run targets ─────────────────────────────────────────────────────────── + _run_one() { + local t="$1" + local BINARY="fuzz/target/release/$t" + local CORPUS="fuzz/corpus/$t" + local OUTPUT="afl-output/$t" + mkdir -p "$CORPUS" "$OUTPUT" + if [ ! -f "$BINARY" ]; then + echo "Binary not found — building $t first…" + just afl-build-target "$t" + fi + timeout "$TIME" afl-fuzz -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" || true + } + for t in "${TARGETS[@]}"; do + echo "=== afl++ $t for ${TIME}s ===" + _run_one "$t" + done + just afl-corpus-sync + +# Run AFL++ with N parallel instances (1 main + N-1 secondary) for TIME seconds. +# Requires that afl-fuzz is on PATH; all instances share afl-output/{{TARGET}}/. +# On macOS the crash reporter is disabled automatically for the duration of the +# run and re-enabled when the script exits. +# +# Usage: just fuzz-afl-parallel fuzz_state_transition +# just fuzz-afl-parallel fuzz_state_transition 8 600 +fuzz-afl-parallel TARGET WORKERS="4" TIME="300": + #!/bin/bash + set -euo pipefail + BINARY="fuzz/target/release/{{TARGET}}" + CORPUS="fuzz/corpus/{{TARGET}}" + OUTPUT="afl-output/{{TARGET}}" + mkdir -p "$CORPUS" "$OUTPUT" + if [ ! -f "$BINARY" ]; then + echo "Binary not found — building first…" + just afl-build-target {{TARGET}} + fi + # ── macOS: disable crash reporter for the duration of this run ─────────── + if [ "$(uname)" = "Darwin" ]; then + SL=/System/Library; PL=com.apple.ReportCrash + echo "macOS: disabling crash reporter (AFL++ requirement)…" + launchctl unload -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true + sudo launchctl unload -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + trap ' + echo "Re-enabling macOS crash reporter…" + SL=/System/Library; PL=com.apple.ReportCrash + launchctl load -w "${SL}/LaunchAgents/${PL}.plist" 2>/dev/null || true + sudo launchctl load -w "${SL}/LaunchDaemons/${PL}.Root.plist" 2>/dev/null || true + ' EXIT + fi + # Main instance + afl-fuzz -M main -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" & + # Secondary instances + for i in $(seq 1 $(( {{WORKERS}} - 1 ))); do + afl-fuzz -S "secondary${i}" -i "$CORPUS" -o "$OUTPUT" -- "$BINARY" & + done + sleep {{TIME}} + kill $(jobs -p) 2>/dev/null || true + wait 2>/dev/null || true + just afl-corpus-sync + +# Copy all queue entries from every AFL++ output directory into the matching +# shared corpus directory (fuzz/corpus//). Run after any AFL++ session +# to make new interesting inputs available to cargo-fuzz and CI. +afl-corpus-sync: + #!/bin/bash + set -euo pipefail + if [ ! -d afl-output ]; then + echo "afl-output/ does not exist — nothing to sync." + exit 0 + fi + for target_dir in afl-output/*/; do + TARGET=$(basename "$target_dir") + DEST="fuzz/corpus/${TARGET}" + mkdir -p "$DEST" + count=0 + for instance_dir in "$target_dir"*/; do + QUEUE="${instance_dir}queue" + [ -d "$QUEUE" ] || continue + for f in "$QUEUE"/id:*; do + [ -f "$f" ] || continue + HASH=$(sha1sum "$f" | cut -d' ' -f1) + DEST_FILE="${DEST}/${HASH}" + if [ ! -f "$DEST_FILE" ]; then + cp "$f" "$DEST_FILE" + count=$((count + 1)) + fi + done + done + echo "Synced $count new input(s) → $DEST" + done + +# Show AFL++ campaign statistics for a target +# Usage: just afl-status fuzz_state_transition +afl-status TARGET: + afl-whatsup afl-output/{{TARGET}} + +# Minimise a crash or hang artifact to the smallest reproducing input. +# Usage: just afl-tmin fuzz_state_transition afl-output/fuzz_state_transition/crashes/id:000000,... +afl-tmin TARGET ARTIFACT: + afl-tmin -i {{ARTIFACT}} -o {{ARTIFACT}}.min -- fuzz/target/release/{{TARGET}} + +# Pretty-print an AFL++ artifact as a Rust byte-string literal (for copy-paste +# into a unit test or issue report). +# Usage: just afl-fmt afl-output/fuzz_state_transition/crashes/id:000000,... +afl-fmt ARTIFACT: + python3 -c "import sys; data=open(sys.argv[1],'rb').read(); print('b\"' + ''.join(f'\\\\x{b:02x}' for b in data) + '\"')" {{ARTIFACT}} + +# ── Coverage ────────────────────────────────────────────────────────────────── + +# Generate a coverage report for a single target. +# +# Step 1 (libFuzzer): cargo fuzz coverage {{TARGET}} +# Step 2 (AFL++, only if afl-output/{{TARGET}}/ exists): +# Build with instrument-coverage, run the AFL++ queue through the binary, +# merge raw profiles, and generate an HTML report in coverage/afl/{{TARGET}}/. +# +# Usage: just coverage fuzz_state_transition +coverage TARGET: + #!/bin/bash + set -euo pipefail + # ── Step 1: libFuzzer coverage ──────────────────────────────────────────── + echo "=== cargo fuzz coverage {{TARGET}} ===" + cargo fuzz coverage {{TARGET}} || true + + # ── Step 2: AFL++ LLVM coverage (only if queue data exists) ────────────── + AFL_OUTPUT="afl-output/{{TARGET}}" + if [ ! -d "$AFL_OUTPUT" ]; then + echo "No AFL++ output for {{TARGET}} — skipping AFL++ coverage step." + exit 0 + fi + echo "=== AFL++ LLVM coverage for {{TARGET}} ===" + BINARY_DIR="fuzz/target/release" + COV_DIR="coverage/afl/{{TARGET}}" + PROFRAW_DIR="${COV_DIR}/profraw" + mkdir -p "$PROFRAW_DIR" + + # Build the target with LLVM instrumentation enabled. + RUSTFLAGS="-C instrument-coverage" \ + cargo build \ + --manifest-path fuzz/Cargo.toml \ + --no-default-features \ + --features fuzzer-afl \ + --release \ + --bin {{TARGET}} + + BINARY="${BINARY_DIR}/{{TARGET}}" + + # Run every queue entry through the instrumented binary. + idx=0 + for instance_dir in "$AFL_OUTPUT"/*/; do + QUEUE="${instance_dir}queue" + [ -d "$QUEUE" ] || continue + for f in "$QUEUE"/id:*; do + [ -f "$f" ] || continue + LLVM_PROFILE_FILE="${PROFRAW_DIR}/${idx}.profraw" "$BINARY" < "$f" 2>/dev/null || true + idx=$((idx + 1)) + done + done + + # Merge raw profiles. + PROFDATA="${COV_DIR}/merged.profdata" + llvm-profdata merge -sparse "${PROFRAW_DIR}"/*.profraw -o "$PROFDATA" + + # Generate HTML report. + HTML_DIR="${COV_DIR}/html" + mkdir -p "$HTML_DIR" + llvm-cov show \ + "$BINARY" \ + --instr-profile="$PROFDATA" \ + --format=html \ + --output-dir="$HTML_DIR" \ + --ignore-filename-regex='\.cargo|rustc' + echo "AFL++ HTML coverage report: ${HTML_DIR}/index.html" + +# Generate coverage for ALL registered fuzz targets (libFuzzer + AFL++). +coverage-all: + #!/bin/bash + set -euo pipefail + for target in $(cargo fuzz list 2>/dev/null); do + echo "=== coverage $target ===" + just coverage "$target" + done # ── Housekeeping ────────────────────────────────────────────────────────────── @@ -104,9 +400,13 @@ clean: clean-artifacts: rm -rf fuzz/artifacts/ -# Remove coverage reports generated by `cargo fuzz coverage` +# Remove coverage reports generated by `cargo fuzz coverage` and `just coverage` clean-coverage: - rm -rf fuzz/coverage/ + rm -rf fuzz/coverage/ coverage/ -# Remove everything: builds, artifacts, and coverage -clean-all: clean clean-artifacts clean-coverage +# Remove AFL++ output directories (crash/hang/queue findings) +clean-afl: + rm -rf afl-output/ + +# Remove everything: builds, artifacts, coverage, and AFL++ output +clean-all: clean clean-artifacts clean-coverage clean-afl diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 78e5535..796130a 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -9,14 +9,54 @@ directory that must be cloned separately). --- +## Architecture + +The fuzz workspace (`fuzz/`) is a single Cargo workspace that covers **both** fuzzing +engines via Cargo features. No separate Cargo manifest is needed. + +| | libFuzzer lane | AFL++ lane | +|---|---|---| +| **Build command** | `cargo fuzz build ` | `cd fuzz && cargo afl build --no-default-features --features fuzzer-afl --release --bin ` | +| **Run command** | `cargo fuzz run ` | `afl-fuzz -i fuzz/corpus/ -o afl-output/ -- fuzz/target/release/` | +| **Cargo feature** | `fuzzer-libfuzzer` (default) | `fuzzer-afl` | +| **Harness entry** | `::libfuzzer_sys::fuzz_target!(…)` | `fn main() { ::afl::fuzz!(…) }` | +| **`main()` presence** | Suppressed via `#![no_main]` | Required; provided by `afl::fuzz!` | +| **`fuzz/Cargo.toml`** | ✅ Source of truth | ✅ Same file — covers both lanes | + +The engine is selected at the call site via the `fuzz_props::fuzz_entry!` macro: + +```rust +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] + +fuzz_props::fuzz_entry!(|data: &[u8]| { + // … harness body … +}); +``` + +The `cfg` attributes in the macro expansion resolve against the **calling crate's** features +(`fuzz/`), not `fuzz_props`'s features. + +--- + ## Prerequisites ```bash -# Rust nightly is required by cargo-fuzz / libFuzzer +# libFuzzer lane rustup install nightly rustup component add llvm-tools-preview --toolchain nightly - cargo install cargo-fuzz + +# AFL++ lane (additional) +# macOS: +brew install afl-fuzz + +# Linux — build from source (apt packages are several major versions behind): +git clone https://github.com/AFLplusplus/AFLplusplus +cd AFLplusplus && make distrib && sudo make install +cd .. + +# Rust wrapper (all platforms): +cargo install cargo-afl ``` --- @@ -47,10 +87,10 @@ proof generation. The `just` recipes handle this automatically. ```bash # From lez-fuzzing/ -# Run all targets for 30 s each +# Run all targets for 30 s each (libFuzzer) just fuzz -# Run a specific target for 120 s +# Run a specific target for 120 s (libFuzzer) RISC0_DEV_MODE=1 cargo fuzz run fuzz_state_transition -- -max_total_time=120 # Run the saved corpus (regression mode, no mutations) @@ -95,12 +135,11 @@ This single command does four things automatically: |---|---| | Creates the corpus directory | `fuzz/corpus/fuzz_my_feature/` | | Writes a typed fuzz target template | `fuzz/fuzz_targets/fuzz_my_feature.rs` | -| Appends `[[bin]]` entry | `fuzz/Cargo.toml` | +| Appends `[[bin]]` entry to `fuzz/Cargo.toml` | Covers **both** the libFuzzer and AFL++ lanes | | Inserts target into every CI matrix + perf loop | `.github/workflows/fuzz.yml` | -The generated template uses `ArbNSSATransaction` from `fuzz_props::arbitrary_types` -so libfuzzer drives every field of `NSSATransaction` independently — no manual -`Unstructured` wiring required. +The generated template uses `fuzz_props::fuzz_entry!` and works with both engines +without modification. ### Step 2 — Implement the target @@ -110,13 +149,15 @@ function under test and any invariant assertions. Use the typed wrappers from structured input, or the proptest generators from [`fuzz_props::generators`](../fuzz_props/src/generators.rs) for richer strategies. -### Step 3 — Register the binary (automated) +### Step 3 — Automated registration (cargo-fuzz + CI) `just new-target` calls [`scripts/add_fuzz_target.py`](../scripts/add_fuzz_target.py) -which appends the `[[bin]]` entry to [`fuzz/Cargo.toml`](../fuzz/Cargo.toml) -automatically. Once present, `cargo fuzz list` (and therefore `just fuzz`, -`just fuzz-regression`, `just corpus-cmin`) pick up the target automatically — no -further Justfile edits required. +which: +- Appends the `[[bin]]` entry to [`fuzz/Cargo.toml`](../fuzz/Cargo.toml). + This **single entry** covers both the libFuzzer lane (`cargo fuzz build`) and + the AFL++ lane (`cargo afl build --no-default-features --features fuzzer-afl`). +- Inserts the target name into every strategy matrix and the perf-baseline shell + loop in [`.github/workflows/fuzz.yml`](../.github/workflows/fuzz.yml). > **Manual fallback:** if you create a target without `just new-target`, add the > entry yourself: @@ -129,21 +170,19 @@ further Justfile edits required. > bench = false > ``` -### Step 4 — Add to CI matrix (automated) - -`just new-target` also inserts `fuzz_my_feature` into every strategy matrix and the -perf-baseline shell loop in [`.github/workflows/fuzz.yml`](../.github/workflows/fuzz.yml) -automatically via `scripts/add_fuzz_target.py`. - -> **Manual fallback:** if you created the target without `just new-target`, add -> `- fuzz_my_feature` to the `target:` list in the three places shown in -> `.github/workflows/fuzz.yml` (smoke-fuzz, regression, perf-baseline). - -### Step 5 — Verify +### Step 4 — Verify ```bash +# Verify the libFuzzer build RISC0_DEV_MODE=1 cargo fuzz build fuzz_my_feature just fuzz-regression # runs the new target against its (empty) corpus + +# Verify the AFL++ build (same fuzz/Cargo.toml — no separate manifest needed) +cd fuzz && cargo afl build \ + --no-default-features \ + --features fuzzer-afl \ + --release \ + --bin fuzz_my_feature ``` ### Quick reference: what to touch @@ -152,12 +191,170 @@ just fuzz-regression # runs the new target against its (empty) corpus |---|---|---| | `fuzz/fuzz_targets/fuzz_.rs` | Create | ✅ `just new-target` | | `fuzz/corpus/fuzz_/` | Create | ✅ `just new-target` | -| `fuzz/Cargo.toml` | Add `[[bin]]` | ✅ `just new-target` | +| `fuzz/Cargo.toml` | Add `[[bin]]` (covers both lanes) | ✅ `just new-target` | | `Justfile` | Nothing — auto-discovers | ✅ automatic | | `.github/workflows/fuzz.yml` | Add to 3 matrix lists | ✅ `just new-target` | --- +## AFL++ Parallel Fuzzing Lane + +### Prerequisites + +Install AFL++ natively on your machine. + +> **Note on Linux package versions**: The `afl++` package in Debian stable (Bookworm) +> and Ubuntu LTS is several major versions behind the current AFL++ 4.x series and may +> be incompatible with `cargo-afl`. **Build from source** for a current version. + +```bash +# macOS — Homebrew keeps the formula up to date +brew install afl-fuzz + +# Linux — build from source (~5 min) +git clone https://github.com/AFLplusplus/AFLplusplus +cd AFLplusplus +make distrib # builds all components: afl-fuzz, afl-cc, afl-clang-fast, … +sudo make install +cd .. + +# Rust build wrapper (all platforms) +cargo install cargo-afl +``` + +> **macOS: crash reporter must be disabled** — AFL++ detects the macOS `ReportCrash` +> daemon and aborts if it is active (it delays crash notifications and causes AFL++ to +> mis-classify crashes as timeouts). The `just fuzz-afl` and `just fuzz-afl-parallel` +> recipes disable it automatically for the duration of the run and re-enable it on exit +> (via a shell `trap`). You can also manage it manually: +> +> ```bash +> # Disable (run once before a long session) +> just afl-macos-setup +> +> # Re-enable afterward +> just afl-macos-teardown +> ``` +> +> Or use the raw `launchctl` commands shown in the AFL++ error message: +> +> ```bash +> SL=/System/Library; PL=com.apple.ReportCrash +> launchctl unload -w ${SL}/LaunchAgents/${PL}.plist +> sudo launchctl unload -w ${SL}/LaunchDaemons/${PL}.Root.plist +> ``` + +### Build + +```bash +# All targets +just afl-build + +# Single target +just afl-build-target fuzz_state_transition +``` + +Both commands compile `fuzz/` with `--no-default-features --features fuzzer-afl`. +Output binaries land in `fuzz/target/release/`. + +### Run (single instance) + +```bash +# 120-second smoke run +just fuzz-afl fuzz_state_transition + +# Custom duration +just fuzz-afl fuzz_state_transition 600 +``` + +### Run (parallel) + +```bash +# 1 main + 3 secondary instances for 5 minutes +just fuzz-afl-parallel fuzz_state_transition 4 300 + +# AFL++ rule: always start the main instance first; +# secondary instances are started with -S flags automatically. +``` + +### Monitor + +```bash +just afl-status fuzz_state_transition +# … calls afl-whatsup afl-output/fuzz_state_transition +``` + +### Triage + +```bash +# Minimise a crash artifact to the smallest reproducing input +just afl-tmin fuzz_state_transition afl-output/fuzz_state_transition/default/crashes/id:000000,... + +# Pretty-print as Rust byte literal (for pasting into a unit test) +just afl-fmt afl-output/fuzz_state_transition/default/crashes/id:000000,... +``` + +### Sync queue to shared corpus + +```bash +# Copies afl-output/*/queue/id:* into fuzz/corpus// +# Run this after any AFL++ session to share findings with cargo-fuzz +just afl-corpus-sync +``` + +### How the shared harness works + +| Mechanism | libFuzzer | AFL++ | +|---|---|---| +| **Entry macro** | `::libfuzzer_sys::fuzz_target!(…)` | `::afl::fuzz!(…)` inside `fn main()` | +| **`no_main` suppression** | `#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]` | Not applied (AFL++ needs a real `main`) | +| **Feature gate** | `cfg(feature = "fuzzer-libfuzzer")` | `cfg(feature = "fuzzer-afl")` | +| **Feature resolution** | Resolved at `fuzz/` (calling crate), not at `fuzz_props/` | Same | +| **`libfuzzer-sys` dep** | Optional, active under `fuzzer-libfuzzer` | Not compiled — avoids `main()` conflict | +| **`afl` dep** | Not compiled | Optional, active under `fuzzer-afl` | +| **Default build** | `default = ["fuzzer-libfuzzer"]` → `cargo fuzz` just works | Requires `--no-default-features --features fuzzer-afl` | + +The `fuzz_props::fuzz_entry!` macro defined in [`fuzz_props/src/lib.rs`](../fuzz_props/src/lib.rs) +expands to the right entry point based on the active feature: + +```rust +#[macro_export] +macro_rules! fuzz_entry { + (|$data:ident: &[u8]| $body:block) => { + #[cfg(feature = "fuzzer-libfuzzer")] + ::libfuzzer_sys::fuzz_target!(|$data: &[u8]| $body); + + #[cfg(feature = "fuzzer-afl")] + fn main() { + ::afl::fuzz!(|$data: &[u8]| $body); + } + }; +} +``` + +### CI (`.github/workflows/fuzz-afl.yml`) + +The nightly AFL++ CI workflow has two jobs: + +| Job | Triggers | Matrix | +|-----|----------|--------| +| `afl-smoke` | nightly + `workflow_dispatch` | 7 priority targets, 120 s each | +| `afl-coverage` | nightly, `needs: afl-smoke` | 3 key targets; LLVM HTML report | + +The smoke job: +1. Builds the target with `cargo afl build --no-default-features --features fuzzer-afl` +2. Runs `afl-fuzz` for 120 s in `aflplusplus/aflplusplus:v4.40c` container +3. Syncs new queue entries into `fuzz/corpus//` and opens a corpus PR +4. Uploads crashes/hangs as a workflow artifact + +The coverage job: +1. Downloads the smoke findings +2. Rebuilds with `RUSTFLAGS="-C instrument-coverage"` +3. Runs all corpus + queue inputs through the binary +4. Merges `.profraw` → `.profdata` → HTML report via `llvm-cov show` + +--- + ## Updating the LEZ Dependency `lez-fuzzing` reads LEZ source directly from `../logos-execution-zone`. To pick up LEZ @@ -186,18 +383,27 @@ just update-lez When `cargo fuzz` finds a crash it writes an artifact to `fuzz/artifacts/fuzz_/crash-`. -### Minimise +### Minimise (libFuzzer) ```bash # Produces a smaller input that still triggers the same crash just fuzz-tmin fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-abc123 ``` +### Minimise (AFL++) + +```bash +just afl-tmin fuzz_state_transition afl-output/fuzz_state_transition/default/crashes/id:000000,... +``` + ### Convert to a regression test ```bash -# Print the bytes as a Rust byte-literal (paste into a #[test]) +# libFuzzer: print bytes as a Rust byte-literal cargo fuzz fmt fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-abc123 + +# AFL++: print bytes as a Rust byte-literal +just afl-fmt afl-output/fuzz_state_transition/default/crashes/id:000000,... ``` Add the minimised file to the corpus so CI always reproduces it: @@ -211,6 +417,42 @@ Open a PR. The `regression` CI job will permanently block re-introduction of thi --- +## Coverage Reports + +### Step 1 — libFuzzer coverage (via `cargo fuzz coverage`) + +```bash +# Generates coverage for a single target +cargo fuzz coverage fuzz_state_transition + +# Generates coverage for all targets +just coverage-all +``` + +Reports land in `fuzz/coverage//`. + +### Step 2 — AFL++ LLVM coverage + +Run after a successful AFL++ session (queue data in `afl-output//`): + +```bash +# Combines libFuzzer + AFL++ corpus into a single LLVM HTML report +just coverage fuzz_state_transition +``` + +This: +1. Runs `cargo fuzz coverage` (step 1) +2. Detects `afl-output/fuzz_state_transition/` and builds the target with + `RUSTFLAGS="-C instrument-coverage" cargo build --manifest-path fuzz/Cargo.toml --no-default-features --features fuzzer-afl --release` +3. Runs all AFL++ queue entries through the binary, collects `.profraw` files +4. Merges profiles with `llvm-profdata merge` and generates an HTML report with `llvm-cov show` +5. Writes the report to `coverage/afl/fuzz_state_transition/html/index.html` + +The AFL++ CI coverage job (`afl-coverage` in [`.github/workflows/fuzz-afl.yml`](../.github/workflows/fuzz-afl.yml)) +automates steps 2–5 and uploads the report as a workflow artifact. + +--- + ## Invariant Framework Shared invariants live in `fuzz_props/src/invariants.rs`. Each invariant implements @@ -242,6 +484,8 @@ Concrete invariants currently registered in `assert_invariants()`: > whose signer-account list is private to the `nssa` crate. The caller must derive signer > IDs from the transaction's witness set before consuming the diff, then call the standalone > `assert_nonce_increment_correctness(signer_ids, nonces_before, state_after)` helper. +> The `signer_account_ids()` helper in `fuzz_props::generators` extracts signer `AccountId`s +> from an `NSSATransaction`'s witness set. Additional invariants enforced **inline** in specific targets (not via `ProtocolInvariant`): @@ -289,6 +533,7 @@ fuzz target parameters for zero-boilerplate structured fuzzing. | `arb_fuzz_native_transfer()` | Correctly-signed native-transfer `NSSATransaction` referencing accounts from an `arbitrary_fuzz_state()` result; gives the fuzzer a path to successful state transitions | | `arbitrary_transaction()` | Structured `NSSATransaction` (`Public` or `ProgramDeployment`) from unstructured bytes via `ArbNSSATransaction` | | `arb_borsh_transaction_bytes()` | Raw Borsh bytes including invalid encodings | +| `signer_account_ids()` | Extracts `AccountId`s of all signers from an `NSSATransaction`'s witness set; used to derive signer IDs before `apply_state_diff` consumes the diff | | `arb_native_transfer_tx()` | Valid native-transfer `NSSATransaction` between known testnet genesis accounts (proptest strategy) | | `test_accounts()` | Returns `(AccountId, PrivateKey)` pairs from `testnet_initial_state` | | `arb_hashable_block_data()` | `HashableBlockData` with 0–8 valid native transfers (proptest strategy) | @@ -326,9 +571,12 @@ Measured on a 4-core x86_64 Linux runner with `RISC0_DEV_MODE=1`: Recommended local settings for longer runs: ```bash -# Use all available cores +# libFuzzer — use all available cores RISC0_DEV_MODE=1 cargo fuzz run fuzz_state_transition \ -- -max_total_time=3600 -jobs=$(nproc) -workers=$(nproc) + +# AFL++ — parallel (1 main + N-1 secondary) +just fuzz-afl-parallel fuzz_state_transition $(nproc) 3600 ``` --- @@ -354,6 +602,6 @@ flag stubs out ZK proof generation and replaces it with a fast mock implementati |------|-------| | `PrivacyPreservingTransaction` coverage | Excluded from `fuzz_encoding_roundtrip` because its ZK receipt cannot be reconstructed in a fuzzing loop. A dedicated slow target with `RISC0_DEV_MODE=1` and `proptest` should be added after the current targets are stable | | `fuzz_validate_execute_consistency` new-account detection | If `execute_check_on_state` creates a brand-new account absent from both the genesis set and the diff, that state-widening will not be detected — full detection requires iterating all accounts in `V03State`, which the API does not currently expose | -| AFL++ integration | A `just fuzz-afl` recipe can be added later; the same corpus is compatible | | Differential testing (sequencer vs replayer) | ✅ Implemented — `fuzz_sequencer_vs_replayer` feeds the same block through the sequencer path (`validate_on_state` → `apply_state_diff`) and the replayer path (`execute_check_on_state`) and asserts identical state for all known accounts | +| AFL++ integration | ✅ Implemented — `just afl-build`, `just fuzz-afl`, `just fuzz-afl-parallel`; nightly CI in `.github/workflows/fuzz-afl.yml`; single `fuzz/Cargo.toml` covers both engines via feature flags | | LEZ version tracking | There is no submodule pin — `lez-fuzzing` reads `../logos-execution-zone` as checked out. Update that repo to a release tag or a tested commit, then run `just update-lez` (which does `git pull --ff-only`) and open a PR to bump it | diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 7173479..b371412 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -37,6 +37,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "afl" +version = "0.15.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927cd71710d1a232519e2393470e8f74a178ae59367efe58fa122884bba35ca4" +dependencies = [ + "home", + "libc", + "rustc_version", + "xdg", +] + [[package]] name = "ahash" version = "0.8.12" @@ -1994,8 +2006,10 @@ dependencies = [ name = "fuzz" version = "0.1.0" dependencies = [ + "afl", "arbitrary", "borsh", + "cc", "common", "fuzz_props", "libfuzzer-sys", @@ -2300,6 +2314,15 @@ version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "019ece39bbefc17f13f677a690328cb978dbf6790e141a3c24e66372cb38588b" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "hostname" version = "0.3.1" @@ -7398,6 +7421,12 @@ dependencies = [ "time", ] +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + [[package]] name = "xml-rs" version = "0.8.28" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 726b805..1aeaff3 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -3,6 +3,8 @@ name = "fuzz" version = "0.1.0" edition = "2024" publish = false +# Provides sys_alloc_aligned stub for non-RISC-V host builds (see build.rs) +build = "build.rs" [package.metadata] cargo-fuzz = true @@ -34,8 +36,14 @@ path = "fuzz_targets/fuzz_block_verification.rs" test = false bench = false +[features] +default = ["fuzzer-libfuzzer"] +fuzzer-libfuzzer = ["libfuzzer-sys", "fuzz_props/fuzzer-libfuzzer"] +fuzzer-afl = ["afl", "fuzz_props/fuzzer-afl"] + [dependencies] -libfuzzer-sys = "0.4" +libfuzzer-sys = { version = "0.4", optional = true } +afl = { version = "0.15", optional = true } arbitrary = { version = "1", features = ["derive"] } borsh = "1" nssa = { path = "../../logos-execution-zone/nssa" } @@ -44,6 +52,10 @@ common = { path = "../../logos-execution-zone/common" } fuzz_props = { path = "../fuzz_props" } testnet_initial_state = { path = "../../logos-execution-zone/testnet_initial_state" } +[build-dependencies] +# Used by build.rs to compile the sys_alloc_aligned stub for non-RISC-V hosts +cc = "1" + [profile.release] debug = true opt-level = 3 diff --git a/fuzz/build.rs b/fuzz/build.rs new file mode 100644 index 0000000..c12f2bd --- /dev/null +++ b/fuzz/build.rs @@ -0,0 +1,25 @@ +// fuzz/build.rs +// +// Provides `sys_alloc_aligned` for non-RISC-V host targets. +// +// `risc0_zkvm_platform::syscall::sys_alloc_words` calls the bare-metal symbol +// `sys_alloc_aligned`, which is normally supplied by the RISC-V zkVM runtime. +// When compiling fuzz targets for a host target (x86_64-unknown-linux-gnu, +// aarch64-unknown-linux-gnu, …) that symbol is absent, causing a linker error. +// This build script compiles a small C stub via the `cc` crate so the symbol +// is always available in the final fuzz binary. +// +// On macOS host builds (used by `cargo fuzz` / libFuzzer) the `cc` crate +// compiles the same stub; it is harmlessly dead-stripped if the symbol is not +// referenced. + +fn main() { + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); + + // RISC-V builds get the real symbol from the zkVM runtime — skip the stub. + if target_arch != "riscv32" && target_arch != "riscv64" { + cc::Build::new() + .file("build_stubs/sys_alloc_aligned.c") + .compile("sys_alloc_stub"); + } +} diff --git a/fuzz/build_stubs/sys_alloc_aligned.c b/fuzz/build_stubs/sys_alloc_aligned.c new file mode 100644 index 0000000..995f6ad --- /dev/null +++ b/fuzz/build_stubs/sys_alloc_aligned.c @@ -0,0 +1,26 @@ +/* + * sys_alloc_aligned.c + * + * Provides `sys_alloc_aligned` for non-RISC-V host targets + * (e.g. aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu). + * + * On RISC-V the real symbol is supplied by the zkVM bare-metal runtime. + * On host targets, risc0_zkvm_platform may still reference this symbol via + * its `sys_alloc_words` helper; this stub satisfies that reference using + * POSIX `posix_memalign`. + */ +#ifndef __riscv + +#include +#include + +void *sys_alloc_aligned(size_t bytes, size_t align) { + void *ptr = NULL; + /* posix_memalign requires alignment >= sizeof(void*) and a power of 2. */ + size_t real_align = align < sizeof(void *) ? sizeof(void *) : align; + if (posix_memalign(&ptr, real_align, bytes) != 0) + return NULL; + return ptr; +} + +#endif /* !__riscv */ diff --git a/fuzz/fuzz_targets/_template.rs b/fuzz/fuzz_targets/_template.rs index e4a4bcc..38e72d5 100644 --- a/fuzz/fuzz_targets/_template.rs +++ b/fuzz/fuzz_targets/_template.rs @@ -1,23 +1,9 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] +// use fuzz_props::arbitrary_types::*; +// use fuzz_props::generators::*; +// use fuzz_props::invariants::*; -use fuzz_props::arbitrary_types::ArbNSSATransaction; -use libfuzzer_sys::fuzz_target; - -fuzz_target!(|wrapped: ArbNSSATransaction| { - let tx = wrapped.0; - - // ── Stateless gate ──────────────────────────────────────────────────────── - // Remove this block to fuzz malformed / unsigned transactions too. - let Ok(tx) = tx.transaction_stateless_check() else { - return; - }; - - // ── Call the function under test ────────────────────────────────────────── - // Example: - // let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0); - // let result = tx.execute_check_on_state(&mut state, block_id, timestamp); - - // ── Assert invariants ───────────────────────────────────────────────────── - // Use fuzz_props::invariants::assert_invariants(&ctx) or inline assertions. - let _ = tx; // replace once the target body is implemented +fuzz_props::fuzz_entry!(|data: &[u8]| { + // TODO: implement harness body + let _ = data; }); diff --git a/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs b/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs index 6db12ec..7cd5fe8 100644 --- a/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs +++ b/fuzz/fuzz_targets/fuzz_apply_state_diff_split_path.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: `validate_on_state` → `apply_state_diff` split path vs //! `execute_check_on_state` direct path. //! @@ -33,14 +33,12 @@ use std::collections::HashSet; use arbitrary::{Arbitrary, Unstructured}; -use common::transaction::NSSATransaction; use fuzz_props::arbitrary_types::ArbNSSATransaction; -use fuzz_props::generators::arbitrary_fuzz_state; +use fuzz_props::generators::{arbitrary_fuzz_state, signer_account_ids}; use fuzz_props::invariants::{NonceSnapshot, assert_nonce_increment_correctness}; -use libfuzzer_sys::fuzz_target; use nssa::V03State; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // Generate a fuzz-driven initial state. @@ -75,23 +73,7 @@ fuzz_target!(|data: &[u8]| { }; // ── Extract signer IDs and capture nonce snapshot before apply ──────────── - // Signer IDs are private to ValidatedStateDiff; derive them from the transaction's - // witness set before the diff is consumed by apply_state_diff. - let signer_ids: Vec = match &tx { - NSSATransaction::Public(pub_tx) => pub_tx - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::PrivacyPreserving(pp_tx) => pp_tx - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::ProgramDeployment(_) => vec![], - }; + let signer_ids = signer_account_ids(&tx); let nonces_before = NonceSnapshot( signer_ids .iter() diff --git a/fuzz/fuzz_targets/fuzz_block_verification.rs b/fuzz/fuzz_targets/fuzz_block_verification.rs index 2d8847b..0973bea 100644 --- a/fuzz/fuzz_targets/fuzz_block_verification.rs +++ b/fuzz/fuzz_targets/fuzz_block_verification.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: block hash integrity — three invariants unique to block-level validation. //! //! 1. **Hash integrity via `From` round-trip** — `HashableBlockData::from(block)` @@ -21,12 +21,11 @@ use arbitrary::{Arbitrary, Unstructured}; use common::block::HashableBlockData; use fuzz_props::arbitrary_types::ArbHashableBlockData; -use libfuzzer_sys::fuzz_target; use nssa::PrivateKey; const DUMMY_KEY_BYTES: [u8; 32] = [1u8; 32]; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); let Ok(wrap) = ArbHashableBlockData::arbitrary(&mut u) else { return; diff --git a/fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs b/fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs index cc9268f..936b0be 100644 --- a/fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs +++ b/fuzz/fuzz_targets/fuzz_encoding_roundtrip.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: encoding round-trip for all transaction types. //! //! Invariants exercised: @@ -18,10 +18,9 @@ use arbitrary::{Arbitrary, Unstructured}; use fuzz_props::arbitrary_types::{ArbProgramDeploymentTransaction, ArbPublicTransaction}; -use libfuzzer_sys::fuzz_target; use nssa::{ProgramDeploymentTransaction, PublicTransaction}; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // ── Test 1: PublicTransaction round-trip ────────────────────────────────── diff --git a/fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs b/fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs index 299bab3..bb084de 100644 --- a/fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs +++ b/fuzz/fuzz_targets/fuzz_multi_block_state_sequence.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: multi-block transaction sequence with long-range invariants. //! //! Verifies properties that span an entire *sequence* of blocks: @@ -35,16 +35,14 @@ //! the total; only mint/burn bugs or token-inflation bugs would break it. use arbitrary::{Arbitrary, Unstructured}; -use common::transaction::NSSATransaction; -use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction}; +use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction, signer_account_ids}; use fuzz_props::invariants::{ BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_nonce_increment_correctness, assert_replay_rejection, }; -use libfuzzer_sys::fuzz_target; use nssa::V03State; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // Generate a fuzz-driven initial state. @@ -120,22 +118,8 @@ fuzz_target!(|data: &[u8]| { // First verify every signer's nonce was incremented by exactly one, then // replay in the next block to confirm the nonce is permanently consumed. if let Ok(applied_tx) = result { - let signer_ids: Vec = match &applied_tx { - NSSATransaction::Public(t) => t - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::PrivacyPreserving(t) => t - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::ProgramDeployment(_) => vec![], - }; - assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state); + let ids = signer_account_ids(&applied_tx); + assert_nonce_increment_correctness(&ids, &nonces_before, &state); assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1); } } diff --git a/fuzz/fuzz_targets/fuzz_program_deployment_lifecycle.rs b/fuzz/fuzz_targets/fuzz_program_deployment_lifecycle.rs index 8631f5c..03040ac 100644 --- a/fuzz/fuzz_targets/fuzz_program_deployment_lifecycle.rs +++ b/fuzz/fuzz_targets/fuzz_program_deployment_lifecycle.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: `V03State::transition_from_program_deployment_transaction`. //! //! The deployment path runs `ValidatedStateDiff::from_program_deployment_transaction` @@ -24,10 +24,9 @@ use arbitrary::{Arbitrary, Unstructured}; use fuzz_props::arbitrary_types::ArbProgramDeploymentTransaction; use fuzz_props::generators::arbitrary_fuzz_state; -use libfuzzer_sys::fuzz_target; use nssa::V03State; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // Generate a fuzz-driven initial state. diff --git a/fuzz/fuzz_targets/fuzz_replay_prevention.rs b/fuzz/fuzz_targets/fuzz_replay_prevention.rs index 1aacfcb..1bc2d63 100644 --- a/fuzz/fuzz_targets/fuzz_replay_prevention.rs +++ b/fuzz/fuzz_targets/fuzz_replay_prevention.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: transaction replay prevention. //! //! Invariant: a transaction that is accepted in block N must be rejected when @@ -23,16 +23,16 @@ //! - **ReplayRejection** — accepted tx rejected on replay use arbitrary::{Arbitrary, Unstructured}; -use common::transaction::NSSATransaction; -use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction}; -use fuzz_props::invariants::{ - BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_nonce_increment_correctness, - assert_replay_rejection, +use fuzz_props::generators::{ + arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction, signer_account_ids, +}; +use fuzz_props::invariants::{ + BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, + assert_nonce_increment_correctness, assert_replay_rejection, }; -use libfuzzer_sys::fuzz_target; use nssa::V03State; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // Generate a fuzz-driven initial state. @@ -96,21 +96,7 @@ fuzz_target!(|data: &[u8]| { // First verify every signer's nonce was incremented by exactly one, then // assert that replaying in the next block is rejected (nonce permanently consumed). if let Ok(applied_tx) = result { - let signer_ids: Vec = match &applied_tx { - NSSATransaction::Public(t) => t - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::PrivacyPreserving(t) => t - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::ProgramDeployment(_) => vec![], - }; + let signer_ids = signer_account_ids(&applied_tx); assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state); assert_replay_rejection(applied_tx, &mut state, 2, 1); } diff --git a/fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs b/fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs index 5cbc4a2..9b46905 100644 --- a/fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs +++ b/fuzz/fuzz_targets/fuzz_sequencer_vs_replayer.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: sequencer vs replayer differential state-root equivalence. //! //! Feeds the same block of transactions through two independent state-transition @@ -40,10 +40,9 @@ use std::collections::HashSet; use arbitrary::{Arbitrary, Unstructured}; use common::transaction::{NSSATransaction, clock_invocation}; use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction}; -use libfuzzer_sys::fuzz_target; use nssa::V03State; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // ── Initial state ───────────────────────────────────────────────────────── diff --git a/fuzz/fuzz_targets/fuzz_signature_verification.rs b/fuzz/fuzz_targets/fuzz_signature_verification.rs index 83c3221..71db2ae 100644 --- a/fuzz/fuzz_targets/fuzz_signature_verification.rs +++ b/fuzz/fuzz_targets/fuzz_signature_verification.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: signature creation and verification. //! //! Invariants exercised: @@ -12,10 +12,9 @@ use arbitrary::{Arbitrary, Unstructured}; use fuzz_props::arbitrary_types::{ArbPrivateKey, ArbPublicKey, ArbSignature}; -use libfuzzer_sys::fuzz_target; use nssa::{PublicKey, Signature}; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // ── 1. Freshly signed message always verifies with the correct key ───────── diff --git a/fuzz/fuzz_targets/fuzz_state_diff_computation.rs b/fuzz/fuzz_targets/fuzz_state_diff_computation.rs index 410afb3..0c912fe 100644 --- a/fuzz/fuzz_targets/fuzz_state_diff_computation.rs +++ b/fuzz/fuzz_targets/fuzz_state_diff_computation.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: state diff isolation — bidirectional. //! //! Invariants: @@ -22,10 +22,9 @@ use arbitrary::{Arbitrary, Unstructured}; use common::transaction::NSSATransaction; use fuzz_props::arbitrary_types::ArbPublicTransaction; use fuzz_props::generators::arbitrary_fuzz_state; -use libfuzzer_sys::fuzz_target; use nssa::{V03State, ValidatedStateDiff}; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // Generate a fuzz-driven initial state. diff --git a/fuzz/fuzz_targets/fuzz_state_serialization.rs b/fuzz/fuzz_targets/fuzz_state_serialization.rs index 535201c..d0cde75 100644 --- a/fuzz/fuzz_targets/fuzz_state_serialization.rs +++ b/fuzz/fuzz_targets/fuzz_state_serialization.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: `V03State` Borsh serialization/deserialization. //! //! The state blob is transmitted between nodes and persisted to disk, so a panic or @@ -22,10 +22,9 @@ //! place for a logic bug — and the fuzzer should be steered towards exercising //! the duplicate-nullifier code path. -use libfuzzer_sys::fuzz_target; use nssa::V03State; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { // ── Invariant 1: NoPanic ────────────────────────────────────────────────── // `borsh::from_slice` must never panic. If it returns `Err`, we simply // return early; only structurally valid blobs proceed to the round-trip check. diff --git a/fuzz/fuzz_targets/fuzz_state_transition.rs b/fuzz/fuzz_targets/fuzz_state_transition.rs index 8dc3de0..4915237 100644 --- a/fuzz/fuzz_targets/fuzz_state_transition.rs +++ b/fuzz/fuzz_targets/fuzz_state_transition.rs @@ -1,16 +1,16 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] use arbitrary::{Arbitrary, Unstructured}; -use common::transaction::NSSATransaction; -use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction}; -use fuzz_props::invariants::{ - BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, assert_nonce_increment_correctness, - assert_replay_rejection, +use fuzz_props::generators::{ + arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction, signer_account_ids, +}; +use fuzz_props::invariants::{ + BalanceSnapshot, InvariantCtx, NonceSnapshot, assert_invariants, + assert_nonce_increment_correctness, assert_replay_rejection, }; -use libfuzzer_sys::fuzz_target; use nssa::V03State; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // Generate a fuzz-driven initial state instead of always using the fixed @@ -95,21 +95,7 @@ fuzz_target!(|data: &[u8]| { // First verify every signer's nonce was incremented by exactly one, then // replay in the next block to confirm the nonce is permanently consumed. if let Ok(applied_tx) = result { - let signer_ids: Vec = match &applied_tx { - NSSATransaction::Public(t) => t - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::PrivacyPreserving(t) => t - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::ProgramDeployment(_) => vec![], - }; + let signer_ids = signer_account_ids(&applied_tx); assert_nonce_increment_correctness(&signer_ids, &nonces_before, &state); assert_replay_rejection(applied_tx, &mut state, block_id + 1, timestamp + 1); } diff --git a/fuzz/fuzz_targets/fuzz_stateless_verification.rs b/fuzz/fuzz_targets/fuzz_stateless_verification.rs index 3cb792e..767edf1 100644 --- a/fuzz/fuzz_targets/fuzz_stateless_verification.rs +++ b/fuzz/fuzz_targets/fuzz_stateless_verification.rs @@ -1,11 +1,10 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] use arbitrary::Unstructured; use common::transaction::NSSATransaction; use fuzz_props::generators::arbitrary_transaction; -use libfuzzer_sys::fuzz_target; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // Path A: try to build a structured transaction from unstructured bytes diff --git a/fuzz/fuzz_targets/fuzz_transaction_decoding.rs b/fuzz/fuzz_targets/fuzz_transaction_decoding.rs index fae71e5..07a4302 100644 --- a/fuzz/fuzz_targets/fuzz_transaction_decoding.rs +++ b/fuzz/fuzz_targets/fuzz_transaction_decoding.rs @@ -1,12 +1,11 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] use common::{ block::{Block, HashableBlockData}, transaction::NSSATransaction, }; -use libfuzzer_sys::fuzz_target; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { // Attempt 1: decode as NSSATransaction and verify roundtrip if let Ok(tx) = borsh::from_slice::(data) { let re_encoded = borsh::to_vec(&tx).expect("re-encode of valid tx must succeed"); diff --git a/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs b/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs index d9f1b72..b8a8b95 100644 --- a/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs +++ b/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: `validate_on_state` and `execute_check_on_state` consistency. //! //! Invariants: @@ -25,14 +25,12 @@ //! reachable by the fuzzer. use arbitrary::{Arbitrary, Unstructured}; -use common::transaction::NSSATransaction; use fuzz_props::arbitrary_types::ArbNSSATransaction; -use fuzz_props::generators::arbitrary_fuzz_state; +use fuzz_props::generators::{arbitrary_fuzz_state, signer_account_ids}; use fuzz_props::invariants::{NonceSnapshot, assert_nonce_increment_correctness}; -use libfuzzer_sys::fuzz_target; use nssa::V03State; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // Generate a fuzz-driven initial state. The state shape — account IDs, @@ -160,21 +158,7 @@ fuzz_target!(|data: &[u8]| { // consistency checks above: it catches bugs where validate_on_state and // execute_check_on_state agree (passing INVARIANT 1) but both increment // the wrong account's nonce, or skip the increment entirely. - let signer_ids: Vec = match &applied_tx { - NSSATransaction::Public(t) => t - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::PrivacyPreserving(t) => t - .witness_set() - .signatures_and_public_keys() - .iter() - .map(|(_, pk)| nssa::AccountId::from(pk)) - .collect(), - NSSATransaction::ProgramDeployment(_) => vec![], - }; + let signer_ids = signer_account_ids(&applied_tx); assert_nonce_increment_correctness(&signer_ids, &nonces_before, &exec_state); } (Err(_), Err(_)) => { diff --git a/fuzz/fuzz_targets/fuzz_witness_set_verification.rs b/fuzz/fuzz_targets/fuzz_witness_set_verification.rs index dc4102f..585cbb1 100644 --- a/fuzz/fuzz_targets/fuzz_witness_set_verification.rs +++ b/fuzz/fuzz_targets/fuzz_witness_set_verification.rs @@ -1,4 +1,4 @@ -#![no_main] +#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)] //! Fuzz target: `WitnessSet` authentication isolation for public transactions. //! //! The most security-critical property of `WitnessSet` is **message isolation**: @@ -23,10 +23,9 @@ use arbitrary::{Arbitrary, Unstructured}; use fuzz_props::arbitrary_types::{ArbPrivateKey, ArbPubTxMessage, ArbWitnessSet}; -use libfuzzer_sys::fuzz_target; use nssa::{PublicKey, public_transaction::WitnessSet}; -fuzz_target!(|data: &[u8]| { +fuzz_props::fuzz_entry!(|data: &[u8]| { let mut u = Unstructured::new(data); // ── Invariant 1: NoPanic on adversarial WitnessSet ──────────────────────── diff --git a/fuzz_props/Cargo.toml b/fuzz_props/Cargo.toml index ad96ec6..a6796bd 100644 --- a/fuzz_props/Cargo.toml +++ b/fuzz_props/Cargo.toml @@ -6,6 +6,10 @@ edition = "2024" [lints] workspace = true +[features] +fuzzer-libfuzzer = [] +fuzzer-afl = [] + [dependencies] nssa = { workspace = true } nssa_core = { workspace = true } diff --git a/fuzz_props/src/generators.rs b/fuzz_props/src/generators.rs index 4d2b165..70f7788 100644 --- a/fuzz_props/src/generators.rs +++ b/fuzz_props/src/generators.rs @@ -6,6 +6,30 @@ use crate::arbitrary_types::{ArbAccountId, ArbNSSATransaction, ArbPrivateKey}; use proptest::prelude::*; use testnet_initial_state::initial_pub_accounts_private_keys; +// ── Signer account ID extraction ───────────────────────────────────────────── + +/// Extract the [`AccountId`]s of all signers from a transaction's +/// witness set. Used by fuzz targets that need to verify nonce +/// increments after `execute_check_on_state`. +pub fn signer_account_ids(tx: &common::transaction::NSSATransaction) -> Vec { + use common::transaction::NSSATransaction; + match tx { + NSSATransaction::Public(pt) => pt + .witness_set() + .signatures_and_public_keys() + .iter() + .map(|(_, pk)| nssa::AccountId::from(pk)) + .collect(), + NSSATransaction::PrivacyPreserving(pt) => pt + .witness_set() + .signatures_and_public_keys() + .iter() + .map(|(_, pk)| nssa::AccountId::from(pk)) + .collect(), + NSSATransaction::ProgramDeployment(_) => vec![], + } +} + // ── Fuzz-driven state generation ───────────────────────────────────────────── /// An account with an arbitrary identifier, balance, and private key, diff --git a/fuzz_props/src/lib.rs b/fuzz_props/src/lib.rs index 6bfa46e..3195541 100644 --- a/fuzz_props/src/lib.rs +++ b/fuzz_props/src/lib.rs @@ -6,6 +6,26 @@ pub mod arbitrary_types; pub mod generators; pub mod invariants; +/// Generates the fuzzer entry point for whichever engine this crate is +/// compiled with, selected via Cargo features: +/// +/// | Feature | Expansion | +/// |----------------------|-----------| +/// | `fuzzer-libfuzzer` | `libfuzzer_sys::fuzz_target!(…)` | +/// | `fuzzer-afl` | `fn main() { afl::fuzz!(…) }` | +#[macro_export] +macro_rules! fuzz_entry { + (|$data:ident: &[u8]| $body:block) => { + #[cfg(feature = "fuzzer-libfuzzer")] + ::libfuzzer_sys::fuzz_target!(|$data: &[u8]| $body); + + #[cfg(feature = "fuzzer-afl")] + fn main() { + ::afl::fuzz!(|$data: &[u8]| $body); + } + }; +} + #[cfg(test)] mod seed_gen { use std::fs; diff --git a/scripts/add_fuzz_target.py b/scripts/add_fuzz_target.py index 52bd5a6..3dde883 100644 --- a/scripts/add_fuzz_target.py +++ b/scripts/add_fuzz_target.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Fully automates registering a new cargo-fuzz target. +"""Fully automates registering a new cargo-fuzz / AFL++ fuzz target. Usage: python3 scripts/add_fuzz_target.py @@ -7,12 +7,19 @@ Usage: Where TARGET_NAME is the full binary name, e.g. fuzz_my_feature. Actions performed: - 1. Appends a [[bin]] entry to fuzz/Cargo.toml + 1. Appends a [[bin]] entry to fuzz/Cargo.toml (one entry covers BOTH + the libFuzzer lane and the AFL++ lane — no separate Cargo.toml needed) 2. Inserts TARGET_NAME into every YAML matrix block in .github/workflows/fuzz.yml (smoke-fuzz, regression) 3. Inserts TARGET_NAME into the perf-baseline shell for-loop in .github/workflows/fuzz.yml +NOTE: A single fuzz/Cargo.toml is the source of truth for both engines. + - libFuzzer build: cargo fuzz build + - AFL++ build: cd fuzz && cargo afl build \\ + --no-default-features --features fuzzer-afl \\ + --release --bin + Run from the repository root. """ @@ -172,6 +179,24 @@ def main() -> None: append_cargo_bin(target, cargo_toml) insert_into_workflow(target, workflow) + # ── Print build instructions ────────────────────────────────────────────── + print() + print("Registration complete! Next steps:") + print() + print(" 1. Implement the harness body in:") + print(f" fuzz/fuzz_targets/{target}.rs") + print() + print(" 2. Verify the libFuzzer (cargo-fuzz) build:") + print(f" RISC0_DEV_MODE=1 cargo fuzz build {target}") + print() + print(" 3. Verify the AFL++ build (single shared fuzz/Cargo.toml):") + print(f" cd fuzz && cargo afl build \\") + print(f" --no-default-features --features fuzzer-afl \\") + print(f" --release --bin {target}") + print() + print(" 4. Run with libFuzzer: just fuzz-one", target) + print(" Run with AFL++: just fuzz-afl", target) + if __name__ == "__main__": main()