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

75979 bc medium compute message id decompresses every incoming gossip message via snap raw decoder decompress vec without enforcing the documented decompressed size cap allowing per messa

Submitted on May 2nd 2026 at 02:03:48 UTC by @Another for Audit Comp | Base Azul

  • Report ID: #75979

  • Report Type: Blockchain/DLT

  • Report severity: Medium

  • Target: https://github.com/base/base/tree/v0.8.0-rc.28

  • Impacts:

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

Description

Brief/Intro

The MAX_GOSSIP_SIZE constant is documented as covering both compressed and decompressed message sizes. In practice, the code only enforces it on the compressed input via libp2p max_transmit_size. The Snappy decompressors in the gossip path call decompress_vec directly, which calls vec![0; decompress_len(input)?] and reads the declared length from the message header — capped only at u32::MAX (~4 GiB) by the snap crate.

Vulnerability Details

// base/crates/consensus/gossip/src/config.rs:14-15
/// The maximum gossip size.
/// Limits the total size of gossip RPC containers as well as decompressed individual messages.
pub const MAX_GOSSIP_SIZE: usize = 10 * (1 << 20);

// base/crates/consensus/gossip/src/config.rs:104-122
/// Computes the [`MessageId`] of a [`gossipsub`] message.
fn compute_message_id(msg: &Message) -> MessageId {
    let mut decoder = Decoder::new();
    let id = decoder.decompress_vec(&msg.data).map_or_else(
        |_| {
            warn!(target: "cfg", "Failed to decompress message, using invalid snappy");
            let domain_invalid_snappy: Vec<u8> = vec![0x0, 0x0, 0x0, 0x0];
            sha256([domain_invalid_snappy.as_slice(), msg.data.as_slice()].concat().as_slice())
                [..20]
                .to_vec()
        },
        |data| {// data may be GiB-sized
            let domain_valid_snappy: Vec<u8> = vec![0x1, 0x0, 0x0, 0x0];
            sha256([domain_valid_snappy.as_slice(), data.as_slice()].concat().as_slice())[..20]
                .to_vec()
        },
    );

    MessageId(id)
}

The doc claims a decompressed-size limit; the implementation does not enforce one. compute_message_id is invoked by libp2p for every incoming gossip message before any application-level validation runs.

Same patterns exist in multiple files:

  • base/crates/consensus/gossip/src/config.rs:14-15 — doc claims decompressed-size limit

  • base/crates/consensus/gossip/src/config.rs:104-122 — compute_message_id with unbounded decompress_vec

  • base/crates/common/rpc-types-engine/src/envelope.rs:255-275 — decode_v1 with same pattern

  • base/crates/common/rpc-types-engine/src/envelope.rs:299-319 — decode_v2

  • base/crates/common/rpc-types-engine/src/envelope.rs:343-365 — decode_v3

  • base/crates/common/rpc-types-engine/src/envelope.rs:399-420 — decode_v4

Impact Details

On default Linux with overcommit, each per-message vec![0; 4 GiB] is virtual address space (mmap MAP_ANONYMOUS) that does not commit physical RAM until written. Snappy then writes up to ~10 MiB × 65 ≈ 650 MiB of physical pages before the decoder errors on input exhaustion (Snappy max copy expansion is ~65×). 650 MiB transient per message, multiplied across the seen-message cache window, sustains a 30%+ increase in RSS on a typical Base validator (16-32 GB RAM). On strict-overcommit systems (vm.overcommit_memory=2) or cgroup-bounded containers, allocation failure aborts the affected task. The compute_message_id callback runs on the libp2p gossipsub task; abort terminates the gossip subsystem.

References

https://github.com/base/base/blob/e3467a2048881213b56739a54a876efb9c6ea103/crates/consensus/gossip/src/config.rs#L88

https://github.com/base/base/blob/e3467a2048881213b56739a54a876efb9c6ea103/crates/consensus/gossip/src/config.rs#L104

https://github.com/base/base/blob/e3467a2048881213b56739a54a876efb9c6ea103/crates/common/rpc-types-engine/src/envelope.rs#L255-L425

Proof of Concept

Place this inside the existing #[cfg(test)] mod tests { ... } block in base/crates/consensus/gossip/src/config.rs, alongside test_compute_message_id_invalid_snappy / _valid_snappy. It must live in-tree because compute_message_id is private (it is referenced via .message_id_fn(compute_message_id) from default_config_builder, but never re-exported from lib.rs).

Why the body is [0x00, 0x42]

Snappy raw format expects the varint header followed by element tags. 0x00 is a literal tag with (length-1) = 0, so it announces 1 literal byte (0x42). After consuming this, Snappy expects more elements until declared bytes are produced; it errors before reaching that, but only after the buffer has been allocated. This deliberately exercises the allocation-precedes-decode-error path.

Why 100 MiB rather than 4 GiB

The PoC's purpose is to demonstrate the bypass, not to OOM the test runner. 100 MiB is unambiguously above the 10 MiB cap and is safe to allocate as a virtual reservation (Snappy writes one byte then errors, so physical commit stays at one page). Bumping declared to u32::MAX - 1 exercises the same code path with a ~4 GiB virtual reservation; on overcommit-strict hosts (vm.overcommit_memory=2) or cgroup-bounded containers, it aborts the gossipsub task instead.

Required workspace config workaround

The repo's base/.cargo/config.toml pins a non-default linker on macOS:

If lld is not installed (brew install lld), cargo will fail at the link step before any test can run, with:

Cargo merges target rustflags rather than replacing them, so neither RUSTFLAGS=, CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS=, nor cargo --config 'target.aarch64-apple-darwin.rustflags=[]' will neutralize the workspace setting. The two working options are:

1

Install lld — brew install lld (one-time setup the workspace expects).

2

Temporarily comment the line in base/.cargo/config.toml for the duration of the run, then restore it:

CI Linux runners use mold, which is unaffected.

Build & run

From the base/ workspace root:

Cold compile takes ~9 minutes (libp2p, openssl-vendored, alloy, etc.). Subsequent runs reuse target/.

Recorded run logs

Tail of the cargo build & test output from the actual run on aarch64-apple-darwin:

Test execution time of 0.01s confirms the 100 MiB vec![0; n] is satisfied by virtual address-space reservation alone — no measurable physical commit before Snappy errors and the buffer is dropped.

Was this helpful?