Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion core/src/core/merkle_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::core::hash::Hash;
use crate::core::pmmr;
use crate::ser;
use crate::ser::{PMMRIndexHashable, Readable, Reader, Writeable, Writer};
use std::cmp::min;
use util::ToHex;

/// Merkle proof errors.
Expand All @@ -27,6 +28,15 @@ pub enum MerkleProofError {
RootMismatch,
}

/// Maximum sibling path length accepted when deserializing a Merkle proof.
///
/// Path length is O(log n) for an MMR of size n (plus a few peak hashes).
/// 64 is far larger than any legitimate proof and prevents a malicious
/// `path_len` from causing a capacity overflow / huge allocation.
pub const MAX_MERKLE_PROOF_PATH: u64 = 64;

const MERKLE_PROOF_PATH_PREALLOC: u64 = 16;

/// A Merkle proof that proves a particular element exists in the MMR.
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, PartialOrd, Ord)]
pub struct MerkleProof {
Expand All @@ -50,7 +60,11 @@ impl Readable for MerkleProof {
fn read<R: Reader>(reader: &mut R) -> Result<MerkleProof, ser::Error> {
let mmr_size = reader.read_u64()?;
let path_len = reader.read_u64()?;
let mut path = Vec::with_capacity(path_len as usize);
if path_len > MAX_MERKLE_PROOF_PATH {
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?

for _ in 0..path_len {
let hash = Hash::read(reader)?;
path.push(hash);
Expand Down
4 changes: 4 additions & 0 deletions core/src/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,10 @@ impl<'a> Reader for StreamingReader<'a> {

/// 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?

return Err(Error::TooLargeReadErr);
}
let mut buf = vec![0u8; len];
self.stream.read_exact(&mut buf)?;
self.total_bytes_read += len as u64;
Expand Down
39 changes: 37 additions & 2 deletions core/tests/merkle_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,54 @@

mod common;

use self::core::core::merkle_proof::MerkleProof;
use self::core::core::merkle_proof::{MerkleProof, MAX_MERKLE_PROOF_PATH};
use self::core::core::pmmr::{ReadablePMMR, VecBackend, PMMR};
use self::core::ser::{self, PMMRIndexHashable};
use self::core::ser::{self, DeserializationMode, PMMRIndexHashable, ProtocolVersion};
use crate::common::TestElem;
use grin_core as core;

fn push_u64(buf: &mut Vec<u8>, v: u64) {
buf.extend_from_slice(&v.to_be_bytes());
}

#[test]
fn empty_merkle_proof() {
let proof = MerkleProof::empty();
assert_eq!(proof.path, vec![]);
assert_eq!(proof.mmr_size, 0);
}

#[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.

// mmr_size=0, path_len just over the allowed maximum → TooLargeReadErr
// before any allocation of path entries.
let mut bytes = vec![];
push_u64(&mut bytes, 0);
push_u64(&mut bytes, MAX_MERKLE_PROOF_PATH + 1);

let res: Result<MerkleProof, _> = ser::deserialize(
&mut &bytes[..],
ProtocolVersion(1),
DeserializationMode::default(),
);
assert_eq!(res.err(), Some(ser::Error::TooLargeReadErr));
}

#[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.

let mut bytes = vec![];
push_u64(&mut bytes, 1);
push_u64(&mut bytes, u64::MAX);

let res: Result<MerkleProof, _> = ser::deserialize(
&mut &bytes[..],
ProtocolVersion(1),
DeserializationMode::default(),
);
assert_eq!(res.err(), Some(ser::Error::TooLargeReadErr));
}

#[test]
fn merkle_proof_ser_deser() {
let mut ba = VecBackend::new();
Expand Down
Loading