Skip to content

fuzz: add fuzz target for P2PGossipSync gossip message handling#4532

Open
NishantBansal2003 wants to merge 1 commit intolightningdevkit:mainfrom
NishantBansal2003:fuzz-gossip
Open

fuzz: add fuzz target for P2PGossipSync gossip message handling#4532
NishantBansal2003 wants to merge 1 commit intolightningdevkit:mainfrom
NishantBansal2003:fuzz-gossip

Conversation

@NishantBansal2003
Copy link
Copy Markdown

Added gossip state machine fuzz tests for gossip discovery state handling. In these fuzz tests, we read bytes from the fuzz input to determine actions such as connecting peers, feeding channel announcements, node announcements, channel updates, or query messages. Both valid and malformed messages are generated to exercise error paths.

The reason for adding these fuzz tests is that P2PGossipSync is the ultimate sink for gossip messages received from peers, so there must not be any crashes or unintended behavior in LDK when handling messages from potentially malicious peers.

I only included the states that LDK currently handles. For example, I did not add reply_channel_range or query_scids messages since LDK does not process them at the moment. Similarly, since P2PGossipSync does not handle gossip_timestamp_filter, it is not included in these fuzz tests. Additionally, there is limited benefit in fuzzing gossip_timestamp_filter in LDK, as it is typically processed only once per peer and involves minimal logical processing.

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 1, 2026

I've assigned @joostjager as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Comment on lines +204 to +209

let graph = network_graph.read_only();
let node_id = NodeId::from_pubkey(&peer.node_id);
let node = graph.node(&node_id).unwrap();
let info = node.announcement_info.as_ref().unwrap();
assert_eq!(info.last_update(), timestamp);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Ok branch assertions look up peer.node_id (the original, pre-mutation value), but if the node_id was mutated at line 184-186, the accepted announcement would be stored under ann.contents.node_id (the mutated value). In secp256k1_fuzz mode, signature verification is simplified and could theoretically allow a mutated node_id to pass, causing graph.node(&NodeId::from_pubkey(&peer.node_id)).unwrap() to panic on a node that was never updated.

To be robust regardless of fuzz-mode crypto behavior, this should use the actual message values:

Suggested change
let graph = network_graph.read_only();
let node_id = NodeId::from_pubkey(&peer.node_id);
let node = graph.node(&node_id).unwrap();
let info = node.announcement_info.as_ref().unwrap();
assert_eq!(info.last_update(), timestamp);
let graph = network_graph.read_only();
let node = graph.node(&ann.contents.node_id).unwrap();
let info = node.announcement_info.as_ref().unwrap();
assert_eq!(info.last_update(), timestamp);

Comment on lines +310 to +318
let graph = network_graph.read_only();
let chan = graph.channel(scid).unwrap();
assert_eq!(chan.node_one, NodeId::from_pubkey(&peer1.node_id));
assert_eq!(chan.node_two, NodeId::from_pubkey(&peer2.node_id));

let node1_id = NodeId::from_pubkey(&peer1.node_id);
let node2_id = NodeId::from_pubkey(&peer2.node_id);
assert!(graph.node(&node1_id).is_some());
assert!(graph.node(&node2_id).is_some());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the node announcement case: these assertions use the original peer1.node_id/peer2.node_id, but the node_id_1/node_id_2 fields may have been mutated at lines 238-246. If a mutation passes signature verification in secp256k1_fuzz mode, chan.node_one/chan.node_two would hold the mutated values, causing the assert_eq! to fail spuriously.

Use the actual announcement contents instead:

Suggested change
let graph = network_graph.read_only();
let chan = graph.channel(scid).unwrap();
assert_eq!(chan.node_one, NodeId::from_pubkey(&peer1.node_id));
assert_eq!(chan.node_two, NodeId::from_pubkey(&peer2.node_id));
let node1_id = NodeId::from_pubkey(&peer1.node_id);
let node2_id = NodeId::from_pubkey(&peer2.node_id);
assert!(graph.node(&node1_id).is_some());
assert!(graph.node(&node2_id).is_some());
let graph = network_graph.read_only();
let chan = graph.channel(scid).unwrap();
assert_eq!(chan.node_one, ann.contents.node_id_1);
assert_eq!(chan.node_two, ann.contents.node_id_2);
assert!(graph.node(&ann.contents.node_id_1).is_some());
assert!(graph.node(&ann.contents.node_id_2).is_some());

@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

ldk-claude-review-bot commented Apr 1, 2026

Review Summary

One new issue found (plus two from prior review that are still unresolved):

New inline comment

  • fuzz/src/gossip_discovery.rs:1 — Missing standard licensing header on new file.

Prior comments (still unresolved)

  • fuzz/src/gossip_discovery.rs:209 — Node announcement Ok branch uses peer.node_id instead of ann.contents.node_id (the possibly-mutated value).
  • fuzz/src/gossip_discovery.rs:318 — Channel announcement Ok branch uses peer1.node_id/peer2.node_id instead of ann.contents.node_id_1/ann.contents.node_id_2.

No other bugs, security issues, or logic errors found. The fuzz target structure is sound, the UTXO mock is correct, event assertions for peer_connected and handle_query_channel_range are well-reasoned, and the mutation/validation coverage is thorough.

@@ -0,0 +1,462 @@
//! Test that no series of gossip messages received from peers can result in a crash. We do this
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This file is missing the standard licensing header that other fuzz targets (e.g., full_stack.rs, chanmon_consistency.rs, feature_flags.rs) include at the top:

// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

The doc comment (//! Test that ...) should come after this header.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.11%. Comparing base (450c03a) to head (d29150d).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4532   +/-   ##
=======================================
  Coverage   87.10%   87.11%           
=======================================
  Files         163      163           
  Lines      108740   108740           
  Branches   108740   108740           
=======================================
+ Hits        94723    94730    +7     
+ Misses      11531    11526    -5     
+ Partials     2486     2484    -2     
Flag Coverage Δ
fuzzing 40.35% <ø> (+0.15%) ⬆️
tests 86.20% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks! Have you run any comparisons on the coverage of this vs the router fuzzer? I'm wondering if it makes sense to add a new fuzzer here or focus more CPU on the router fuzzer given it tests very similar codepaths.

@NishantBansal2003
Copy link
Copy Markdown
Author

Cool, thanks! Have you run any comparisons on the coverage of this vs the router fuzzer? I'm wondering if it makes sense to add a new fuzzer here or focus more CPU on the router fuzzer given it tests very similar codepaths.

Coverage wise, the additional benefit is that P2PGossipSync also gets covered, so that's a plus.

My main goal was to cover all announcement messages in the fuzz tests (which the router fuzzer already does), and additionally support pruning cases like fail channel (already covered), fail node, and stale channel, along with handling gossip queries. However, since LDK only supports query channel range, we effectively get only one additional path to fuzz.

One option is to extend the router.rs fuzzer by adding new states (query channel range, fail node, stale channel) and checking invariants at the end of each state also. But for this, we would also need to include P2P gossip sync there.

OR, we can keep a separate fuzz target (gossip_discovery.rs) dedicated purely to gossip related cases (which was my original intention). The downside is some duplication in CPU usage with router.rs.

WDYT? I'm fine with either approach since both will ultimately achieve full coverage of gossip message processing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants