From 167bb23e67913ac03f786a7df8b7214b1d582f20 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 | 319 ++++++---------------- Justfile | 387 ++++++--------------------- docs/fuzzing.md | 23 -- fuzz/Cargo.lock | 1 + fuzz/Cargo.toml | 6 + fuzz/build.rs | 25 ++ fuzz/build_stubs/sys_alloc_aligned.c | 26 ++ fuzz_props/src/generators.rs | 1 - fuzz_props/src/lib.rs | 12 + 9 files changed, 224 insertions(+), 576 deletions(-) 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 index 542af4b..069dfe8 100644 --- a/.github/workflows/fuzz-afl.yml +++ b/.github/workflows/fuzz-afl.yml @@ -2,244 +2,118 @@ name: AFL++ Fuzzing on: schedule: - - cron: "0 2 * * *" - workflow_dispatch: - push: - branches: [main] + - cron: "0 2 * * *" # nightly at 02:00 UTC + workflow_dispatch: # manual trigger env: RISC0_DEV_MODE: "1" - CARGO_TERM_COLOR: always jobs: # ──────────────────────────────────────────────────────────────────────────── - # afl-smoke — 60-second per targets + # afl-smoke — 120-second campaign for 7 priority targets # ──────────────────────────────────────────────────────────────────────────── afl-smoke: name: "AFL++ smoke — ${{ matrix.target }}" runs-on: ubuntu-latest - - permissions: - contents: read + container: + image: aflplusplus/aflplusplus:v4.40c strategy: fail-fast: false matrix: target: - - fuzz_apply_state_diff_split_path - - fuzz_block_verification - - fuzz_encoding_roundtrip - - fuzz_multi_block_state_sequence - - fuzz_program_deployment_lifecycle - - fuzz_replay_prevention - - fuzz_sequencer_vs_replayer - - fuzz_signature_verification - - fuzz_state_diff_computation - - fuzz_state_serialization - - fuzz_state_transition - - fuzz_stateless_verification - fuzz_transaction_decoding - - fuzz_validate_execute_consistency - - fuzz_witness_set_verification + - 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: Checkout logos-execution-zone - uses: ./.github/actions/checkout-lez - - - name: Install logos-blockchain-circuits - uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install AFL++ v4.40c from source - run: | - sudo apt-get update -q - sudo apt-get install -y \ - build-essential python3-dev automake cmake \ - flex bison libglib2.0-dev libpixman-1-dev \ - python3-setuptools ninja-build - git clone --depth 1 --branch v4.40c \ - https://github.com/AFLplusplus/AFLplusplus /tmp/aflplusplus - cd /tmp/aflplusplus - make distrib - sudo make install - afl-fuzz --version - - 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: | - cargo afl build \ - --manifest-path fuzz/Cargo.toml \ + cd fuzz && cargo afl build \ --no-default-features \ --features fuzzer-afl \ --release \ --bin ${{ matrix.target }} - - name: Prepare seed corpus + - 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 }}" - SEEDS="afl-seeds/${TARGET}" - mkdir -p "$SEEDS" - # Merge checked-in libFuzzer corpus and accumulated AFL corpus - for src in corpus/libfuzz/${TARGET} corpus/afl/${TARGET}; do - [ -d "$src" ] || continue - for f in "$src"/*; do + 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 - cp -n "$f" "$SEEDS/" 2>/dev/null || true + 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 - # Guarantee at least one seed so afl-fuzz does not abort - if [ -z "$(ls -A "$SEEDS")" ]; then - echo -n "seed" > "$SEEDS/default_seed" - fi - echo "Seed inputs: $(ls "$SEEDS" | wc -l)" + echo "Synced ${count} new input(s) to ${DEST}" - - name: Run AFL++ for 60 seconds - env: - AFL_SKIP_CPUFREQ: "1" - AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: "1" - run: | - TARGET="${{ matrix.target }}" - mkdir -p afl-output/${TARGET} - # Disable errexit so that timeout's exit code 124 (expected signal) does not - # cause bash -e to abort the script before the guard below can run. - set +e - timeout 60 \ - afl-fuzz \ - -i afl-seeds/${TARGET} \ - -o afl-output/${TARGET} \ - -- fuzz/target/release/${TARGET} - rc=$? - set -e - # 124 = SIGALRM from timeout (expected); 0 = clean exit; anything else is a real failure - [ $rc -eq 0 ] || [ $rc -eq 124 ] || exit $rc + - 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: Calculate and show edge bitmap coverage - if: always() - run: | - TARGET="${{ matrix.target }}" - MAP_SIZE=65536 - - # ── Method 1: bitmap_cvg from fuzzer_stats (written live by afl-fuzz) ── - STATS="afl-output/${TARGET}/default/fuzzer_stats" - if [ -f "$STATS" ]; then - cvg=$(grep '^bitmap_cvg' "$STATS" | awk '{print $3}') - filled_stat=$(grep '^edges_found' "$STATS" | awk '{print $3}' || echo "n/a") - else - cvg="n/a" - filled_stat="n/a" - fi - - # ── Method 2: afl-showmap union over checked-in corpus ── - CORPUS="corpus/afl/${TARGET}" - BINARY="fuzz/target/release/${TARGET}" - showmap_filled="n/a" - showmap_pct="n/a" - if [ -d "$CORPUS" ] && [ -f "$BINARY" ]; then - afl-showmap -C \ - -i "$CORPUS" \ - -o "afl-edges-${TARGET}.txt" \ - -- "$BINARY" 2>/dev/null || true - if [ -f "afl-edges-${TARGET}.txt" ]; then - showmap_filled=$(wc -l < "afl-edges-${TARGET}.txt" | tr -d ' ') - showmap_pct=$(echo "scale=2; ${showmap_filled} * 100 / ${MAP_SIZE}" | bc) - fi - fi - - # ── ASCII bitmap visualisation (64×64 grid, one cell = 1024 slots) ── - # Each of the 4096 cells represents 16 consecutive bitmap slots. - # Cell is '■' if ANY of its 16 slots is non-zero, '·' otherwise. - EDGE_FILE="afl-edges-${TARGET}.txt" - CELLS=64 # 64 cells wide × 64 tall = 4096 cells × 16 slots = 65536 - SLOTS_PER_CELL=16 - if [ -f "$EDGE_FILE" ]; then - python3 - "$EDGE_FILE" "$CELLS" "$SLOTS_PER_CELL" <<'PYEOF' - import sys, math - - edge_file = sys.argv[1] - cells = int(sys.argv[2]) # cells per row - spc = int(sys.argv[3]) # slots per cell - MAP_SIZE = 65536 - total_cells = cells * cells # 4096 - - hit = set() - with open(edge_file) as f: - for line in f: - line = line.strip() - if ':' in line: - slot = int(line.split(':')[0]) - hit.add(slot) - - print(f"\nEdge bitmap visualisation — {cells}×{cells} grid " - f"(each cell = {spc} slots, ■=any hit, ·=none)") - print("+" + "─" * (cells * 2 - 1) + "+") - for row in range(cells): - row_str = "" - for col in range(cells): - cell_idx = row * cells + col - slot_start = cell_idx * spc - slot_end = slot_start + spc - filled = any(s in hit for s in range(slot_start, slot_end)) - row_str += ("■" if filled else "·") + " " - print("|" + row_str.rstrip() + "|") - print("+" + "─" * (cells * 2 - 1) + "+") - - filled_cells = sum( - 1 for c in range(total_cells) - if any((c * spc + s) in hit for s in range(spc)) - ) - print(f"Cells filled: {filled_cells}/{total_cells} " - f"({filled_cells*100/total_cells:.1f}%)\n") - PYEOF - fi - - # ── GitHub Step Summary ── - { - echo "## Edge Bitmap Coverage — \`${TARGET}\`" - echo "" - echo "| Method | Filled slots | Bitmap filled % |" - echo "|---|---|---|" - echo "| \`fuzzer_stats\` (afl-fuzz live) | ${filled_stat} | **${cvg}** |" - echo "| \`afl-showmap\` (corpus union) | ${showmap_filled} | **${showmap_pct}%** |" - echo "" - echo "> MAP_SIZE = ${MAP_SIZE} slots (2¹⁶). " - echo "> A slot is filled when any corpus input exercises that program edge." - } >> "$GITHUB_STEP_SUMMARY" - - - name: Package AFL findings into tarball - if: always() - run: | - TARGET="${{ matrix.target }}" - OUTPUT="afl-output/${TARGET}" - # AFL++ queue/crash/hang filenames contain colons, which are forbidden by - # actions/upload-artifact on NTFS-based runners. Bundle everything into a - # single tarball so the colon-bearing filenames never appear as individual - # artifact entries. - if [ -d "$OUTPUT" ]; then - tar -czf "afl-findings-${TARGET}.tar.gz" \ - -C "$(dirname "$OUTPUT")" "$(basename "$OUTPUT")" - else - tar -czf "afl-findings-${TARGET}.tar.gz" -T /dev/null - fi - - - name: Upload AFL findings artifact + - name: Upload crashes and hangs artifact if: always() uses: actions/upload-artifact@v4 with: name: afl-findings-${{ matrix.target }} - path: afl-findings-${{ matrix.target }}.tar.gz + path: | + afl-output/${{ matrix.target }}/*/crashes/ + afl-output/${{ matrix.target }}/*/hangs/ if-no-files-found: ignore # ──────────────────────────────────────────────────────────────────────────── - # afl-coverage — LLVM coverage report for all 15 targets + # afl-coverage — LLVM coverage report for 3 key targets # ──────────────────────────────────────────────────────────────────────────── afl-coverage: name: "AFL++ coverage — ${{ matrix.target }}" @@ -250,67 +124,38 @@ jobs: fail-fast: false matrix: target: - - fuzz_apply_state_diff_split_path - - fuzz_block_verification - - fuzz_encoding_roundtrip - - fuzz_multi_block_state_sequence - - fuzz_program_deployment_lifecycle - - fuzz_replay_prevention - - fuzz_sequencer_vs_replayer - - fuzz_signature_verification - - fuzz_state_diff_computation - - fuzz_state_serialization - fuzz_state_transition - - fuzz_stateless_verification - - fuzz_transaction_decoding - - fuzz_validate_execute_consistency - - fuzz_witness_set_verification + - fuzz_sequencer_vs_replayer + - fuzz_apply_state_diff_split_path steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Checkout logos-execution-zone - uses: ./.github/actions/checkout-lez - - - name: Install logos-blockchain-circuits - uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - 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: . - continue-on-error: true # no crashes/hangs/queue is fine - - - name: Extract AFL findings tarball - run: | - TARGET="${{ matrix.target }}" - TARBALL="afl-findings-${TARGET}.tar.gz" - if [ -f "$TARBALL" ]; then - tar -xzf "$TARBALL" - fi + 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: | - # Build with the libfuzzer harness: libFuzzer accepts corpus files as - # positional arguments, runs each through the fuzz closure once, then - # exits — LLVM coverage counters (-C instrument-coverage) are flushed - # to the .profraw file on exit regardless of the fuzzer runtime used. cargo build \ --manifest-path fuzz/Cargo.toml \ --no-default-features \ - --features fuzzer-libfuzzer \ + --features fuzzer-afl \ --release \ --bin ${{ matrix.target }} @@ -322,20 +167,20 @@ jobs: mkdir -p "$PROFRAW_DIR" idx=0 - # AFL corpus (checked-in, accumulated from prior runs) - for f in corpus/afl/${TARGET}/*; do + # 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 + LLVM_PROFILE_FILE="${PROFRAW_DIR}/${idx}.profraw" "$BINARY" < "$f" 2>/dev/null || true idx=$((idx + 1)) done - # AFL++ queue entries from today's smoke run (downloaded artifact) + # 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 + LLVM_PROFILE_FILE="${PROFRAW_DIR}/${idx}.profraw" "$BINARY" < "$f" 2>/dev/null || true idx=$((idx + 1)) done done @@ -346,16 +191,13 @@ jobs: TARGET="${{ matrix.target }}" PROFRAW_DIR="coverage/afl/${TARGET}/profraw" PROFDATA="coverage/afl/${TARGET}/merged.profdata" - SYSROOT="$(rustc --print sysroot)" - HOST_TRIPLE="$(rustc -vV | awk '/^host:/{print $2}')" - LLVM_PROFDATA="${SYSROOT}/lib/rustlib/${HOST_TRIPLE}/bin/llvm-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" + llvm-profdata merge -sparse "${files[@]}" -o "$PROFDATA" - name: Generate HTML coverage report run: | @@ -363,15 +205,12 @@ jobs: BINARY="fuzz/target/release/${TARGET}" PROFDATA="coverage/afl/${TARGET}/merged.profdata" HTML_DIR="coverage/afl/${TARGET}/html" - SYSROOT="$(rustc --print sysroot)" - HOST_TRIPLE="$(rustc -vV | awk '/^host:/{print $2}')" - LLVM_COV="${SYSROOT}/lib/rustlib/${HOST_TRIPLE}/bin/llvm-cov" if [ ! -f "$PROFDATA" ]; then echo "No profdata — skipping HTML report." exit 0 fi mkdir -p "$HTML_DIR" - "$LLVM_COV" show \ + llvm-cov show \ "$BINARY" \ --instr-profile="$PROFDATA" \ --format=html \ diff --git a/Justfile b/Justfile index 7bfb9be..d5b5084 100644 --- a/Justfile +++ b/Justfile @@ -165,11 +165,7 @@ afl-macos-teardown: 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`. +# 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). @@ -179,11 +175,11 @@ afl-macos-teardown: # 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 +# 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, 30 s +# 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="30": +fuzz-afl TARGET="" TIME="120": #!/bin/bash set -euo pipefail TARGET="{{TARGET}}" @@ -238,7 +234,7 @@ fuzz-afl TARGET="" TIME="30": _run_one() { local t="$1" local BINARY="fuzz/target/release/$t" - local CORPUS="corpus/libfuzz/$t" # seed from libFuzzer corpus + local CORPUS="fuzz/corpus/$t" local OUTPUT="afl-output/$t" mkdir -p "$CORPUS" "$OUTPUT" if [ ! -f "$BINARY" ]; then @@ -253,48 +249,6 @@ fuzz-afl TARGET="" TIME="30": 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 @@ -306,7 +260,7 @@ 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 + CORPUS="fuzz/corpus/{{TARGET}}" OUTPUT="afl-output/{{TARGET}}" mkdir -p "$CORPUS" "$OUTPUT" if [ ! -f "$BINARY" ]; then @@ -338,8 +292,8 @@ fuzz-afl-parallel TARGET WORKERS="4" TIME="300": 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. +# 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 @@ -349,7 +303,7 @@ afl-corpus-sync: fi for target_dir in afl-output/*/; do TARGET=$(basename "$target_dir") - DEST="corpus/afl/${TARGET}" + DEST="fuzz/corpus/${TARGET}" mkdir -p "$DEST" count=0 for instance_dir in "$target_dir"*/; do @@ -385,263 +339,80 @@ 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. # -# 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. +# 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}}/. # -# 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 + # ── Step 1: libFuzzer coverage ──────────────────────────────────────────── + echo "=== cargo fuzz coverage {{TARGET}} ===" + cargo fuzz coverage {{TARGET}} || true - # ── 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 + # ── 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 - if [ "{{ENGINE}}" = "all" ] || [ "{{ENGINE}}" = "afl" ]; then - for target in "${TARGETS[@]}"; do - echo "=== coverage (afl) $target ===" - just coverage-afl "$target" + 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 - fi + done - # ── 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" + # Merge raw profiles. + PROFDATA="${COV_DIR}/merged.profdata" + llvm-profdata merge -sparse "${PROFRAW_DIR}"/*.profraw -o "$PROFDATA" - 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 + # 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" - 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 +# 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 ────────────────────────────────────────────────────────────── @@ -654,21 +425,13 @@ clean: 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. +# Remove coverage reports generated by `cargo fuzz coverage` and `just coverage` clean-coverage: - rm -rf coverage/ fuzz/coverage/ - find . -name '*.profraw' -delete + rm -rf fuzz/coverage/ coverage/ -# Remove AFL++ output directories (crashes, hangs, queue). -# Note: the queue is also stored in corpus/afl/ via `just afl-corpus-sync`. +# Remove AFL++ output directories (crash/hang/queue findings) 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/) +# 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 bb870ca..796130a 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -222,29 +222,6 @@ cd .. cargo install cargo-afl ``` -> **macOS: run `afl-system-config` once before fuzzing** — AFL++ uses System V shared -> memory (`shmget`) to pass coverage bitmaps between the fuzzer and the target. macOS -> ships with very small defaults (`kern.sysv.shmmax = 4 MB`, `kern.sysv.shmmni = 32`) -> that are exhausted as soon as multiple AFL++ instances start in parallel, causing every -> run to abort immediately with: -> -> ``` -> [-] SYSTEM ERROR : shmget() failed, try running afl-system-config -> OS message : Invalid argument -> ``` -> -> Fix by running the AFL++ system-configuration helper once per boot (or after every -> macOS update): -> -> ```bash -> sudo afl-system-config -> ``` -> -> This raises `shmmax`, `shmmni`, `shmall`, and related limits to values suitable for -> parallel fuzzing. The change is not persistent across reboots, so re-run it after -> each restart. The `just fuzz-afl` and `just fuzz-afl-parallel` recipes **do not** -> call this automatically because it requires `sudo`. - > **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` diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 5ad1b08..b371412 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -2009,6 +2009,7 @@ dependencies = [ "afl", "arbitrary", "borsh", + "cc", "common", "fuzz_props", "libfuzzer-sys", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index d2d8e87..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 @@ -50,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_props/src/generators.rs b/fuzz_props/src/generators.rs index def495f..10927cd 100644 --- a/fuzz_props/src/generators.rs +++ b/fuzz_props/src/generators.rs @@ -11,7 +11,6 @@ use testnet_initial_state::initial_pub_accounts_private_keys; /// 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`. -#[must_use] pub fn signer_account_ids(tx: &common::transaction::NSSATransaction) -> Vec { use common::transaction::NSSATransaction; match tx { diff --git a/fuzz_props/src/lib.rs b/fuzz_props/src/lib.rs index 6c45ffd..748fb0b 100644 --- a/fuzz_props/src/lib.rs +++ b/fuzz_props/src/lib.rs @@ -85,5 +85,17 @@ macro_rules! fuzz_entry { }; } +#[cfg(test)] +mod seed_gen { + use std::fs; + use std::path::Path; + + #[cfg(feature = "fuzzer-afl")] + fn main() { + ::afl::fuzz!(|$data: &[u8]| $body); + } + }; +} + #[cfg(test)] mod tests;