What changed, and why it matters
This change fixes a potential denial-of-service weakness in Electrum's Lightning network peer handling. Previously, when Electrum replied to a peer's 'ping' message, it would add the reply to the outgoing socket buffer without waiting to confirm it could actually be sent. A malicious peer that accepts data very slowly (or not at all) could trick Electrum into piling up hundreds of megabytes of unsent replies in memory each hour. The patch makes the reply wait until the socket can drain the data, limiting memory growth.
Apply the patch. For operators, keep Electrum updated and monitor memory use on long-lived Lightning connections. Consider reviewing other message types for similar lack of backpressure.
Security signals we found
memory exhaustion / DoS mitigation
unbounded outbound buffer growth prevented
backpressure added to peer ping/pong handling
commit message explicitly mentions 'memory exhaustion attacks'
no CVE or advisory referenced in commit
Evidence from the diff
In electrum/lnpeer.py, on_ping() previously called self.send_message(‘pong’, byteslen=l), which enqueued the encoded ‘pong’ reply via the transport’s send buffer without backpressure. The patch switches to encode_msg(‘pong’, byteslen=l) followed by await self.transport.send_bytes_and_drain(raw_msg), which waits for the transport/underlying asyncio writer to drain before returning. This prevents an unbounded accumulation of outbound pong messages when the remote peer stops reading. A corresponding mock method was added in tests/lnhelpers.py.
Changed components
electrum/lnpeer.pytests/lnhelpers.pyLightning network peer ping/pong protocol handlingInspect captured patch +6 / −1
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index 75057b4..d7594bf 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -386,7 +386,8 @@ class Peer(Logger, EventListener):
await asyncio.sleep(min_delay - elapsed_since_last)
self._last_ping_recv_time = time.monotonic()
l = payload['num_pong_bytes']
- self.send_message('pong', byteslen=l)
+ raw_msg = encode_msg('pong', byteslen=l)
+ await self.transport.send_bytes_and_drain(raw_msg)
def on_pong(self, payload):
self.pong_event.set()
diff --git a/tests/lnhelpers.py b/tests/lnhelpers.py
index 3e8ce3d..d5207a3 100644
--- a/tests/lnhelpers.py
+++ b/tests/lnhelpers.py
@@ -214,6 +214,10 @@ class PutIntoOthersQueueTransport(MockTransport):
def send_bytes(self, data):
self.other_mock_transport.queue.put_nowait(data)
+ async def send_bytes_and_drain(self, data):
+ self.send_bytes(data)
+
+
def transport_pair(k1, k2, name1, name2):
t1 = PutIntoOthersQueueTransport(k1, name1)
t2 = PutIntoOthersQueueTransport(k2, name2)
Why this scored 55/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.