Reject RGS snapshots that leave our graph absurdly-sized
What changed, and why it matters
This commit fixes a denial-of-service weakness in Lightning Dev Kit's rapid gossip sync feature. A malicious or compromised RGS server could send an enormous snapshot of Lightning network data, causing a user's node to allocate so much memory that it crashes with an out-of-memory (OOM) error. The patch now rejects snapshots with absurdly high node counts and stops adding new channels once the graph grows about 10 times larger than expected. The issue was reported by Jordan Mecom of Block's Security Team.
Treat this as a security hardening fix and include it in the next maintenance release. Users relying on RGS should upgrade to avoid OOM DoS from a compromised or malicious RGS server. No immediate incident response is required unless an untrusted RGS source is in use.
Security signals we found
OOM/DoS mitigation for semi-trusted gossip data source
Input-size bounds added to RGS snapshot parsing
Explicit security framing in commit message and doc comments
External security report credited (Block's Security Team)
Constants made pub(crate-visible) to support enforcement
Evidence from the diff
The patch hardens lightning-rapid-gossip-sync against oversized RGS snapshots. It exposes CHAN_COUNT_ESTIMATE (63,000) and NODE_COUNT_ESTIMATE (20,000) from NetworkGraph, then uses 10x multiples as caps. During processing it rejects node_id_count > MAX_NODE_COUNT and update_count > MAX_CHANNEL_COUNT, and skips further channel announcements once original_graph_channel_count + i exceeds MAX_CHANNEL_COUNT. This prevents unbounded graph growth and OOM while still allowing normal operation. The commit message explicitly frames this as a DoS mitigation for semi-trusted RGS servers.
Changed components
lightning-rapid-gossip-sync/src/processing.rslightning-rapid-gossip-sync/src/lib.rslightning/src/routing/gossip.rsInspect captured patch +45 / −6
diff --git a/lightning-rapid-gossip-sync/src/lib.rs b/lightning-rapid-gossip-sync/src/lib.rs
index a965375..70a2a79 100644
--- a/lightning-rapid-gossip-sync/src/lib.rs
+++ b/lightning-rapid-gossip-sync/src/lib.rs
@@ -147,6 +147,10 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> {
/// Sync gossip data from a file.
/// Returns the last sync timestamp to be used the next time rapid sync data is queried.
///
+ /// You should consider the gossip data source as semi-trusted. It is generally the case that it
+ /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating
+ /// the graph such that it leads to eventual OOM on the client.
+ ///
/// `network_graph`: The network graph to apply the updates to
///
/// `sync_path`: Path to the file where the gossip update data is located
@@ -166,6 +170,10 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> {
/// Update network graph from binary data.
/// Returns the last sync timestamp to be used the next time rapid sync data is queried.
///
+ /// You should consider the gossip data source as semi-trusted. It is generally the case that it
+ /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating
+ /// the graph such that it leads to eventual OOM on the client.
+ ///
/// `update_data`: `&[u8]` binary stream that comprises the update data
#[cfg(feature = "std")]
pub fn update_network_graph(&self, update_data: &[u8]) -> Result<u32, GraphSyncError> {
@@ -176,6 +184,10 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> {
/// Update network graph from binary data.
/// Returns the last sync timestamp to be used the next time rapid sync data is queried.
///
+ /// You should consider the gossip data source as semi-trusted. It is generally the case that it
+ /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating
+ /// the graph such that it leads to eventual OOM on the client.
+ ///
/// `update_data`: `&[u8]` binary stream that comprises the update data
/// `current_time_unix`: `Option<u64>` optional current timestamp to verify data age
pub fn update_network_graph_no_std(
diff --git a/lightning-rapid-gossip-sync/src/processing.rs b/lightning-rapid-gossip-sync/src/processing.rs
index cce3dc2..45aa1a8 100644
--- a/lightning-rapid-gossip-sync/src/processing.rs
+++ b/lightning-rapid-gossip-sync/src/processing.rs
@@ -9,7 +9,9 @@ use lightning::ln::msgs::{
DecodeError, ErrorAction, LightningError, SocketAddress, UnsignedChannelUpdate,
UnsignedNodeAnnouncement,
};
-use lightning::routing::gossip::{NetworkGraph, NodeAlias, NodeId};
+use lightning::routing::gossip::{
+ NetworkGraph, NodeAlias, NodeId, CHAN_COUNT_ESTIMATE, NODE_COUNT_ESTIMATE,
+};
use lightning::util::logger::Logger;
use lightning::util::ser::{BigSize, FixedLengthReader, Readable};
use lightning::{log_debug, log_given_level, log_gossip, log_trace, log_warn};
@@ -112,17 +114,27 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> {
}
};
+ const MAX_NODE_COUNT: u32 = (NODE_COUNT_ESTIMATE as u32) * 10;
+ const MAX_CHANNEL_COUNT: u64 = (CHAN_COUNT_ESTIMATE as u64) * 10;
+
let node_id_count: u32 = Readable::read(read_cursor)?;
+ if node_id_count > MAX_NODE_COUNT {
+ return Err(LightningError {
+ err: "RGS data contained nonsense number of nodes to update".to_owned(),
+ action: ErrorAction::IgnoreError,
+ }
+ .into());
+ }
let mut node_ids: Vec<NodeId> = Vec::with_capacity(core::cmp::min(
node_id_count,
MAX_INITIAL_NODE_ID_VECTOR_CAPACITY,
) as usize);
-
let network_graph = &self.network_graph;
let mut node_modifications: Vec<UnsignedNodeAnnouncement> = Vec::new();
+ let read_only_network_graph = network_graph.read_only();
+
if parse_node_details {
- let read_only_network_graph = network_graph.read_only();
for _ in 0..node_id_count {
let mut pubkey_bytes = [0u8; 33];
read_cursor.read_exact(&mut pubkey_bytes)?;
@@ -234,9 +246,12 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> {
}
}
+ let original_graph_channel_count = read_only_network_graph.channels().len() as u32;
+ core::mem::drop(read_only_network_graph);
+
let mut previous_scid: u64 = 0;
let announcement_count: u32 = Readable::read(read_cursor)?;
- for _ in 0..announcement_count {
+ for i in 0..announcement_count {
let features = Readable::read(read_cursor)?;
// handle SCID
@@ -281,6 +296,10 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> {
}
}
+ if (original_graph_channel_count as u64) + (i as u64) > MAX_CHANNEL_COUNT {
+ continue;
+ }
+
let announcement_result = network_graph.add_channel_from_partial_announcement(
short_channel_id,
funding_sats,
@@ -326,6 +345,13 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> {
previous_scid = 0;
let update_count: u32 = Readable::read(read_cursor)?;
+ if update_count as u64 > MAX_CHANNEL_COUNT {
+ return Err(LightningError {
+ err: "RGS data contained nonsense number of channels to update".to_owned(),
+ action: ErrorAction::IgnoreError,
+ }
+ .into());
+ }
log_debug!(self.logger, "Processing RGS update from {} with {} nodes, {} channel announcements and {} channel updates.",
latest_seen_timestamp, node_id_count, announcement_count, update_count);
if update_count == 0 {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 71e96ea..6eb583e 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -1765,12 +1765,13 @@ impl<L: Logger> PartialEq for NetworkGraph<L> {
///
/// We over-allocate by a bit because ~15% more is better than the double we get if we're slightly
/// too low.
-const CHAN_COUNT_ESTIMATE: usize = 63_000;
+pub const CHAN_COUNT_ESTIMATE: usize = 63_000;
+
/// In Jan, 2026 there were about 17K nodes
///
/// We over-allocate by a bit because 15% more is better than the double we get if we're slightly
/// too low.
-const NODE_COUNT_ESTIMATE: usize = 20_000;
+pub const NODE_COUNT_ESTIMATE: usize = 20_000;
impl<L: Logger> NetworkGraph<L> {
/// Creates a new, empty, network graph.
Why this scored 74/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.