# ── 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 corpus/afl// when done. # # AFL++ is seeded from corpus/libfuzz// (the libFuzzer corpus). # After the run, new inputs discovered by AFL++ are synced to corpus/afl// # via `just afl-corpus-sync`. # # 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, 30 s each # just fuzz-afl "" 60 # all targets, 60 s each # just fuzz-afl fuzz_state_transition # single target, 30 s # just fuzz-afl fuzz_state_transition 300 # single target, 300 s fuzz-afl TARGET="" TIME="30": #!/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="corpus/libfuzz/$t" # seed from libFuzzer corpus 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 # ── Crash / hang summary ────────────────────────────────────────────────── echo "" echo "=== AFL++ crash / hang summary ===" total_crashes=0 total_hangs=0 for target_dir in afl-output/*/; do [ -d "$target_dir" ] || continue for instance_dir in "$target_dir"*/; do [ -d "$instance_dir" ] || continue crashes_dir="${instance_dir}crashes" hangs_dir="${instance_dir}hangs" n_crashes=0 n_hangs=0 if [ -d "$crashes_dir" ]; then n_crashes=$(find "$crashes_dir" -maxdepth 1 -type f | wc -l | tr -d ' ') fi if [ -d "$hangs_dir" ]; then n_hangs=$(find "$hangs_dir" -maxdepth 1 -type f | wc -l | tr -d ' ') fi if [ "$n_crashes" -gt 0 ] || [ "$n_hangs" -gt 0 ]; then echo " !! $(basename "$target_dir")/$(basename "$instance_dir") crashes=$n_crashes hangs=$n_hangs" for f in "$crashes_dir"/id:*; do [ -f "$f" ] && echo " $f" done for f in "$hangs_dir"/id:*; do [ -f "$f" ] && echo " $f" done fi total_crashes=$((total_crashes + n_crashes)) total_hangs=$((total_hangs + n_hangs)) done done echo "" if [ "$total_crashes" -eq 0 ] && [ "$total_hangs" -eq 0 ]; then echo " ✓ No crashes or hangs found across all targets." else echo " TOTAL crashes=$total_crashes hangs=$total_hangs" echo "" echo " Minimise a crash : just afl-tmin " echo " Format for a report: just afl-fmt " fi # 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="corpus/libfuzz/{{TARGET}}" # seed from libFuzzer corpus 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 # AFL corpus directory (corpus/afl//). Run after any AFL++ session # to make new interesting inputs available for coverage measurement and future runs. 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="corpus/afl/${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 ────────────────────────────────────────────────────────────────── # # cargo-fuzz always writes its profdata to the fixed path: # fuzz/coverage//coverage.profdata # Each coverage recipe immediately copies that file into the organised tree # (coverage/libfuzz/ or coverage/afl/) so that a subsequent run of the other # engine cannot overwrite the data we need for the summary reports. # Generate a libFuzzer-only coverage report for a single target. # Runs `cargo fuzz coverage` against corpus/libfuzz//, then copies the # profdata into coverage/libfuzz// and renders an HTML report there. # Output: coverage/libfuzz/{{TARGET}}/html/index.html # Usage: just coverage-libfuzz fuzz_state_transition coverage-libfuzz TARGET: #!/bin/bash set -euo pipefail # ── Resolve LLVM tools from the active Rust toolchain ───────────────────── _SYSROOT=$(rustc --print sysroot) _HOST=$(rustc -vV | sed -n 's/^host: //p') _LLVM_BIN="${_SYSROOT}/lib/rustlib/${_HOST}/bin" LLVM_COV="${_LLVM_BIN}/llvm-cov" command -v "$LLVM_COV" &>/dev/null || LLVM_COV=$(command -v llvm-cov 2>/dev/null || true) if [ -z "$LLVM_COV" ] || [ ! -x "$LLVM_COV" ]; then echo "ERROR: llvm-cov not found in Rust sysroot (${_LLVM_BIN}) or PATH." echo " Run: rustup component add llvm-tools-preview" exit 1 fi CORPUS="corpus/libfuzz/{{TARGET}}" mkdir -p "$CORPUS" echo "=== cargo fuzz coverage {{TARGET}} (libFuzzer corpus) ===" cargo fuzz coverage {{TARGET}} "$CORPUS" # ── Copy profdata to the organised tree ─────────────────────────────────── # cargo-fuzz always writes here; we copy immediately so a later AFL pass # cannot clobber this file before the summary reads it. CARGO_PROFDATA="fuzz/coverage/{{TARGET}}/coverage.profdata" LF_COV_DIR="coverage/libfuzz/{{TARGET}}" mkdir -p "$LF_COV_DIR" if [ ! -f "$CARGO_PROFDATA" ]; then echo "WARNING: profdata not produced — skipping HTML generation." exit 0 fi cp "$CARGO_PROFDATA" "${LF_COV_DIR}/coverage.profdata" PROFDATA="${LF_COV_DIR}/coverage.profdata" # ── Render HTML ─────────────────────────────────────────────────────────── BINARY="target/${_HOST}/coverage/${_HOST}/release/{{TARGET}}" HTML_DIR="${LF_COV_DIR}/html" if [ -f "$BINARY" ]; then mkdir -p "$HTML_DIR" "$LLVM_COV" show \ "$BINARY" \ --instr-profile="$PROFDATA" \ --format=html \ --output-dir="$HTML_DIR" \ --ignore-filename-regex='\.cargo|rustc' echo "libFuzzer HTML coverage report: ${HTML_DIR}/index.html" else echo "WARNING: binary not found — skipping HTML generation." echo " Binary: $BINARY" fi # Measure code coverage exercised by the AFL++ corpus for a single target. # # Strategy: replay corpus/afl// through the libFuzzer coverage binary # (built by `cargo fuzz coverage`). Run `just afl-corpus-sync` first to # populate corpus/afl// from the AFL++ queue. # # Output: coverage/afl/{{TARGET}}/html/index.html # Usage: just coverage-afl fuzz_state_transition coverage-afl TARGET: #!/bin/bash set -euo pipefail # ── Resolve LLVM tools from the active Rust toolchain ───────────────────── _SYSROOT=$(rustc --print sysroot) _HOST=$(rustc -vV | sed -n 's/^host: //p') _LLVM_BIN="${_SYSROOT}/lib/rustlib/${_HOST}/bin" LLVM_COV="${_LLVM_BIN}/llvm-cov" command -v "$LLVM_COV" &>/dev/null || LLVM_COV=$(command -v llvm-cov 2>/dev/null || true) if [ -z "$LLVM_COV" ] || [ ! -x "$LLVM_COV" ]; then echo "ERROR: llvm-cov not found in Rust sysroot (${_LLVM_BIN}) or PATH." echo " Run: rustup component add llvm-tools-preview" exit 1 fi AFL_CORPUS="corpus/afl/{{TARGET}}" if [ ! -d "$AFL_CORPUS" ] || [ -z "$(ls -A "$AFL_CORPUS" 2>/dev/null)" ]; then echo "No AFL++ corpus for {{TARGET}} at $AFL_CORPUS." echo " Run 'just afl-corpus-sync' after an AFL++ session to populate it." exit 0 fi echo "=== AFL++ corpus coverage for {{TARGET}} ===" # ── Replay AFL++ corpus through the libFuzzer coverage binary ───────────── # cargo fuzz coverage always writes: # profdata → fuzz/coverage/{{TARGET}}/coverage.profdata # binary → target//coverage//release/{{TARGET}} cargo fuzz coverage {{TARGET}} "$AFL_CORPUS" # ── Copy profdata to the organised tree ─────────────────────────────────── CARGO_PROFDATA="fuzz/coverage/{{TARGET}}/coverage.profdata" AFL_COV_DIR="coverage/afl/{{TARGET}}" mkdir -p "$AFL_COV_DIR" if [ ! -f "$CARGO_PROFDATA" ]; then echo "WARNING: profdata not produced — skipping HTML generation." exit 0 fi cp "$CARGO_PROFDATA" "${AFL_COV_DIR}/coverage.profdata" # ── Render HTML ─────────────────────────────────────────────────────────── BINARY="target/${_HOST}/coverage/${_HOST}/release/{{TARGET}}" HTML_DIR="${AFL_COV_DIR}/html" mkdir -p "$HTML_DIR" if [ -f "$BINARY" ]; then "$LLVM_COV" show \ "$BINARY" \ --instr-profile="${AFL_COV_DIR}/coverage.profdata" \ --format=html \ --output-dir="$HTML_DIR" \ --ignore-filename-regex='\.cargo|rustc' echo "AFL++ corpus HTML coverage report: ${HTML_DIR}/index.html" else echo "WARNING: binary not found: $BINARY" fi # Generate a combined coverage report for a single target (libFuzzer + AFL++). # Delegates to coverage-libfuzz then coverage-afl. # Usage: just coverage fuzz_state_transition coverage TARGET: just coverage-libfuzz {{TARGET}} || true just coverage-afl {{TARGET}} # Generate coverage for ALL registered fuzz targets. # ENGINE selects which fuzzer engine to measure: # "all" — libFuzzer + AFL++ (default) # "libfuzz" — libFuzzer only (cargo fuzz coverage against corpus/libfuzz/) # "afl" — AFL++ only (cargo fuzz coverage against corpus/afl/) # # After the per-target loop, a merged summary HTML report is written: # libfuzz → coverage/libfuzz/summary/html/index.html # afl → coverage/afl/summary/html/index.html # # Usage: just coverage-all # both engines # just coverage-all libfuzz # libFuzzer only # just coverage-all afl # AFL++ only coverage-all ENGINE="all": #!/bin/bash set -euo pipefail # ── Resolve LLVM tools from the active Rust toolchain ───────────────────── _SYSROOT=$(rustc --print sysroot) _HOST=$(rustc -vV | sed -n 's/^host: //p') _LLVM_BIN="${_SYSROOT}/lib/rustlib/${_HOST}/bin" LLVM_COV="${_LLVM_BIN}/llvm-cov" LLVM_PROFDATA="${_LLVM_BIN}/llvm-profdata" command -v "$LLVM_COV" &>/dev/null || LLVM_COV=$(command -v llvm-cov 2>/dev/null || true) command -v "$LLVM_PROFDATA" &>/dev/null || LLVM_PROFDATA=$(command -v llvm-profdata 2>/dev/null || true) TARGETS=($(cargo fuzz list 2>/dev/null)) # ── Per-target passes ───────────────────────────────────────────────────── # Each coverage-libfuzz / coverage-afl call copies its profdata into the # organised tree (coverage/libfuzz/ or coverage/afl/) immediately after # cargo-fuzz produces it. Because the two engines write to distinct # directories there is no risk of one overwriting the other's data, # regardless of the order in which the targets are processed. if [ "{{ENGINE}}" = "all" ] || [ "{{ENGINE}}" = "libfuzz" ]; then for target in "${TARGETS[@]}"; do echo "=== coverage (libfuzz) $target ===" just coverage-libfuzz "$target" || true done fi if [ "{{ENGINE}}" = "all" ] || [ "{{ENGINE}}" = "afl" ]; then for target in "${TARGETS[@]}"; do echo "=== coverage (afl) $target ===" just coverage-afl "$target" done fi # ── Merged summary report (libfuzz) ─────────────────────────────────────── if [ "{{ENGINE}}" = "libfuzz" ] || [ "{{ENGINE}}" = "all" ]; then echo "" echo "=== libFuzzer summary report (all targets merged) ===" SUMMARY_DIR="coverage/libfuzz/summary" mkdir -p "$SUMMARY_DIR" PROFDATA_FILES=() BINARY_ARGS=() for t in "${TARGETS[@]}"; do PD="coverage/libfuzz/$t/coverage.profdata" BIN="target/${_HOST}/coverage/${_HOST}/release/$t" [ -f "$PD" ] && PROFDATA_FILES+=("$PD") [ -f "$BIN" ] && BINARY_ARGS+=("--object" "$BIN") done if [ ${#PROFDATA_FILES[@]} -eq 0 ]; then echo "No libFuzzer profdata found — skipping summary." else MERGED="${SUMMARY_DIR}/merged.profdata" "$LLVM_PROFDATA" merge -sparse "${PROFDATA_FILES[@]}" -o "$MERGED" HTML_DIR="${SUMMARY_DIR}/html" mkdir -p "$HTML_DIR" # First binary is positional; the rest are --object flags. FIRST_BIN="${BINARY_ARGS[1]}" # index 1 is the path after '--object' REST_ARGS=("${BINARY_ARGS[@]:2}") "$LLVM_COV" show \ "$FIRST_BIN" \ "${REST_ARGS[@]}" \ --instr-profile="$MERGED" \ --format=html \ --output-dir="$HTML_DIR" \ --ignore-filename-regex='\.cargo|rustc' echo "libFuzzer summary HTML report: ${HTML_DIR}/index.html" fi fi # ── Merged summary report (afl) ─────────────────────────────────────────── if [ "{{ENGINE}}" = "afl" ] || [ "{{ENGINE}}" = "all" ]; then echo "" echo "=== AFL++ corpus summary report (all targets merged) ===" SUMMARY_DIR="coverage/afl/summary" mkdir -p "$SUMMARY_DIR" PROFDATA_FILES=() BINARY_ARGS=() for t in "${TARGETS[@]}"; do PD="coverage/afl/$t/coverage.profdata" BIN="target/${_HOST}/coverage/${_HOST}/release/$t" [ -f "$PD" ] && PROFDATA_FILES+=("$PD") [ -f "$BIN" ] && BINARY_ARGS+=("--object" "$BIN") done if [ ${#PROFDATA_FILES[@]} -eq 0 ]; then echo "No AFL++ profdata found — skipping summary." else MERGED="${SUMMARY_DIR}/merged.profdata" "$LLVM_PROFDATA" merge -sparse "${PROFDATA_FILES[@]}" -o "$MERGED" HTML_DIR="${SUMMARY_DIR}/html" mkdir -p "$HTML_DIR" FIRST_BIN="${BINARY_ARGS[1]}" REST_ARGS=("${BINARY_ARGS[@]:2}") "$LLVM_COV" show \ "$FIRST_BIN" \ "${REST_ARGS[@]}" \ --instr-profile="$MERGED" \ --format=html \ --output-dir="$HTML_DIR" \ --ignore-filename-regex='\.cargo|rustc' echo "AFL++ corpus summary HTML report: ${HTML_DIR}/index.html" fi fi # ── 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 all coverage reports. # Also removes the temporary profdata that cargo-fuzz writes to fuzz/coverage/ # and any stray .profraw files left by the instrumented binaries. clean-coverage: rm -rf coverage/ fuzz/coverage/ find . -name '*.profraw' -delete # Remove AFL++ output directories (crashes, hangs, queue). # Note: the queue is also stored in corpus/afl/ via `just afl-corpus-sync`. clean-afl: rm -rf afl-output/ # Remove the corpus directories (libFuzzer and AFL). clean-corpus: rm -rf corpus/ # Remove everything: builds, artifacts, coverage, and AFL++ output (preserves corpus/) clean-all: clean clean-artifacts clean-coverage clean-afl