74657 bc critical remote node dos via unbounded snappy decompression in gossip message processing
Submitted on Apr 24th 2026 at 04:14:49 UTC by @nord0x for Audit Comp | Base Azul
Report ID: #74657
Report Type: Blockchain/DLT
Report severity: Critical
Target: https://github.com/base/base/releases/tag/v0.8.0-rc.15
Impacts:
Shutdown of greater than or equal to 30% of network processing nodes without brute force actions, but does not shut down the network
Description
Brief/Intro
The compute_message_id function in Base's gossip layer (crates/consensus/gossip/src/config.rs:104) calls snap::raw::Decoder::decompress_vec() on every inbound P2P gossip message before block-level validation, before signature verification (disabled via ValidationMode::None), and before duplicate cache insertion. Because Base uses MessageAuthenticity::Anonymous, any peer that completes a Noise handshake and joins the gossip mesh can send a single 6 MiB message (within the 10 MiB MAX_GOSSIP_SIZE limit) that decompresses to 128 MiB, causing a +276 MiB peak physical memory spike (verified via VmHWM). Under standard container memory limits (256 MiB), this triggers an immediate OOM-kill by the Linux kernel.
An attacker can sequentially crash any reachable Base node at a measured rate of ~4 seconds per node — connect (2 ms), mesh formation (1.5 s), publish (instant), keepalive (2 s). With public discv5 peer discovery providing a complete node list and a fresh Ed25519 identity costing microseconds (bypassing per-peer scoring), a single attacker can crash 30% of a 200-node network in under 4 minutes.
Vulnerability Details
Root Cause
In crates/consensus/gossip/src/config.rs, the gossipsub config registers a message ID function at line 93:
The compute_message_id function (lines 104-122) decompresses every inbound message using snappy, regardless of claimed decompressed size:
Why This Is Exploitable
No size pre-check. The
snapcrate'sdecompress_vec()reads the snappy varint header to determine decompressed size, then callsvec![0u8; n]to allocate. There is no check onnbefore allocation. For valid-snappy payloads, thevec![0; n]zeroes every byte, committing physical pages (not just virtual reservation).Pre-validation execution. In the libp2p-gossipsub 0.49.4 pipeline,
message_id_fnruns beforeblock_valid(the application handler), beforereport_message_validation_result, and before duplicate cache insertion. Specifically, inbehaviour.rs:1792, the message ID is computed immediately after the data transform, before the message is passed to the application's validation handler.No authentication.
ValidationMode::Nonedisables gossipsub's built-in signature/author validation.MessageAuthenticity::Anonymousmeans messages carry no source PeerId. Any peer that completes a libp2p Noise handshake can publish to the mesh.No effective rate limiting. Base's
ConnectionGateronly restricts outbound dials (can_dial). There are no inbound connection limits. The libp2p swarm has noConnectionLimitsconfigured (builder.rs:215— onlywith_idle_connection_timeoutis set). An attacker can connect inbound without restriction.Peer scoring bypass. Base enables
PeerScoreLevel::Lightby default (client/cli/src/p2p.rs:156). Theinvalid_message_deliveries_weightis -140.4475 and thegraylist_thresholdis -40.0 — meaning one invalid message drops a peer below graylist. However, graylist only blocks future messages from that peer identity. The first bomb always lands. Generating a new Ed25519 keypair costs microseconds, giving the attacker a fresh identity with a clean score for each target.
Attack Model
This is a sequential per-node attack, not a mesh propagation attack. The bomb does not propagate because:
validate_messages()is set (config.rs:92) — gossipsub holds messages until the application callsreport_message_validation_result(Accept).The bomb payload fails
NetworkPayloadEnvelope::decode_vN→ returnsMessageAcceptance::Reject→ gossipsub does not forward.If the bomb triggers OOM-kill, the node dies before forwarding can occur.
Instead, the attacker connects to each target node individually and publishes the bomb directly. The attack cycle is:
Ed25519 keypair generation
<1 ms
TCP + Noise handshake
2.3 ms
Mesh formation (3 heartbeats × 500 ms)
1.5 s
Bomb publish
<1 ms
Delivery keepalive
2 s
Total per node
~3.9 s
Network-Scale Impact (Demonstrated)
Multi-node OOM-kill test: Three separate Base gossip nodes, each running in its own 256 MiB cgroup (simulating standard k8s/Docker container limits), were attacked sequentially from a single attacker process:
Attack economics at scale:
100 nodes
1.6 min
2.0 min
6.5 min
200 nodes
3.3 min
3.9 min
13 min
500 nodes
8.1 min
9.8 min
33 min
Node discovery is trivial: Base uses discv5 (public DHT). The BootStore tracks up to 2,048 peers (crates/consensus/peers/src/store.rs:13). All peers are enumerable by participating in the DHT.
The attack requires no credentials, no tokens, no stake, and no knowledge of the target beyond its gossip port (discoverable via discv5). The attacker binary is ~100 lines of Rust using Base's own default_config_builder().
Secondary Decompression Paths
The same unbounded decompression exists in the envelope decode functions called by the BlockHandler after compute_message_id:
NetworkPayloadEnvelope::decode_v1(crates/common/rpc-types-engine/src/envelope.rs) —decoder.decompress_vec(data)NetworkPayloadEnvelope::decode_v2— sameNetworkPayloadEnvelope::decode_v3— sameNetworkPayloadEnvelope::decode_v4— same
These provide a secondary decompression amplification: even if compute_message_id is patched, the handler's decode step also decompresses without size checks. Both paths must be fixed.
Impact
Severity: High
Impact category: Shutdown of greater than or equal to 30% of network processing nodes without brute force actions.
Justification: A single attacker crashes any reachable containerized Base node in ~4 seconds with one 6 MiB message, no authentication, and a trivially rotatable identity. At 916 nodes/hour sequential throughput, 30% of a 200-node network (60 nodes) is crashable in 3.9 minutes. This is automated sequential exploitation — not computational brute force (no hash cracking, key guessing, or entropy search). All nodes are discoverable via public discv5 DHT.
Concrete impact:
Any containerized Base node (k8s, Docker with standard memory limits) is killed by a single gossip message. On bare-metal hosts without memory limits, the attack causes a transient +276 MiB memory spike that is freed after decompression; repeated attacks can degrade performance but do not crash the node.
Nodes restart via orchestrator (k8s, systemd) — typical 5-30 second restart. During restart, the node is offline and cannot process blocks or serve peers.
Sustained attack keeps nodes oscillating: up → crash → restart → crash. Attacker re-dials after each restart.
Chain liveness degrades as sequencer/verifier nodes go offline.
No on-chain cost to the attacker (pure p2p layer attack).
Link to Proof of Concept
https://gist.github.com/drawrowfly/a5be22a414400df43e78ccb3721936b1
Proof of Concept
I've created convinient to execute POC just in case https://gist.github.com/drawrowfly/a5be22a414400df43e78ccb3721936b1
The PoC consists of two runnable Rust programs — a victim gossip node and an attacker — that demonstrate the complete attack over a real libp2p gossipsub connection. Both use Base's own default_config_builder(), the exact production gossipsub configuration.
Setup
Alternatively, run the automated script which performs all steps:
Attack
The attacker:
Generates a fresh Ed25519 identity (<1 ms)
Connects to the victim via TCP + Noise handshake (2 ms)
Joins the gossipsub mesh (waits 1.5 s for 3 heartbeats)
Compresses 128 MiB of
0xAAbytes with snappy → 6,295,556 bytes (6 MiB), within the 10 MiBMAX_GOSSIP_SIZEPublishes the bomb via
gossipsub::publish()
Attacker output:
Summary
Single-node memory spike
E2E gossip attack
+276 MiB VmHWM from 6 MiB message
OOM-kill (256 MiB cgroup)
E2E + cgroup
Exit 137, oom_kill=1, kernel confirmed
Multi-node attack
3 nodes, 3 cgroups
3/3 OOM-killed in 11.8s
Attack cycle time
Measured end-to-end
~4s per node (916 nodes/hour)
Recommended Fix
Add a size check before decompression in compute_message_id:
Apply the same size check to decode_v1/v2/v3/v4 in crates/common/rpc-types-engine/src/envelope.rs.
Was this helpful?