# ── Fuzzing ─────────────────────────────────────────────────────────────────── export RISC0_DEV_MODE := "1" # ── Directory layout ────────────────────────────────────────────────────────── # # corpus/ # libfuzz// — inputs generated/discovered by cargo-fuzz (libFuzzer) # afl// — inputs generated by AFL++ (synced from afl-output queue) # # coverage/ # libfuzz// — per-target libFuzzer coverage report + profdata # libfuzz/summary/ — merged libFuzzer summary (all targets) # afl// — per-target AFL++ corpus coverage report + profdata # afl/summary/ — merged AFL++ corpus summary (all targets) # # afl-output// — AFL++'s raw working directory (queue, crashes, hangs) # The queue is synced to corpus/afl// via # `just afl-corpus-sync`; crashes/hangs are kept here. # # Note: cargo-fuzz (coverage, run, cmin) always writes its profdata to the # fixed path fuzz/coverage//coverage.profdata regardless of the # corpus argument. The coverage recipes copy that file into the organised # coverage/ tree immediately after it is produced, so AFL passes can never # overwrite a libFuzzer profdata that is still needed. # List all registered fuzz targets (reads fuzz/Cargo.toml via cargo-fuzz) list-targets: cargo fuzz list # Run all fuzz targets for TIME seconds each (default: 30). # Targets are discovered automatically from fuzz/Cargo.toml — no edit needed here # when a new [[bin]] entry is added. fuzz TIME="30": #!/bin/bash set -euo pipefail for target in $(cargo fuzz list 2>/dev/null); do echo "=== fuzzing $target for {{TIME}}s ===" mkdir -p "corpus/libfuzz/$target" cargo fuzz run "$target" "corpus/libfuzz/$target" -- -max_total_time={{TIME}} done # Re-run the saved corpus for every target (regression mode, no new mutations) fuzz-regression: #!/bin/bash set -euo pipefail for target in $(cargo fuzz list 2>/dev/null); do echo "=== regression $target ===" mkdir -p "corpus/libfuzz/$target" cargo fuzz run "$target" "corpus/libfuzz/$target" -- -runs=0 done # Minimise a crash artifact # Usage: just fuzz-tmin fuzz_state_transition fuzz/artifacts/fuzz_state_transition/crash-XXX fuzz-tmin TARGET ARTIFACT: cargo fuzz tmin {{TARGET}} {{ARTIFACT}} # Run the proptest-based property tests fuzz-props: cargo test -p fuzz_props --release # Pull the latest LEZ changes from the sibling logos-execution-zone directory update-lez: git -C ../logos-execution-zone pull --ff-only # ── Corpus management ───────────────────────────────────────────────────────── # Minimise the corpus for all targets (removes dominated inputs) corpus-cmin: #!/bin/bash set -euo pipefail for target in $(cargo fuzz list 2>/dev/null); do echo "=== cmin $target ===" mkdir -p "corpus/libfuzz/$target" cargo fuzz cmin "$target" "corpus/libfuzz/$target" done # Minimise the corpus for a single target # Usage: just corpus-cmin-target fuzz_state_transition corpus-cmin-target TARGET: mkdir -p corpus/libfuzz/{{TARGET}} cargo fuzz cmin {{TARGET}} corpus/libfuzz/{{TARGET}} # ── Adding a new target ─────────────────────────────────────────────────────── # Scaffold a new fuzz target — fully automated, no manual edits required. # # Steps performed automatically: # 1. Creates corpus/libfuzz// # 2. Copies fuzz/fuzz_targets/_template.rs → fuzz/fuzz_targets/.rs # 3. Appends the [[bin]] entry to fuzz/Cargo.toml # 4. Inserts into every strategy matrix in .github/workflows/fuzz.yml # # Usage: just new-target my_feature # (the "fuzz_" prefix is added automatically) new-target NAME: #!/bin/bash set -euo pipefail TARGET="fuzz_{{NAME}}" TEMPLATE="fuzz/fuzz_targets/_template.rs" RS_FILE="fuzz/fuzz_targets/${TARGET}.rs" CORPUS_DIR="corpus/libfuzz/${TARGET}" # ── 1. Create corpus directory ──────────────────────────────────────────── mkdir -p "$CORPUS_DIR" echo "[1/4] Created corpus directory: $CORPUS_DIR" # ── 2. Copy the typed fuzz target template ──────────────────────────────── if [ -f "$RS_FILE" ]; then echo "SKIP [2/4]: $RS_FILE already exists — not overwriting." else cp "$TEMPLATE" "$RS_FILE" echo "[2/4] Created target from template: $RS_FILE" fi # ── 3 & 4. Update Cargo.toml and fuzz.yml automatically ────────────────── python3 scripts/add_fuzz_target.py "$TARGET" echo "" 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 ────────────────────────────────────────────────────────────── # Remove all Cargo build artefacts (workspace + fuzz sub-crate) clean: cargo clean cargo clean --manifest-path fuzz/Cargo.toml # Remove libFuzzer crash/timeout artifacts for all targets (corpus is kept) clean-artifacts: rm -rf fuzz/artifacts/ # Remove coverage reports generated by `cargo fuzz coverage` and `just coverage` clean-coverage: rm -rf fuzz/coverage/ 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