Skip to content

fix(grey): bound count in decode_guarantee to prevent OOM on malformed gossip#754

Open
nianchen1231-netizen wants to merge 1 commit into
jarchain:masterfrom
nianchen1231-netizen:fix-decode-guarantee-oom
Open

fix(grey): bound count in decode_guarantee to prevent OOM on malformed gossip#754
nianchen1231-netizen wants to merge 1 commit into
jarchain:masterfrom
nianchen1231-netizen:fix-decode-guarantee-oom

Conversation

@nianchen1231-netizen

Copy link
Copy Markdown

fix(grey): bound count in decode_guarantee to prevent OOM on malformed gossip

Problem

grey::guarantor::decode_guarantee reads a 16-bit credential count from
untrusted gossip bytes and pre-allocates a Vec of that size before the
per-iteration bounds check runs:

// grey/crates/grey/src/guarantor.rs:286-300
let cred_count = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;

let mut credentials = Vec::with_capacity(cred_count);   // ← unbounded
for _ in 0..cred_count {
    if pos + 2 + 64 > data.len() {
        return None;
    }
    ...
}

Each credential entry is (u16, Ed25519Signature) = 66 bytes on the wire,
so a peer broadcasting a 38-byte message with cred_count = 0xFFFF forces
every receiver subscribed to the guarantees gossipsub topic to allocate
~4.1 MiB before the loop's bounds check rejects the message and the Vec
is dropped. The decode runs before any signature verification — anyone
on the network can do this.

This is the same class of bug fixed by:

Reachability

handle_received_guarantee (same file, line 326) is the gossipsub handler
for the guarantees topic. Decode happens before any auth check, so the
attacker is the network — no validator key required.

Fix

Bound cred_count by the bytes remaining in data before any allocation:

if cred_count > data.len().saturating_sub(pos) / 66 {
    return None;
}
let mut credentials = Vec::with_capacity(cred_count);

Each credential is exactly 66 bytes on the wire (2-byte validator index +
64-byte Ed25519 signature, per the format docstring on decode_guarantee),
so any cred_count larger than (remaining_bytes) / 66 is necessarily
malformed and can be rejected before allocation.

Reproducer (peak-allocation measurement)

A standalone reproducer that vendors the decode logic with and without the
fix, run with cargo run --release on macOS (Apple Silicon):

Adversarial input: 38 bytes, claimed cred_count = 65535

current (vulnerable)    iters=    1  peak_delta=  4_325_310 bytes (4.12 MB)  returned_Some=0
with proposed fix       iters=    1  peak_delta=          0 bytes (0.00 MB)  returned_Some=0

--- amplification at gossip rates (1000 messages/sec scenario) ---
current x1000           iters= 1000  peak_delta=  4_325_310 bytes (4.12 MB)  returned_Some=0
fixed x1000             iters= 1000  peak_delta=          0 bytes (0.00 MB)  returned_Some=0

Per-call peak allocation: 4.12 MiB → 0 bytes for the same input that
both implementations correctly reject. Reproducer source available on
request.

Tests

cargo test -p grey decode_guarantee --release:

test guarantor::tests::proptests::decode_guarantee_rejects_oversized_cred_count ... ok
test guarantor::tests::proptests::decode_guarantee_never_panics ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 102 filtered out

The existing decode_guarantee_never_panics proptest generates random
bytes 0..2048 in length, but uniform-random sampling effectively never
lands on the adversarial shape (a 38-byte input with cred_count = 0xFFFF).
The new decode_guarantee_rejects_oversized_cred_count test exercises
that shape directly as a regression guard.

Future work (not in this PR)

A libfuzzer target fuzz_guarantee_decode is staged locally; it requires
adding grey as a path dependency in grey/fuzz/Cargo.toml. Happy to
fold it into this PR or split it out — let me know preference.


Checklist

  • cargo fmt --check -p grey
  • cargo clippy -p grey --all-targets -- -D warnings
  • cargo test -p grey decode_guarantee --release
  • No public API change
  • No spec change (network-input hardening only)

@github-actions

Copy link
Copy Markdown
Contributor

Genesis Review

Comparison targets:

How to review

Post a comment with the following format (rank from best to worst):

/review
difficulty: <commit1>, <commit2>, ..., <commitN>, currentPR
novelty: <commit1>, <commit2>, ..., <commitN>, currentPR
design: <commit1>, <commit2>, ..., <commitN>, currentPR
verdict: merge

Use the short commit hashes above and currentPR for this PR.
Each line ranks all comparison targets + this PR from best to worst.

To meta-review another reviewer's comment, react with 👍 or 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant