Skip to content

fix: bound MerkleProof path length on deserialize#3902

Open
iho wants to merge 1 commit into
mimblewimble:stagingfrom
iho:fix/merkle-proof-read-bounds
Open

fix: bound MerkleProof path length on deserialize#3902
iho wants to merge 1 commit into
mimblewimble:stagingfrom
iho:fix/merkle-proof-read-bounds

Conversation

@iho

@iho iho commented Jul 9, 2026

Copy link
Copy Markdown

Summary

MerkleProof::read used the untrusted path_len directly in Vec::with_capacity. A crafted proof with a huge path length could panic (capacity overflow) or attempt a massive allocation — the same class of issue fixed for PMMR segments in #3850 / reported in #3827.

Changes

  • Cap Merkle proof sibling path length at 64 (O(log n) in practice; far above real proofs)
  • Return TooLargeReadErr for oversized path_len before allocating
  • Cap path preallocation to 16 entries
  • Align StreamingReader::read_fixed_bytes with BinReader’s existing 100k single-read limit
  • Tests for oversized and near-u64::MAX path lengths

Test plan

  • cargo test -p grin_core --test merkle_proof
  • cargo test -p grin_core --test segment

MerkleProof::read trusted path_len from the peer/API and called
Vec::with_capacity without a cap, so a huge path_len could panic with
capacity overflow or force a large allocation. Reject path lengths above
64 (well beyond any legitimate O(log n) proof) and cap preallocation.
Also apply the same 100k single-read limit to StreamingReader that
BinReader already enforces.

Related to segment decode bounds (mimblewimble#3827 / mimblewimble#3850).
return Err(ser::Error::TooLargeReadErr);
}
// Cap preallocation even when path_len is within the allowed range.
let mut path = Vec::with_capacity(min(path_len, MERKLE_PROOF_PATH_PREALLOC) as usize);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With path_len capped at 64, the full allocation is at most 2 KiB. Could we allocate path_len directly and drop the second limit and the min import?

Comment thread core/src/ser.rs
/// Read a fixed number of bytes.
fn read_fixed_bytes(&mut self, len: usize) -> Result<Vec<u8>, Error> {
// Match BinReader / BufReader: refuse huge single reads that would OOM or panic.
if len > 100_000 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now the third copy of the 100k reader limit. Could we use one shared constant and add a direct StreamingReader boundary test here?

}

#[test]
fn merkle_proof_read_rejects_huge_path_len() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also cover the accepted side of the boundary: 64 hashes accepted and 65 rejected? The current tests only exercise rejection.


#[test]
fn merkle_proof_read_rejects_capacity_overflow_path_len() {
// Historical bug: path_len near u64::MAX caused Vec::with_capacity to panic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we trim these tests a little? The names already describe the cases, and the historical context is covered by the PR description. A compact bounds test covering 64, 65, and u64::MAX would avoid the duplicated setup.

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.

2 participants