For the complete documentation index, see llms.txt. This page is also available as Markdown.

74531 bc critical gossipsub message id computation snappy decompresses untrusted payloads without a decompressed size cap

Submitted on Apr 23rd 2026 at 09:01:58 UTC by @oxeix for Audit Comp | Base Azul

  • Report ID: #74531

  • Report Type: Blockchain/DLT

  • Report severity: Critical

  • Target: https://github.com/base/base/releases/tag/v0.8.0-rc.15

  • Impacts:

    • Increasing network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours

Description

Brief/Intro

Every Base consensus node derives a gossipsub message-id by Snappy-decompressing the raw message bytes inside compute_message_id, which runs on the hot, pre-validation path for every inbound gossip message from every mesh peer. Because the implementation calls snap::raw::Decoder::decompress_vecwithout first checking snap::raw::decompress_len against MAX_GOSSIP_SIZE, the output buffer is sized from an attacker-controlled 1–5-byte varint in the frame header. In production this lets any peer on the public gossip mesh publish tiny messages (~a few bytes of header) that force every receiving Base node to perform multi-gigabyte heap allocations and bulk SHA-256 hashing per message, yielding a one-to-many CPU/memory amplification that trivially clears the "≥30% increase in node resource consumption" bar and, at the extreme, OOM-kills validator / sequencer / verifier nodes network-wide.

Vulnerability Details

Consider the code:

pub fn default_config_builder() -> ConfigBuilder {
    let mut builder = ConfigBuilder::default();
    builder
        .mesh_n(DEFAULT_MESH_D)
        .mesh_n_low(DEFAULT_MESH_DLO)
        .mesh_n_high(DEFAULT_MESH_DHI)
        .gossip_lazy(DEFAULT_MESH_DLAZY)
        .heartbeat_interval(GOSSIP_HEARTBEAT)
        .fanout_ttl(Duration::from_secs(60))
        .history_length(12)
        .history_gossip(3)
        .flood_publish(false)
        .support_floodsub()
        .max_transmit_size(MAX_GOSSIP_SIZE)
        .duplicate_cache_time(Duration::from_secs(120))
        .connection_handler_queue_len(MAX_OUTBOUND_QUEUE)
        .validation_mode(libp2p::gossipsub::ValidationMode::None)
        .validate_messages()
        .message_id_fn(compute_message_id);

    builder
}

compute_message_id is installed as the gossipsub message_id_fn, so libp2p-gossipsub invokes it for every received message before deduplication (the seen-cache lookup is keyed on the id it returns) and before any semantic validation:

The function itself is:

The crate defines a 10 MiB cap and its doc-comment explicitly claims it bounds decompressed individual messages:

but the only place this constant is wired into the gossipsub config is .max_transmit_size(MAX_GOSSIP_SIZE). max_transmit_size is a libp2p-gossipsub setting that bounds the on-wire, compressed RPC size - not what decompress_vec allocates after the bytes are in memory. Nothing in compute_message_id consults snap::raw::decompress_len or caps the output buffer, so the claim in the doc-comment is not enforced on this path.

The attack itself goes as following:

an attacker-published gossip message carrying only:

declares declared_len ≈ 4 GiB in 5 bytes of header. Every receiving Base node, on seeing this message for dedup, runs:

  1. libp2p-gossipsub transport accepts it (≪ 10 MiB, so max_transmit_size is satisfied).

  2. compute_message_id is invoked.

  3. decompress_vec reads the varint, calls vec![0; ~4 GiB] — a multi-gigabyte zero-initialised heap allocation that touches every page (real RSS, real wall time). Either the body is valid and the full decompressed buffer is then fed through sha256([0x01, 0x00, 0x00, 0x00] || data) (more CPU), or the body is invalid, the allocation is dropped, and the invalid-snappy branch is taken — but the allocation already happened.

Impact Details

Increasing network processing node resource consumption by at least 30%

References

Proof of Concept

Add the test to the gossip/config.rs:

Was this helpful?