75962 bc critical pre validation decompression bomb in gossipsub leads to deterministic oom 428 mib msg
Submitted on May 1st 2026 at 23:00:36 UTC by @OadeHack for Audit Comp | Base Azul
Report ID: #75962
Report Type: Blockchain/DLT
Report severity: Critical
Target: https://github.com/base/base/tree/v0.8.0-rc.28
Impacts:
Network not being able to confirm new transactions (total network shutdown)
Causing network processing nodes to process transactions from the mempool beyond set parameters
Description
Summary
The compute_message_id function in libp2p-gossipsub is vulnerable to a pre-validation decompression bomb. Because this hook executes on incoming messages before any rate-limiting, peer scoring, or application-level validation occurs, it leaves the node entirely exposed to attacker-controlled memory allocation. Specifically, an unbounded snap::raw::Decoder::decompress_vec call is followed by an inefficient [domain, data].concat() operation, doubling the memory footprint prior to hashing. As a result, a single 10 MiB message (the MAX_GOSSIP_SIZE limit) deterministically triggers a transient resident-memory spike of ~428 MiB. Given the zero-cost sybil setup and libp2p's concurrent message handling, an attacker can trivially spam these payloads to force Out-Of-Memory (OOM) crashes across the network, including the sequencer.
Details and Walkthrough
The root cause of the vulnerability
The root cause can be seen in the compute_message_id function in (https://github.com/base/base/blob/v0.8.0-rc.28/crates/consensus/gossip/src/config.rs#L104) where there are Two unbounded allocations occur per call:
First unbounded call
This happens at the decompression buffer which can be seen directly from the snappy src code at (https://github.com/BurntSushi/rust-snappy/blob/master/src/decompress.rs#L105). The function imeediately calls decompress_len om the non-validated input and it reads the snappy varint header verbatim.
The header read from input is automatically allocated. Therefore, any length specified by the header is immediately consumed even if the message did not semnd that amount of data. Although snap enforces an internal cap of 2^32 - 1 (~4 GiB), this is not enough as 4gib is way over the intended size that should be allocated as shown further below
Second unbounded call
This happens at [domain, data].concat() which resolves to slice::Concat::concat, which calls Vec::with_capacity(size) followed by extend_from_slice for each input slice, which means committing every byte. With data.len() == decompress_len, this is a second full-size allocation that coexists with allocation A while SHA-256 runs.
Peak commit is therefore ~2× the declared decompressed size.
Production flow to compute_message_id
compute_message_id is registered as the gossipsub message_id_fn (https://github.com/base/base/blob/v0.8.0-rc.28/crates/consensus/gossip/src/config.rs#L93) in the configuration builder used by every Base consensus node:
This builder is invoked during node startup (see crates/client/cli/src/p2p.rs (https://github.com/base/base/blob/v0.8.0-rc.28/crates/client/cli/src/p2p.rs#L463) and crates/consensus/gossip/src/builder.rs (https://github.com/base/base/blob/v0.8.0-rc.28/crates/consensus/gossip/src/builder.rs#L150)), wiring compute_message_id into the libp2p::gossipsub::Config for the node's libp2p Swarm. Every Base node, sequencer, validator, RPC provider, or full node, runs this exact configuration; there is no opt-out.
Once the node is running, the path from "TCP packet" to "compute_message_id executing on attacker bytes" is fixed by libp2p-gossipsub v0.56.0 (the version pinned in Cargo.lock). The relevant call sites are inside the third-party libp2p-gossipsub crate, but the chain is deterministic and observable:
Network IO
The libp2p Swarm in crates/consensus/gossip/src/driver.rs (https://github.com/base/base/blob/v0.8.0-rc.28/crates/consensus/gossip/src/driver.rs#L216) receives raw bytes from any peer that has completed the noise handshake on the node's gossip TCP port. No authentication beyond ephemeral libp2p key exchange is required. Peer identities are self-generated; there is no allow-list.
Protobuf decoding
libp2p-gossipsub's connection handler (https://docs.rs/crate/libp2p-gossipsub/latest/source/src/protocol.rs#272) decodes the wire protobuf stream into libp2p::gossipsub::Message structs. All validation done here are structural and the user message is extracted verbatim and passed to behaviour.rs
message_id_fn invocation
The gossipsub behaviour state machine (https://docs.rs/crate/libp2p-gossipsub/latest/source/src/behaviour.rs#577) calls the registered message_id_fn immediately on every received Message (https://docs.rs/crate/libp2p-gossipsub/latest/source/src/behaviour.rs#604) for duplicate-detection lookup:
This is where compute_message_id runs. The two unbounded allocations described above happen here before any code inside the Base codebase has a chance to inspect the message.
Application yield
Only after the dedup check does libp2p yield Event::Message to the Base consensus driver in crates/consensus/gossip/src/driver.rs, where handle_gossipsub_event finally gets to inspect the payload (https://github.com/base/base/blob/v0.8.0-rc.28/crates/consensus/gossip/src/driver.rs#L333):
Validation feedback
The application reports validity back to libp2p via report_message_validation_result(&id, &src, status). By this point, compute_message_id has already executed against attacker bytes. The validation result governs peer scoring, for future messages from the same peer, but does not affect the cost already paid for the current message.
Impact
This vulnerability allows any network-connected peer to deterministically crash Base consensus nodes, including the sequencer, by publishing crafted gossip messages that force ~478 MiB of transient memory allocation per message before any validation or rate limiting can intervene.
Severity Classification: Critical
Primary Criterion: Network not being able to confirm new transactions (total network shutdown)
The sequencer node participates in the same gossipsub mesh as all other Base consensus nodes. It subscribes to the block topics (/optimism/8453/{0..3}/blocks) as part of normal mesh participation. Gossipsub is a flooding protocol: when any node receives a message with a previously-unseen MessageId, it forwards the message to all its mesh peers after computing the ID. This means a single published bomb message propagates through the entire mesh, basically every connected node invokes compute_message_id on it exactly once.
Attacker publishes bomb messages
The attacker publishes crafted snappy payloads (~10 MiB wire size) to the subscribed topic. Each message has distinct payload bytes, producing a unique MessageId that bypasses the dedup cache. Gossipsub's native mesh forwarding propagates each bomb to every node in the mesh — the attacker does not need to connect to every node individually.
Every node pays the full cost
Each bomb forces a 478 MiB peak resident memory commitment (measured end-to-end in POC 2) on every receiving node. The attacker controls the publication rate. At a rate of N messages before the previous buffers are freed, every node in the mesh simultaneously holds N × 478 MiB of committed memory. On a node with 8 GiB available RAM, approximately 16 concurrent in-flight bombs exhaust memory.
Why this meets the Critical threshold: A single attacker with a single libp2p identity and no privileged access can publish bomb messages that gossipsub's own forwarding mechanism propagates to every node in the mesh — including the sequencer. The per-message cost asymmetry (10 MiB sent by attacker → 478 MiB committed on every receiving node) and the zero-cost publication model (no fees, no staking, no rate limit) allow sustained attack at a rate that keeps the entire fleet, including the sequencer, in a crash-restart loop. During this window, Base cannot confirm new transactions
Floor: High
Even if the triager disputes the sustained-OOM framing (e.g., arguing that operational mitigations like memory limits or restart automation reduce the window), the per-message impact independently satisfies the High criteria:
High — Causing network processing nodes to process transactions from the mempool beyond set parameters
A single 10 MiB wire message deterministically forces 478 MiB of peak resident memory allocation inside compute_message_id. This is a 48× amplification factor that occurs on every received message, on every node, before any application logic can intervene. No reasonable resource parameter set for gossip message processing anticipates half a gigabyte of memory per message ID computation.
High — Shutdown of ≥30% of network processing nodes without brute force actions
The "without brute force" qualifier applies: this is targeted exploitation of a known decompression vulnerability, not brute-force search of an unknown space. Sybil peers publishing known-malicious payloads to a known-vulnerable code path is a directed attack, not a volumetric brute-force. With ~10 sybil identities covering the mesh, every reachable node is affected simultaneously.
Output from one of POC showing the Amplification
Recommendation
To completely eliminate the memory exhaustion vector, the compute_message_id function requires two synergistic modifications. First, the function must read the Snappy header and reject oversized payloads before allocating the decompression buffer. Second, the expensive [domain, data].concat() operation must be replaced with sequential Sha256 updates to hash the data in place, preventing the buffer from being duplicated in memory.
Here is the complete, unified fix implementing both the pre-flight length check and the streaming hash updates:
Proof of Concept
To conclusively demonstrate the vulnerability and its real-world impact, two Proofs of Concept (PoCs) are provided:
PoC 1 (Unit Level - Memory Amplification): A test case added to
crates/consensus/gossip/src/config.rsthat isolates the vulnerable logic. It feeds a crafted Snappy payload directly intocompute_message_idand measures the exact peak resident memory commitment (VmHWM). This proves the deterministic 478 MiB memory spike caused by the function's unbounded allocation.PoC 2 (End-to-End Level - Network Reachability): An integration test added to
crates/consensus/gossip/tests/snappy_bomb_e2e.rsdemonstrating the bug through the live production network stack. TwoSwarminstances using the productionBehaviouranddefault_config()connect over TCP loopback and form a GossipSub mesh on the Base mainnet block topic (/optimism/8453/2/blocks). When the attacker node publishes the crafted bomb, the receiver'scompute_message_idis natively invoked bylibp2p-gossipsubduring its standard duplicate-detection lookup. This proves the vulnerability is reachable purely over the network, entirely independent of the test harness.
POC 1 - Unit test
POC 2
Was this helpful?