name: AFL++ Fuzzing on: schedule: - cron: "0 2 * * *" # nightly at 02:00 UTC workflow_dispatch: # manual trigger push: branches: - feat-add-afl-fuzzing env: RISC0_DEV_MODE: "1" jobs: # ──────────────────────────────────────────────────────────────────────────── # afl-smoke — 120-second campaign for all 15 targets # ──────────────────────────────────────────────────────────────────────────── afl-smoke: name: "AFL++ smoke — ${{ matrix.target }}" runs-on: ubuntu-latest permissions: contents: read 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 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Checkout logos-execution-zone alongside lez-fuzzing uses: actions/checkout@v4 with: repository: logos-blockchain/logos-execution-zone path: logos-execution-zone - name: Symlink logos-execution-zone to sibling directory run: ln -s "$GITHUB_WORKSPACE/logos-execution-zone" "$GITHUB_WORKSPACE/../logos-execution-zone" - 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 \ --no-default-features \ --features fuzzer-afl \ --release \ --bin ${{ matrix.target }} - name: Prepare seed 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 [ -f "$f" ] || continue cp -n "$f" "$SEEDS/" 2>/dev/null || true 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)" - name: Run AFL++ for 120 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 120 \ 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: 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 if: always() uses: actions/upload-artifact@v4 with: name: afl-findings-${{ matrix.target }} path: afl-findings-${{ matrix.target }}.tar.gz if-no-files-found: ignore # ──────────────────────────────────────────────────────────────────────────── # afl-coverage — LLVM coverage report for all 15 targets # ──────────────────────────────────────────────────────────────────────────── afl-coverage: name: "AFL++ coverage — ${{ matrix.target }}" runs-on: ubuntu-latest needs: afl-smoke 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 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Checkout logos-execution-zone alongside lez-fuzzing uses: actions/checkout@v4 with: repository: logos-blockchain/logos-execution-zone path: logos-execution-zone - name: Symlink logos-execution-zone to sibling directory run: ln -s "$GITHUB_WORKSPACE/logos-execution-zone" "$GITHUB_WORKSPACE/../logos-execution-zone" - 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: 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 - 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 \ --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 # AFL corpus (checked-in, accumulated from prior runs) for f in corpus/afl/${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 from today's smoke run (downloaded artifact) 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" LLVM_PROFDATA="$(rustup which 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" - 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" LLVM_COV="$(rustup which llvm-cov)" 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