From 7730f3bceec941d15d9e0cd55d95f395ee829e2d Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 27 Oct 2025 21:32:57 +0000 Subject: [PATCH 1/6] Use `honggfuzz`'s `--run-time` arg to limit per-fuzz runtime in CI We have some complexity in `ci-fuzz.sh` to limit each fuzzer to a rough runtime, but `honggfuzz` has a `--run-time` argument that we can simply use instead, which we do here. --- fuzz/ci-fuzz.sh | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/fuzz/ci-fuzz.sh b/fuzz/ci-fuzz.sh index d1274d751a8..d57a5ad78fa 100755 --- a/fuzz/ci-fuzz.sh +++ b/fuzz/ci-fuzz.sh @@ -33,17 +33,9 @@ cargo --color always hfuzz build -j8 for TARGET in src/bin/*.rs; do FILENAME=$(basename $TARGET) FILE="${FILENAME%.*}" - HFUZZ_RUN_ARGS="--exit_upon_crash -v -n8" - if [ "$FILE" = "chanmon_consistency_target" ]; then - HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -F 64 -N1000" - elif [ "$FILE" = "process_network_graph_target" -o "$FILE" = "full_stack_target" -o "$FILE" = "router_target" -o "$FILE" = "lsps_message_target" ]; then - HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -N10000" - elif [ "$FILE" = "indexedmap_target" ]; then - HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -N100000" - elif [ "$FILE" = "fs_store_target" ]; then - HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -F 64 -N10000" - else - HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -N1000000" + HFUZZ_RUN_ARGS="--exit_upon_crash -v -n8 --run_time 30" + if [ "$FILE" = "chanmon_consistency_target" -o "$FILE" = "fs_store_target" ]; then + HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -F 64" fi export HFUZZ_RUN_ARGS cargo --color always hfuzz run $FILE From 89671a5c24d8003ce47a7fbf6a9a3e084cdd26eb Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 27 Oct 2025 21:36:25 +0000 Subject: [PATCH 2/6] Disable `codegen-units = 1` in CI fuzz job This now slows us down as we run our fuzz job on a machine with more than one or two cores. --- fuzz/ci-fuzz.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/fuzz/ci-fuzz.sh b/fuzz/ci-fuzz.sh index d57a5ad78fa..46d9acf3b2e 100755 --- a/fuzz/ci-fuzz.sh +++ b/fuzz/ci-fuzz.sh @@ -26,6 +26,7 @@ cargo install --color always --force honggfuzz --no-default-features # Because we're fuzzing relatively few iterations, the maximum possible # compiler optimizations aren't necessary, so we turn off LTO sed -i 's/lto = true//' Cargo.toml +sed -i 's/codegen-units = 1//' Cargo.toml export HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" From 1ba5defbd78700a2800c4186e372799a2a655e00 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 28 Oct 2025 00:46:40 +0000 Subject: [PATCH 3/6] Bump fuzz MSRV to 1.80 honggfuzz builds appear to fail on 1.75 - 1.79, so we just pick an MSRV of the first toolchain that actually works. Sadly, github for some reason drops a trailing in the env entry, so we copy the rust version a few places. --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b643058a746..b0b45541a91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -253,15 +253,15 @@ jobs: fuzz: runs-on: self-hosted - env: - TOOLCHAIN: 1.75 steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Install Rust ${{ env.TOOLCHAIN }} toolchain + # For whatever reason, honggfuzz doesn't build on 1.75, and there's not a lot of + # reason to insist on 1.75 for fuzzing, so we just pick an MSRV of 1.80 for fuzz. + - name: Install Rust 1.80 toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} - - name: Sanity check fuzz targets on Rust ${{ env.TOOLCHAIN }} + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain 1.80 + - name: Sanity check fuzz targets on Rust 1.80 run: | cd fuzz RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" cargo test --verbose --color always --lib --bins -j8 From e0fc06420327d7f875cbe3b83b5fa93ab7aa3d40 Mon Sep 17 00:00:00 2001 From: Anyitechs Date: Fri, 10 Oct 2025 13:05:22 +0100 Subject: [PATCH 4/6] Upload CI generated fuzz corpus coverage to codecov Because each CI job runs on a fresh runner and can't share data between jobs. We rely on Github Actions upload-artifact and download-artifact to share the CI generated fuzz corpus, then replay them in the `contrib/generate_fuzz_coverage.sh` script to generate the coverage report. --- .github/workflows/build.yml | 11 +++++++++ contrib/generate_fuzz_coverage.sh | 37 ++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0b45541a91..98730918362 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,6 +110,7 @@ jobs: run: CI_ENV=1 CI_MINIMIZE_DISK_USAGE=1 ./ci/ci-tx-sync-tests.sh coverage: + needs: fuzz strategy: fail-fast: false runs-on: self-hosted @@ -133,6 +134,11 @@ jobs: # Maybe if codecov wasn't broken we wouldn't need to do this... ./codecov --verbose upload-process --disable-search --fail-on-error -f target/codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'tests' cargo clean + - name: Download honggfuzz corpus + uses: actions/download-artifact@v4 + with: + name: hfuzz-corpus + path: fuzz/hfuzz_workspace - name: Run fuzz coverage generation run: | ./contrib/generate_fuzz_coverage.sh --output-dir `pwd` --output-codecov-json @@ -268,6 +274,11 @@ jobs: cargo clean - name: Run fuzzers run: cd fuzz && ./ci-fuzz.sh && cd .. + - name: Upload honggfuzz corpus + uses: actions/upload-artifact@v4 + with: + name: hfuzz-corpus + path: fuzz/hfuzz_workspace linting: runs-on: ubuntu-latest diff --git a/contrib/generate_fuzz_coverage.sh b/contrib/generate_fuzz_coverage.sh index 694ff65aa2f..34f163205ea 100755 --- a/contrib/generate_fuzz_coverage.sh +++ b/contrib/generate_fuzz_coverage.sh @@ -62,9 +62,40 @@ if [ "$OUTPUT_CODECOV_JSON" = "0" ]; then cargo llvm-cov --html --ignore-filename-regex "fuzz/" --output-dir "$OUTPUT_DIR" echo "Coverage report generated in $OUTPUT_DIR/html/index.html" else - cargo llvm-cov -j8 --codecov --ignore-filename-regex "fuzz/" --output-path "$OUTPUT_DIR/fuzz-codecov.json" - echo "Fuzz codecov report available at $OUTPUT_DIR/fuzz-codecov.json" -fi + # Clean previous coverage artifacts to ensure a fresh run. + cargo llvm-cov clean --workspace + # Import honggfuzz corpus if the artifact was downloaded. + imported=0 + if [ -d "hfuzz_workspace" ]; then + echo "Importing corpus from hfuzz_workspace..." + for target_dir in hfuzz_workspace/*; do + [ -d "$target_dir" ] || continue + src_name="$(basename "$target_dir")" + for dest in "$src_name" "${src_name%_target}"; do + mkdir -p "test_cases/$dest" + # Copy corpus files into the test_cases directory + find "$target_dir" -maxdepth 2 -type f \ + \( -path "$target_dir/CORPUS/*" -o -path "$target_dir/INPUT/*" -o -path "$target_dir/NEW/*" -o -path "$target_dir/input/*" \) \ + -print0 | xargs -0 -I{} cp -n {} "test_cases/$dest/" 2>/dev/null || true + done + done + # Check if any files were actually imported + if [ -n "$(find test_cases -type f -print -quit 2>/dev/null)" ]; then + imported=1 + fi + fi + # Generate coverage based on whether a corpus was imported. + if [ "$imported" = "1" ]; then + echo "Replaying imported corpus via tests to generate coverage..." + cargo llvm-cov -j8 --codecov --ignore-filename-regex "fuzz/" \ + --output-path "$OUTPUT_DIR/fuzz-codecov.json" --tests + else + echo "No corpus found; generating no-corpus coverage JSON..." + cargo llvm-cov -j8 --codecov --ignore-filename-regex "fuzz/" \ + --output-path "$OUTPUT_DIR/fuzz-codecov.json" --no-run + fi + echo "Fuzz codecov report available at $OUTPUT_DIR/fuzz-codecov.json" +fi From cc1355faafc1a58b00e224003f0d6f3a7efe5daf Mon Sep 17 00:00:00 2001 From: Anyitechs Date: Tue, 14 Oct 2025 18:25:52 +0100 Subject: [PATCH 5/6] f copy to canonical name, drop guard and always replay via tests --- contrib/generate_fuzz_coverage.sh | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/contrib/generate_fuzz_coverage.sh b/contrib/generate_fuzz_coverage.sh index 34f163205ea..09d37656f47 100755 --- a/contrib/generate_fuzz_coverage.sh +++ b/contrib/generate_fuzz_coverage.sh @@ -66,36 +66,22 @@ else cargo llvm-cov clean --workspace # Import honggfuzz corpus if the artifact was downloaded. - imported=0 if [ -d "hfuzz_workspace" ]; then echo "Importing corpus from hfuzz_workspace..." for target_dir in hfuzz_workspace/*; do [ -d "$target_dir" ] || continue src_name="$(basename "$target_dir")" - for dest in "$src_name" "${src_name%_target}"; do - mkdir -p "test_cases/$dest" - # Copy corpus files into the test_cases directory - find "$target_dir" -maxdepth 2 -type f \ - \( -path "$target_dir/CORPUS/*" -o -path "$target_dir/INPUT/*" -o -path "$target_dir/NEW/*" -o -path "$target_dir/input/*" \) \ - -print0 | xargs -0 -I{} cp -n {} "test_cases/$dest/" 2>/dev/null || true - done + dest="${src_name%_target}" + mkdir -p "test_cases/$dest" + # Copy corpus files into the test_cases directory + find "$target_dir" -maxdepth 2 -type f -path "$target_dir/input/*" \ + -print0 | xargs -0 -I{} cp -n {} "test_cases/$dest/" done - # Check if any files were actually imported - if [ -n "$(find test_cases -type f -print -quit 2>/dev/null)" ]; then - imported=1 - fi fi - # Generate coverage based on whether a corpus was imported. - if [ "$imported" = "1" ]; then - echo "Replaying imported corpus via tests to generate coverage..." - cargo llvm-cov -j8 --codecov --ignore-filename-regex "fuzz/" \ - --output-path "$OUTPUT_DIR/fuzz-codecov.json" --tests - else - echo "No corpus found; generating no-corpus coverage JSON..." - cargo llvm-cov -j8 --codecov --ignore-filename-regex "fuzz/" \ - --output-path "$OUTPUT_DIR/fuzz-codecov.json" --no-run - fi + echo "Replaying imported corpus (if found) via tests to generate coverage..." + cargo llvm-cov -j8 --codecov --ignore-filename-regex "fuzz/" \ + --output-path "$OUTPUT_DIR/fuzz-codecov.json" --tests echo "Fuzz codecov report available at $OUTPUT_DIR/fuzz-codecov.json" fi From 6cd3f8fb021124d94652e9cd9c59df2c462d15af Mon Sep 17 00:00:00 2001 From: Anyitechs Date: Mon, 27 Oct 2025 17:04:32 +0100 Subject: [PATCH 6/6] persist fuzz corpus between CI runs --- .github/workflows/build.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98730918362..f40396702fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -267,6 +267,27 @@ jobs: - name: Install Rust 1.80 toolchain run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain 1.80 + # This is read-only for PRs. It seeds the fuzzer for a more effective run. + # NOTE: The `key` is unique and will always miss, forcing a fallback to + # the `restore-keys` to find the latest global cache from the `main` branch. + - name: Restore persistent fuzz corpus (PR) + if: ${{ github.ref != 'refs/heads/main' }} + uses: actions/cache/restore@v4 + with: + path: fuzz/hfuzz_workspace + key: fuzz-corpus-${{ github.ref }}-${{ github.sha }} + restore-keys: | + fuzz-corpus-refs/heads/main- + # Only on the `main` branch, restores the latest corpus and also save a + # new, updated one. + - name: Restore/Save persistent honggfuzz corpus (Main) + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/cache@v4 + with: + path: fuzz/hfuzz_workspace + key: fuzz-corpus-refs/heads/main-${{ github.sha }} + restore-keys: | + fuzz-corpus-refs/heads/main- - name: Sanity check fuzz targets on Rust 1.80 run: | cd fuzz