From d8c8bfe495bda8e545f1daf528e7b5626e58ddd6 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 19 Mar 2026 10:50:53 +0100 Subject: [PATCH 1/3] fuzz: handle missing SendTx* message events in chanmon_consistency Add handlers for SendTxInitRbf, SendTxAckRbf, SendTxRemoveInput, and SendTxRemoveOutput in the chanmon_consistency fuzz target. These variants were reachable but not matched, causing panics on the wildcard arm ("Unhandled message event"). SendTxInitRbf became reachable after commit 5873660a0 added splicing support without updating the fuzz target's message delivery logic. AI tools were used in preparing this commit. --- fuzz/src/chanmon_consistency.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 96104162db7..e4fd3475024 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -1513,6 +1513,14 @@ pub fn do_test( if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } *node_id == a_id }, + MessageSendEvent::SendTxRemoveInput { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, + MessageSendEvent::SendTxRemoveOutput { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, MessageSendEvent::SendTxComplete { ref node_id, .. } => { if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } *node_id == a_id @@ -1521,6 +1529,14 @@ pub fn do_test( if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } *node_id == a_id }, + MessageSendEvent::SendTxInitRbf { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, + MessageSendEvent::SendTxAckRbf { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, MessageSendEvent::SendTxSignatures { ref node_id, .. } => { if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } *node_id == a_id @@ -1713,6 +1729,22 @@ pub fn do_test( } } }, + MessageSendEvent::SendTxInitRbf { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering tx_init_rbf from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_tx_init_rbf(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendTxAckRbf { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering tx_ack_rbf from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_tx_ack_rbf(nodes[$node].get_our_node_id(), msg); + } + } + }, MessageSendEvent::SendTxSignatures { ref node_id, ref msg } => { for (idx, dest) in nodes.iter().enumerate() { if dest.get_our_node_id() == *node_id { From 1a289ab4709c50b789501b38968eb6a8fb647d52 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 20 Mar 2026 08:42:11 +0100 Subject: [PATCH 2/3] ci: split fuzz sanity check into separate parallel job The sanity check (cargo test on fuzz targets) doesn't use the restored corpus and was blocking the actual fuzz run. Move it to a separate fuzz_sanity job so both run in parallel. AI tools were used in preparing this commit. --- .github/workflows/build.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d512791420..13650d222b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -205,6 +205,21 @@ jobs: - name: Simulate docs.rs build run: ci/check-docsrs.sh + fuzz_sanity: + runs-on: self-hosted + env: + TOOLCHAIN: 1.75 + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Install Rust ${{ env.TOOLCHAIN }} 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 }} + run: | + cd fuzz + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" cargo test --quiet --color always --lib --bins -j8 + fuzz: runs-on: self-hosted env: @@ -238,11 +253,6 @@ jobs: key: fuzz-corpus-refs/heads/main-${{ github.sha }} restore-keys: | fuzz-corpus-refs/heads/main- - - name: Sanity check fuzz targets on Rust ${{ env.TOOLCHAIN }} - run: | - cd fuzz - RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" cargo test --verbose --color always --lib --bins -j8 - cargo clean - name: Run fuzzers run: cd fuzz && ./ci-fuzz.sh && cd .. - name: Upload honggfuzz corpus @@ -308,7 +318,7 @@ jobs: TOR_PROXY="127.0.0.1:9050" RUSTFLAGS="--cfg=tor" cargo test --verbose --color always -p lightning-net-tokio notify-failure: - needs: [build-workspace, build-features, build-bindings, build-nostd, build-cfg-flags, build-sync, fuzz, linting, rustfmt, check_release, check_docs, benchmark, ext-test, tor-connect, coverage] + needs: [build-workspace, build-features, build-bindings, build-nostd, build-cfg-flags, build-sync, fuzz_sanity, fuzz, linting, rustfmt, check_release, check_docs, benchmark, ext-test, tor-connect, coverage] if: failure() && github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: From 63d680256163d6fa0f584b3ed3989d2efc22340d Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 20 Mar 2026 15:38:12 +0100 Subject: [PATCH 3/3] fuzz: improve iteration scaling, add minimization and summary table Replace the fixed 30s run_time with iteration counts scaled to 8x corpus size (plus a 1000 baseline) with a 10-minute hard cap per target. This ensures the full corpus is replayed with room for mutations, while small targets finish quickly. On main (and on PRs with the fuzz-minimize label), run honggfuzz corpus minimization after each target to prune inputs that don't contribute unique coverage, keeping the cache size manageable. Print a summary table at the end with per-target stats: iterations, corpus sizes before/after fuzzing and minimization, and run times. Other changes: - Use -q (quiet) to suppress per-iteration status output - Set 3s per-input timeout (-t 3) for all targets - Pass FUZZ_MINIMIZE env var from PR label in workflow AI tools were used in preparing this commit. --- .github/workflows/build.yml | 2 ++ fuzz/ci-fuzz.sh | 52 ++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13650d222b5..664b6d9a9c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -255,6 +255,8 @@ jobs: fuzz-corpus-refs/heads/main- - name: Run fuzzers run: cd fuzz && ./ci-fuzz.sh && cd .. + env: + FUZZ_MINIMIZE: ${{ contains(github.event.pull_request.labels.*.name, 'fuzz-minimize') }} - name: Upload honggfuzz corpus uses: actions/upload-artifact@v4 with: diff --git a/fuzz/ci-fuzz.sh b/fuzz/ci-fuzz.sh index d57a5ad78fa..778e37fac6e 100755 --- a/fuzz/ci-fuzz.sh +++ b/fuzz/ci-fuzz.sh @@ -30,15 +30,28 @@ sed -i 's/lto = true//' Cargo.toml export HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" cargo --color always hfuzz build -j8 + +SUMMARY="" + for TARGET in src/bin/*.rs; do FILENAME=$(basename $TARGET) FILE="${FILENAME%.*}" - HFUZZ_RUN_ARGS="--exit_upon_crash -v -n8 --run_time 30" + CORPUS_DIR="hfuzz_workspace/$FILE/input" + CORPUS_COUNT=$(find "$CORPUS_DIR" -type f 2>/dev/null | wc -l) + # Run 8x the corpus size plus a baseline, ensuring full corpus replay + # with room for new mutations. The 10-minute hard cap (--run_time 600) + # prevents slow-per-iteration targets from running too long. + ITERATIONS=$((CORPUS_COUNT * 8 + 1000)) + HFUZZ_RUN_ARGS="--exit_upon_crash -q -n8 -t 3 -N $ITERATIONS --run_time 600" if [ "$FILE" = "chanmon_consistency_target" -o "$FILE" = "fs_store_target" ]; then HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -F 64" fi export HFUZZ_RUN_ARGS + FUZZ_START=$(date +%s) cargo --color always hfuzz run $FILE + FUZZ_END=$(date +%s) + FUZZ_TIME=$((FUZZ_END - FUZZ_START)) + FUZZ_CORPUS_COUNT=$(find "$CORPUS_DIR" -type f 2>/dev/null | wc -l) if [ -f hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT ]; then cat hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT for CASE in hfuzz_workspace/$FILE/SIG*; do @@ -46,4 +59,41 @@ for TARGET in src/bin/*.rs; do done exit 1 fi + if [ "$GITHUB_REF" = "refs/heads/main" ] || [ "$FUZZ_MINIMIZE" = "true" ]; then + HFUZZ_RUN_ARGS="-M -q -n8 -t 3" + export HFUZZ_RUN_ARGS + MIN_START=$(date +%s) + cargo --color always hfuzz run $FILE + MIN_END=$(date +%s) + MIN_TIME=$((MIN_END - MIN_START)) + MIN_CORPUS_COUNT=$(find "$CORPUS_DIR" -type f 2>/dev/null | wc -l) + SUMMARY="${SUMMARY}${FILE}|${ITERATIONS}|${CORPUS_COUNT}|${FUZZ_CORPUS_COUNT}|${FUZZ_TIME}|${MIN_CORPUS_COUNT}|${MIN_TIME}\n" + else + SUMMARY="${SUMMARY}${FILE}|${ITERATIONS}|${CORPUS_COUNT}|${FUZZ_CORPUS_COUNT}|${FUZZ_TIME}|-|-\n" + fi +done + +fmt_time() { + local secs=$1 + printf "%dm%ds" $((secs / 60)) $((secs % 60)) +} + +# Print summary table +set +x +echo "" +echo "==== Fuzz Summary ====" +HDR="%-40s %7s %7s %-15s %9s %-15s %9s\n" +FMT="%-40s %7s %7s %6s %-9s %9s %6s %-9s %9s\n" +printf "$HDR" "Target" "Iters" "Corpus" " Fuzzed" "Fuzz time" " Minimized" "Min. time" +printf "$HDR" "------" "-----" "------" "---------------" "---------" "---------------" "---------" +echo -e "$SUMMARY" | while IFS='|' read -r name iters orig fuzzed ftime minimized mtime; do + [ -z "$name" ] && continue + fuzz_delta=$((fuzzed - orig)) + if [ "$minimized" = "-" ]; then + printf "$FMT" "$name" "$iters" "$orig" "$fuzzed" "(+$fuzz_delta)" "$(fmt_time "$ftime")" "-" "" "-" + else + min_delta=$((minimized - fuzzed)) + printf "$FMT" "$name" "$iters" "$orig" "$fuzzed" "(+$fuzz_delta)" "$(fmt_time "$ftime")" "$minimized" "($min_delta)" "$(fmt_time "$mtime")" + fi done +echo "======================"