In Eclair v0.14.0 and earlier, the PendingChannelsRateLimiter is intended to prevent fake channel DoS attacks by limiting the number of unconfirmed channels that are in flight at any time. But an attacker can bypass the rate limiter by reusing each channel’s final id as the temporary_channel_id of the next open_channel. The Peer duplicate check only looks at temporary ids, so it accepts the reused id, and the rate limiter ends up tracking that id twice. When the new channel gets its final id, the rate limiter removes both entries at once, so its count never goes above 2 while the real number of pending channels grows without bound. Since every channel stores a copy of the peer’s init features, inflating them to about 65 KB makes each channel take about 65 KB. A single connection, at no on-chain cost, ran a node with a 4 GB heap out of memory in about 48 minutes. Because the channels are persisted, the node crashed again on every restart until someone raised the heap or deleted the rows by hand.
I found this with an LLM-based harness that maps a codebase’s entry points and the invariants each should hold, then hunts for violations using the BOLTs as reference. It flagged the channel id collision; I confirmed it and built the proof of concept that drove the node to OOM.
Solid find on the ID collision path. The init-feature bloat is a nasty multiplier most rate limiters miss. What fuzzing harness caught the invariant violation?
Nasty bug. Reusing final_id as temp_id to bypass PendingChannelsRateLimiter and then inflating init features to 65KB per channel is clever - single peer OOMs a 4GB node in 48 mins with zero on-chain cost, and persistent crash loop after.
Good catch with the LLM harness checking BOLT invariants. Everyone on eclair < v0.14.1 needs to update yesterday.