Increasing network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours
Description
Brief/Intro
Incoming unsafe-block gossip messages are Snappy-decompressed before any decoded-size bound is enforced. The gossipsub message-id callback decompresses the attacker-supplied packet once, and the block envelope decode path decompresses it again. Both use snap::raw::Decoder::decompress_vec(...), which allocates a vector sized from the attacker-controlled Snappy decoded-length header before signature checks, SSZ checks, or block-validity checks.
This allows a remote peer to send a comparatively small packet and force large memory allocations on the victim node before the message is rejected. The attack is off-chain, requires no gas, and is viable against any node that participates in publicly reachable consensus gossip. Repeated packets also consume CPU, because the large Snappy decode and memory-touch path runs again on each packet even after allocator reuse limits further RSS growth.
Vulnerability Details
The gossipsub message-id callback decompresses the full packet body before computing the deduplication hash:
a live local attacker peer connected to a live local victim GossipDriver, observed the victim subscription, and published the packet over a real local libp2p/gossipsub connection
the live local p2p packet-flow PoC measured about 249,648 KiB (~244 MiB) of additional victim RSS from one packet received over the actual listener
the secondary RSS-stress PoC measured about 209,104 KiB (~204 MiB) of additional RSS from one packet on the direct receive path
thirty-two concurrent packets caused about 12,064,928 KiB (~11.5 GiB) of additional RSS
Attacker-side traffic for that 32-packet burst was only:
So a few hundred MiB of attacker traffic forced more than eleven GiB of victim memory pressure before rejection.
Pre-conditions
The target node participates in consensus gossip and accepts inbound p2p peers.
The attacker can establish a p2p connection and send one or more gossip packets.
The packet is routed into the unsafe-block gossip receive path.
This does not require malformed on-chain state, trusted roles, operator mistakes, or gas expenditure.
This reachability assumption is realistic for publicly peered nodes:
the default p2p bind is 0.0.0.0 with external listener ports in base/crates/client/cli/src/p2p.rs;
the official node-operator documentation instructs operators to keep the peer-discovery ports accessible for sync;
the checked-in mainnet and sepolia chain configs include public bootnodes;
on April 29, 2026, direct TCP connection attempts to multiple bootnode endpoints from the checked-in config succeeded, including 3.231.138.188:9200, 184.72.129.189:9200, 18.210.176.114:9200, and 107.21.251.55:9200.
This does not mean every production node is publicly reachable, because operators can still firewall or private-peer their p2p layer. It does mean the attack surface is normal and intended for public consensus participants, not an artificial lab-only posture.
Impact Details
This is a low-cost off-chain resource exhaustion issue.
The attacker pays only for:
a reachable p2p connection,
bandwidth for the crafted packets,
ordinary host/network overhead.
No transaction needs to be mined and no gas is paid.
The victim, however, allocates large decode buffers before rejecting the message. That creates a clean asymmetric DoS condition:
one ~9 MiB packet can add ~204 MiB of RSS,
8 concurrent packets added ~3.0 GiB,
16 concurrent packets added ~5.9 GiB,
32 concurrent packets added ~11.5 GiB.
This maps cleanly to the Medium impact bucket for materially increasing node resource consumption and, on smaller memory budgets, can plausibly lead to process death or cgroup/OOM-killer intervention.
The same receive path also creates sustained CPU pressure under a paced stream. In the live p2p PoC, increasing M13_MESSAGE_COUNT from 1 to 10 drove the victim process to full CPU utilization while the attacker kept publishing new packets.
The PoC should be added to base/crates/consensus/gossip/examples/m13_gossip_live_p2p_poc.rs:
A secondary PoC can also be run, and create the file m13_gossip_snappy_rss_poc.rs under the same directory:
To run the first PoC:
Expected output shape:
The success conditions are:
the victim prints a real local dial_addr=/ip4/127.0.0.1/tcp/.../p2p/...;
the attacker prints connected to victim;
the attacker prints observed victim subscription;
the attacker prints one or more published count=N lines;
the victim prints matching MESSAGE count=N lines up to the configured M13_MESSAGE_COUNT;
the victim prints acceptance=Reject only after processing the packet;
the victim prints a non-trivial delta_rss_kb.
Second PoC
A secondary PoC can also be run, and create the file m13_gossip_snappy_rss_poc.rs under the same directory:
To run the second PoC:
The example prints these lines:
The important conditions are:
compressed_bytes is below MAX_GOSSIP_SIZE = 10485760
declared_decompressed_bytes=201326592 shows that the packet forces a 192 MiB decode buffer
acceptance=Reject shows the packet is rejected only after the large allocation path has already run
delta_rss_kb=209104 shows that one packet alone drove about 204 MiB of victim RSS growth
This runs multiple workers inside the same victim process so that several large Snappy decodes overlap in time.
The important fields are:
concurrency=32 total_messages=32
compressed_bytes=9443332
declared_decompressed_bytes=201326592
peak_rss_kb=12278576
delta_rss_kb=12064928
That verified run drove about 11.5 GiB of additional RSS from a burst of thirty-two ~9.0 MiB packets, all rejected only after the vulnerable decode path ran.