Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 4 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ jobs:
# Could you use this to fake the coverage report for your PR? Sure.
# Will anyone be impressed by your amazing coverage? No
# Maybe if codecov wasn't broken we wouldn't need to do this...
./codecov --verbose upload-process --disable-search --fail-on-error -f fuzz-codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'fuzzing'
./codecov --verbose upload-process --disable-search --fail-on-error -f fuzz-fake-hashes-codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'fuzzing-fake-hashes'
./codecov --verbose upload-process --disable-search --fail-on-error -f fuzz-real-hashes-codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'fuzzing-real-hashes'

benchmark:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -218,7 +219,8 @@ jobs:
- name: Sanity check fuzz targets on Rust ${{ env.TOOLCHAIN }}
run: |
cd fuzz
cargo test --quiet --color always --lib --bins -j8
RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" cargo test --manifest-path fuzz-fake-hashes/Cargo.toml --quiet --color always --bins -j8
RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz" cargo test --manifest-path fuzz-real-hashes/Cargo.toml --quiet --color always --bins -j8

fuzz:
runs-on: self-hosted
Expand Down
6 changes: 5 additions & 1 deletion ci/check-compiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ echo "Testing $(git log -1 --oneline)"
cargo check
cargo doc
cargo doc --document-private-items
cd fuzz && RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" cargo check --features=stdin_fuzz
cd fuzz
RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" \
cargo check --manifest-path fuzz-fake-hashes/Cargo.toml --features=stdin_fuzz
RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz" \
cargo check --manifest-path fuzz-real-hashes/Cargo.toml --features=stdin_fuzz
cd ../lightning && cargo check --no-default-features
cd .. && RUSTC_BOOTSTRAP=1 RUSTFLAGS="--cfg=c_bindings" cargo check -Z avoid-dev-deps
46 changes: 34 additions & 12 deletions contrib/generate_fuzz_coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,37 @@ fi
# Create output directory if it doesn't exist
mkdir -p "$OUTPUT_DIR"

generate_coverage_report() {
local manifest_path="$1"
local output_path="$2"
local rustflags="$3"

cargo llvm-cov clean --workspace
RUSTFLAGS="$rustflags" cargo llvm-cov --manifest-path "$manifest_path" --codecov \
--dep-coverage lightning,lightning-invoice,lightning-liquidity,lightning-rapid-gossip-sync,lightning-persister \
--no-default-ignore-filename-regex \
--ignore-filename-regex "(\.cargo/registry|\.rustup/toolchains|/fuzz/)" \
--output-path "$output_path" --tests
}

# dont run this command when running in CI
if [ "$OUTPUT_CODECOV_JSON" = "0" ]; then
cargo llvm-cov --html \
cargo llvm-cov clean --workspace
RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" \
cargo llvm-cov --manifest-path fuzz-fake-hashes/Cargo.toml --html \
--dep-coverage lightning,lightning-invoice,lightning-liquidity,lightning-rapid-gossip-sync,lightning-persister \
--no-default-ignore-filename-regex \
--ignore-filename-regex "(\.cargo/registry|\.rustup/toolchains|/fuzz/)" \
--output-dir "$OUTPUT_DIR"
echo "Coverage report generated in $OUTPUT_DIR/html/index.html"
else
# Clean previous coverage artifacts to ensure a fresh run.
--output-dir "$OUTPUT_DIR/fake-hashes" --tests
cargo llvm-cov clean --workspace

RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz" \
cargo llvm-cov --manifest-path fuzz-real-hashes/Cargo.toml --html \
--dep-coverage lightning,lightning-invoice,lightning-liquidity,lightning-rapid-gossip-sync,lightning-persister \
--no-default-ignore-filename-regex \
--ignore-filename-regex "(\.cargo/registry|\.rustup/toolchains|/fuzz/)" \
--output-dir "$OUTPUT_DIR/real-hashes" --tests
echo "Coverage reports generated in $OUTPUT_DIR/fake-hashes and $OUTPUT_DIR/real-hashes"
else
# Import honggfuzz corpus if the artifact was downloaded.
if [ -d "hfuzz_workspace" ]; then
echo "Importing corpus from hfuzz_workspace..."
Expand All @@ -82,11 +101,14 @@ else
fi

echo "Replaying imported corpus (if found) via tests to generate coverage..."
cargo llvm-cov -j8 --codecov \
--dep-coverage lightning,lightning-invoice,lightning-liquidity,lightning-rapid-gossip-sync,lightning-persister \
--no-default-ignore-filename-regex \
--ignore-filename-regex "(\.cargo/registry|\.rustup/toolchains|/fuzz/)" \
--output-path "$OUTPUT_DIR/fuzz-codecov.json" --tests
generate_coverage_report \
"fuzz-fake-hashes/Cargo.toml" \
"$OUTPUT_DIR/fuzz-fake-hashes-codecov.json" \
"--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz"
generate_coverage_report \
"fuzz-real-hashes/Cargo.toml" \
"$OUTPUT_DIR/fuzz-real-hashes-codecov.json" \
"--cfg=fuzzing --cfg=secp256k1_fuzz"

echo "Fuzz codecov report available at $OUTPUT_DIR/fuzz-codecov.json"
echo "Fuzz codecov reports available at $OUTPUT_DIR/fuzz-fake-hashes-codecov.json and $OUTPUT_DIR/fuzz-real-hashes-codecov.json"
fi
162 changes: 162 additions & 0 deletions fc-crashes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Force-close fuzzer LDK crashes

Minimized crash sequences found by the chanmon_consistency fuzzer with
force-close support. All crashes are `debug_assert` or `panic!` inside
LDK, not in the fuzzer harness. Byte 0 encodes monitor styles (bits
0-2) and channel type (bits 3-4: 0=Legacy, 1=KeyedAnchors).

## 1. channelmonitor.rs:2727 - HTLC input not found in transaction

```
debug_assert!(htlc_input_idx_opt.is_some());
```

When resolving an HTLC spend, the monitor searches for the HTLC
outpoint in the spending transaction's inputs but doesn't find it.
Falls back to index 0 in release mode, which would produce incorrect
tracking.

Minimized (17 bytes):
```
0x40 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xdc 0xde 0xff
```

Byte 0 = 0x40: Legacy channels, no async monitors. The sequence is
mostly 0xff (settlement) repeated, with height advances (0xdc, 0xde)
near the end. This suggests the crash happens during settlement when
processing on-chain HTLC spends after repeated settlement attempts.

## 2. onchaintx.rs:913 - Duplicate claim ID in pending requests

```
debug_assert!(self.pending_claim_requests.get(&claim_id).is_none());
```

The OnchainTxHandler registers a claim event with a claim_id that
already exists in the pending_claim_requests map.

Minimized (10 bytes):
```
0x08 0xd2 0x70 0x70 0x71 0x70 0x10 0x19 0xde 0xff
```

Byte 0 = 0x08: KeyedAnchors channels, no async monitors.
- 0xd2: B force-closes the A-B channel
- 0x70/0x71: disconnect/reconnect peers
- 0x10, 0x19: process messages on nodes A and B
- 0xde: advance chain 200 blocks
- 0xff: settle

B force-closes, peers disconnect and reconnect, messages are exchanged,
then height advances and settlement triggers the duplicate claim.

## 3. onchaintx.rs:1025 - Inconsistent internal maps

```
panic!("Inconsistencies between pending_claim_requests map and claimable_outpoints map");
```

The OnchainTxHandler detects that its `pending_claim_requests` and
`claimable_outpoints` maps are out of sync.

Minimized (14 bytes):
```
0x00 0x3c 0x11 0x19 0xd0 0xde 0xff 0xff 0x19 0x21 0x19 0xde 0x26 0xff
```

Byte 0 = 0x00: Legacy channels, all monitors completed.
- 0x3c: send hop payment A->B->C (1M msat)
- 0x11, 0x19: process messages to commit HTLC on A-B
- 0xd0: A force-closes A-B
- 0xde: advance 200 blocks
- 0xff: settle (first round)
- 0xff: settle again (second round, processes more messages)
- 0x19, 0x21, 0x19: continue processing B and C messages
- 0xde: advance 200 more blocks
- 0x26: process events on node C
- 0xff: settle (third round)

A hop payment partially committed, then A force-closes. Multiple
settlement rounds with continued message processing in between triggers
the internal map inconsistency.

## 4. test_channel_signer.rs:395 - Signing revoked commitment

```
panic!("can only sign the next two unrevoked commitment numbers, revoked={} vs requested={}")
```

The test channel signer is asked to sign an HTLC transaction for a
commitment number that has already been revoked.

Minimized (18 bytes):
```
0x22 0x71 0x71 0x71 0x71 0x71 0x71 0x71 0xff 0xff 0xff 0xff 0xff 0xff 0xde 0xde 0xb5 0xff
```

Byte 0 = 0x22: Legacy channels, async monitors on node B.
- 0x71: disconnect B-C peers (repeated, only first effective)
- 0xff: settle (repeated 6 times)
- 0xde 0xde: advance 400 blocks
- 0xb5: restart node B with alternate monitor state
- 0xff: settle

Async monitors on B with peer disconnection, repeated settlements,
height advances, and a node restart with a different monitor state.
The stale monitor combined with the restart puts B's signer in a state
where it's asked to sign for an already-revoked commitment.

## 5. channelmanager.rs:9836 - Payment blocker not found

```
debug_assert!(found_blocker);
```

During payment processing, the ChannelManager expects to find a
specific blocker entry for an in-flight payment but it's missing.

Minimized (13 bytes):
```
0x00 0x3c 0x11 0x19 0x11 0x1f 0x19 0x21 0x19 0x27 0x27 0xde 0xff
```

Byte 0 = 0x00: Legacy channels, all monitors completed.
- 0x3c: send hop A->B->C (1M msat)
- 0x11, 0x19, 0x11: commit HTLC on A-B
- 0x1f: B processes events (forwards HTLC to C)
- 0x19, 0x21, 0x19: commit HTLC on B-C
- 0x27, 0x27: C processes events (claims payment)
- 0xde: advance 200 blocks
- 0xff: settle

A straightforward A->B->C hop payment that completes normally (C
claims), followed by a height advance and settlement. No force-close
in this sequence, so the height advance before settlement may cause
HTLC timeout processing that conflicts with the claim path.

## 6. channelmanager.rs:19484 - Monitor update ID ordering violation

```
debug_assert!(update.update_id >= pending_update.update_id);
```

A ChannelMonitorUpdate has an update_id that is less than a pending
update's id, violating the expected monotonic ordering.

Minimized (10 bytes):
```
0x84 0x70 0x11 0x19 0x11 0x1f 0xd0 0x11 0x1f 0xba
```

Byte 0 = 0x84: Legacy channels, no async monitors, high bits set
(bits 3-4 = 0, bits 7 and 2 set).
- 0x70: disconnect A-B peers
- 0x11, 0x19, 0x11: process messages (likely reestablish after setup)
- 0x1f: process B events
- 0xd0: A force-closes A-B channel
- 0x11: process A messages
- 0x1f: process B events
- 0xba: restart node B with alternate monitor state

A force-close followed by continued message/event processing and a
node B restart triggers a monitor update with an out-of-order ID.
2 changes: 0 additions & 2 deletions fuzz/.cargo/config.toml

This file was deleted.

21 changes: 1 addition & 20 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,6 @@ version = "0.0.1"
authors = ["Automatically generated"]
publish = false
edition = "2021"
# Because the function is unused it gets dropped before we link lightning, so
# we have to duplicate build.rs here. Note that this is only required for
# fuzzing mode.

[package.metadata]
cargo-fuzz = true

[features]
afl_fuzz = ["afl"]
honggfuzz_fuzz = ["honggfuzz"]
libfuzzer_fuzz = ["libfuzzer-sys"]
stdin_fuzz = []

[dependencies]
lightning = { path = "../lightning", features = ["regex", "_test_utils"] }
Expand All @@ -27,16 +15,9 @@ bech32 = "0.11.0"
bitcoin = { version = "0.32.4", features = ["secp-lowmemory"] }
tokio = { version = "~1.35", default-features = false, features = ["rt-multi-thread"] }

afl = { version = "0.12", optional = true }
honggfuzz = { version = "0.5", optional = true, default-features = false }
libfuzzer-sys = { version = "0.4", optional = true }

[build-dependencies]
cc = "1.0"

# Prevent this from interfering with workspaces
[workspace]
members = ["."]
members = [".", "fuzz-fake-hashes", "fuzz-real-hashes", "runner", "write-seeds"]

[profile.release]
panic = "abort"
Expand Down
Loading
Loading