Skip to content

Commit 3c8fdcf

Browse files
authored
mmr: reject oversized forests to prevent num_nodes panics (#857)
* mmr: reject oversized forests to prevent num_nodes panics Forest deserialization previously accepted unbounded usize values. For values > usize::MAX/2 + 1, Forest::num_nodes asserted and would panic when downstream APIs (peaks/open/delta) were called. An attacker could craft a serialized MMR with forest = usize::MAX/2 + 2 to crash consumers after deserialization. This change enforces a maximum forest size at construction and deserialization, removes infallible constructors in favor of fallible ones, makes leaf appends return errors, and adds tests to cover oversize and iterator growth paths so oversized inputs are rejected before any panic can occur. * mmr: cap forest size safely * mmr: simplify append flow and document forest invariant * mmr: trust apply invariants for new_peaks * mmr: hard-cap max leaves to 2^k-1 and simplify xor/or * mmr: remove redundant bounds checks and harden invariants * mmr: centralize mask->node count helper usage
1 parent 62d62a5 commit 3c8fdcf

File tree

9 files changed

+652
-249
lines changed

9 files changed

+652
-249
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
## 0.23.0 (2026-03-11)
66

77
- Replaced `Subtree` internal storage with bitmask layout ([#784](https://github.com/0xMiden/crypto/pull/784)).
8+
- [BREAKING] Enforced a maximum MMR forest size and made MMR/forest constructors and appends fallible to reject oversized inputs ([#857](https://github.com/0xMiden/crypto/pull/857)).
89
- [BREAKING] `PartialMmr::open()` now returns `Option<MmrProof>` instead of `Option<MmrPath>` ([#787](https://github.com/0xMiden/crypto/pull/787)).
910
- [BREAKING] Refactored BLAKE3 to use `Digest<N>` struct, added `Digest192` type alias ([#811](https://github.com/0xMiden/crypto/pull/811)).
1011
- [BREAKING] Added validation to `PartialMmr::from_parts()` and `Deserializable` implementation, added `from_parts_unchecked()` for performance-critical code ([#812](https://github.com/0xMiden/crypto/pull/812)).

miden-crypto/src/merkle/mmr/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub enum MmrError {
1212
InvalidPeaks(String),
1313
#[error("mmr forest is out of bounds: requested {0} > current {1}")]
1414
ForestOutOfBounds(usize, usize),
15+
#[error("mmr forest size {requested} exceeds maximum {max}")]
16+
ForestSizeExceeded { requested: usize, max: usize },
1517
#[error("mmr peak does not match the computed merkle root of the provided authentication path")]
1618
PeakPathMismatch,
1719
#[error("requested peak index is {peak_idx} but the number of peaks is {peaks_len}")]

miden-crypto/src/merkle/mmr/forest.rs

Lines changed: 119 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use core::{
33
ops::{BitAnd, BitOr, BitXor, BitXorAssign},
44
};
55

6-
use super::InOrderIndex;
6+
use super::{InOrderIndex, MmrError};
77
use crate::{
88
Felt,
99
utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
@@ -29,31 +29,61 @@ use crate::{
2929
/// - `Forest(0b1010)` is a forest with two trees: one with 8 leaves (15 nodes), one with 2 leaves
3030
/// (3 nodes).
3131
/// - `Forest(0b1000)` is a forest with one tree, which has 8 leaves (15 nodes).
32+
///
33+
/// Forest sizes are capped at [`Forest::MAX_LEAVES`]. Use [`Forest::new`] or
34+
/// [`Forest::append_leaf`] to enforce the limit.
3235
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
33-
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
36+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
3437
pub struct Forest(usize);
3538

3639
impl Forest {
40+
/// Maximum number of leaves supported by the forest.
41+
///
42+
/// Rationale:
43+
/// - We require `MAX_LEAVES <= usize::MAX / 2 + 1` so `num_nodes()` stays indexable via
44+
/// `usize`.
45+
/// - We choose `usize::MAX / 2` (hard cutoff) rather than `usize::MAX / 2 + 1` so the cap is
46+
/// always of the form `2^k - 1` on all targets.
47+
/// - With that shape, bitwise OR/XOR of valid forest values remains within bounds, so OR/XOR
48+
/// does not need additional overflow protection.
49+
pub const MAX_LEAVES: usize = if (u32::MAX as usize) < (usize::MAX / 2) {
50+
u32::MAX as usize
51+
} else {
52+
usize::MAX / 2
53+
};
54+
3755
/// Creates an empty forest (no trees).
3856
pub const fn empty() -> Self {
3957
Self(0)
4058
}
4159

42-
/// Creates a forest with `num_leaves` leaves.
43-
pub const fn new(num_leaves: usize) -> Self {
44-
Self(num_leaves)
60+
/// Creates a forest with `num_leaves` leaves, returning an error if the value is too large.
61+
pub fn new(num_leaves: usize) -> Result<Self, DeserializationError> {
62+
if !Self::is_valid_size(num_leaves) {
63+
return Err(DeserializationError::InvalidValue(format!(
64+
"forest size {} exceeds maximum {}",
65+
num_leaves,
66+
Self::MAX_LEAVES
67+
)));
68+
}
69+
Ok(Self(num_leaves))
4570
}
4671

4772
/// Creates a forest with a given height.
4873
///
49-
/// This is equivalent to `Forest::new(1 << height)`.
74+
/// This is equivalent to creating a forest with `1 << height` leaves.
5075
///
5176
/// # Panics
5277
///
5378
/// This will panic if `height` is greater than `usize::BITS - 1`.
54-
pub const fn with_height(height: usize) -> Self {
79+
pub fn with_height(height: usize) -> Self {
5580
assert!(height < usize::BITS as usize);
56-
Self::new(1 << height)
81+
Self::new(1 << height).expect("forest height exceeds maximum")
82+
}
83+
84+
/// Returns true if `num_leaves` is within the supported bounds.
85+
pub const fn is_valid_size(num_leaves: usize) -> bool {
86+
num_leaves <= Self::MAX_LEAVES
5787
}
5888

5989
/// Returns true if there are no trees in the forest.
@@ -64,8 +94,15 @@ impl Forest {
6494
/// Adds exactly one more leaf to the capacity of this forest.
6595
///
6696
/// Some smaller trees might be merged together.
67-
pub fn append_leaf(&mut self) {
97+
pub fn append_leaf(&mut self) -> Result<(), MmrError> {
98+
if self.0 >= Self::MAX_LEAVES {
99+
return Err(MmrError::ForestSizeExceeded {
100+
requested: self.0.saturating_add(1),
101+
max: Self::MAX_LEAVES,
102+
});
103+
}
68104
self.0 += 1;
105+
Ok(())
69106
}
70107

71108
/// Returns a count of leaves in the entire underlying forest (MMR).
@@ -75,11 +112,11 @@ impl Forest {
75112

76113
/// Return the total number of nodes of a given forest.
77114
///
78-
/// # Panics
79-
///
80-
/// This will panic if the forest has size greater than `usize::MAX / 2 + 1`.
115+
/// This relies on the `Forest` invariant that `num_leaves() <= Forest::MAX_LEAVES`.
116+
/// The internal assertion is a defensive check and should be unreachable for values created
117+
/// through validated constructors/deserializers.
81118
pub const fn num_nodes(self) -> usize {
82-
assert!(self.0 <= usize::MAX / 2 + 1);
119+
assert!(self.0 <= Self::MAX_LEAVES);
83120
if self.0 <= usize::MAX / 2 {
84121
self.0 * 2 - self.num_trees()
85122
} else {
@@ -197,11 +234,12 @@ impl Forest {
197234
///
198235
/// ```
199236
/// # use miden_crypto::merkle::mmr::Forest;
200-
/// let range = Forest::new(0b0101_0110);
201-
/// assert_eq!(range.trees_larger_than(1), Forest::new(0b0101_0100));
237+
/// let range = Forest::new(0b0101_0110).unwrap();
238+
/// assert_eq!(range.trees_larger_than(1), Forest::new(0b0101_0100).unwrap());
202239
/// ```
203240
pub fn trees_larger_than(self, tree_idx: u32) -> Self {
204-
self & high_bitmask(tree_idx + 1)
241+
let mask = high_bitmask(tree_idx + 1);
242+
Self::new(self.0 & mask).expect("forest size exceeds maximum")
205243
}
206244

207245
/// Creates a new forest with all possible trees smaller than the smallest tree in this
@@ -216,7 +254,7 @@ impl Forest {
216254
/// For a non-panicking version of this function, see [`Forest::all_smaller_trees()`].
217255
pub fn all_smaller_trees_unchecked(self) -> Self {
218256
debug_assert_eq!(self.num_trees(), 1);
219-
Self::new(self.0 - 1)
257+
Self::new(self.0 - 1).expect("forest size exceeds maximum")
220258
}
221259

222260
/// Creates a new forest with all possible trees smaller than the smallest tree in this
@@ -232,9 +270,16 @@ impl Forest {
232270
}
233271

234272
/// Returns a forest with exactly one tree, one size (depth) larger than the current one.
235-
pub fn next_larger_tree(self) -> Self {
273+
///
274+
/// # Errors
275+
/// Returns an error if the resulting forest would exceed [`Forest::MAX_LEAVES`].
276+
pub(crate) fn next_larger_tree(self) -> Result<Self, MmrError> {
236277
debug_assert_eq!(self.num_trees(), 1);
237-
Forest(self.0 << 1)
278+
let value = self.0.saturating_mul(2);
279+
if value > Self::MAX_LEAVES {
280+
return Err(MmrError::ForestSizeExceeded { requested: value, max: Self::MAX_LEAVES });
281+
}
282+
Ok(Forest(value))
238283
}
239284

240285
/// Returns true if the forest contains a single-node tree.
@@ -244,25 +289,29 @@ impl Forest {
244289

245290
/// Add a single-node tree if not already present in the forest.
246291
pub fn with_single_leaf(self) -> Self {
247-
Self::new(self.0 | 1)
292+
// Setting the lowest bit cannot exceed MAX_LEAVES when MAX_LEAVES is 2^k - 1.
293+
Self(self.0 | 1)
248294
}
249295

250296
/// Remove the single-node tree if present in the forest.
251297
pub fn without_single_leaf(self) -> Self {
252-
Self::new(self.0 & (usize::MAX - 1))
298+
// Clearing the lowest bit does not add leaves.
299+
Self(self.0 & (usize::MAX - 1))
253300
}
254301

255302
/// Returns a new forest that does not have the trees that `other` has.
256303
pub fn without_trees(self, other: Forest) -> Self {
257-
self ^ other
304+
// Clearing bits does not add leaves.
305+
Self(self.0 & !other.0)
258306
}
259307

260308
/// Returns index of the forest tree for a specified leaf index.
261309
pub fn tree_index(&self, leaf_idx: usize) -> usize {
262310
let root = self
263311
.leaf_to_corresponding_tree(leaf_idx)
264312
.expect("position must be part of the forest");
265-
let smaller_tree_mask = Self::new(2_usize.pow(root) - 1);
313+
let smaller_tree_mask =
314+
Self::new(2_usize.pow(root) - 1).expect("forest size exceeds maximum");
266315
let num_smaller_trees = (*self & smaller_tree_mask).num_trees();
267316
self.num_trees() - num_smaller_trees - 1
268317
}
@@ -350,8 +399,7 @@ impl Forest {
350399
/// Given a leaf index in the current forest, return the tree number responsible for the
351400
/// leaf.
352401
///
353-
/// Note:
354-
/// The result is a tree position `p`, it has the following interpretations:
402+
/// The result is a tree position `p`:
355403
/// - `p+1` is the depth of the tree.
356404
/// - Because the root element is not part of the proof, `p` is the length of the authentication
357405
/// path.
@@ -396,8 +444,8 @@ impl Forest {
396444
/// the leaf belongs.
397445
pub(super) fn leaf_relative_position(self, leaf_idx: usize) -> Option<usize> {
398446
let tree_idx = self.leaf_to_corresponding_tree(leaf_idx)?;
399-
let forest_before = self & high_bitmask(tree_idx + 1);
400-
Some(leaf_idx - forest_before.0)
447+
let mask = high_bitmask(tree_idx + 1);
448+
Some(leaf_idx - (self.0 & mask))
401449
}
402450
}
403451

@@ -417,23 +465,27 @@ impl BitAnd<Forest> for Forest {
417465
type Output = Self;
418466

419467
fn bitand(self, rhs: Self) -> Self::Output {
420-
Self::new(self.0 & rhs.0)
468+
Self::new(self.0 & rhs.0).expect("forest size exceeds maximum")
421469
}
422470
}
423471

472+
// Compile-time invariant: MAX_LEAVES must be exactly 2^k - 1.
473+
const _: () =
474+
assert!(Forest::MAX_LEAVES != 0 && (Forest::MAX_LEAVES & (Forest::MAX_LEAVES + 1)) == 0);
475+
424476
impl BitOr<Forest> for Forest {
425477
type Output = Self;
426478

427479
fn bitor(self, rhs: Self) -> Self::Output {
428-
Self::new(self.0 | rhs.0)
480+
Self(self.0 | rhs.0)
429481
}
430482
}
431483

432484
impl BitXor<Forest> for Forest {
433485
type Output = Self;
434486

435487
fn bitxor(self, rhs: Self) -> Self::Output {
436-
Self::new(self.0 ^ rhs.0)
488+
Self(self.0 ^ rhs.0)
437489
}
438490
}
439491

@@ -443,9 +495,29 @@ impl BitXorAssign<Forest> for Forest {
443495
}
444496
}
445497

446-
impl From<Felt> for Forest {
447-
fn from(value: Felt) -> Self {
448-
Self::new(value.as_canonical_u64() as usize)
498+
impl TryFrom<Felt> for Forest {
499+
type Error = MmrError;
500+
501+
fn try_from(value: Felt) -> Result<Self, Self::Error> {
502+
let value = usize::try_from(value.as_canonical_u64()).map_err(|_| {
503+
MmrError::ForestSizeExceeded {
504+
requested: usize::MAX,
505+
max: Self::MAX_LEAVES,
506+
}
507+
})?;
508+
if value > Self::MAX_LEAVES {
509+
return Err(MmrError::ForestSizeExceeded { requested: value, max: Self::MAX_LEAVES });
510+
}
511+
Ok(Self(value))
512+
}
513+
}
514+
515+
pub(crate) fn largest_tree_from_mask(mask: usize) -> Forest {
516+
if mask == 0 {
517+
Forest::empty()
518+
} else {
519+
let bit = mask.ilog2();
520+
Forest::new(1usize << bit).expect("forest size exceeds maximum")
449521
}
450522
}
451523

@@ -456,12 +528,8 @@ impl From<Forest> for Felt {
456528
}
457529

458530
/// Return a bitmask for the bits including and above the given position.
459-
pub(crate) const fn high_bitmask(bit: u32) -> Forest {
460-
if bit > usize::BITS - 1 {
461-
Forest::empty()
462-
} else {
463-
Forest::new(usize::MAX << bit)
464-
}
531+
pub(crate) fn high_bitmask(bit: u32) -> usize {
532+
if bit > usize::BITS - 1 { 0 } else { usize::MAX << bit }
465533
}
466534

467535
// SERIALIZATION
@@ -476,7 +544,18 @@ impl Serializable for Forest {
476544
impl Deserializable for Forest {
477545
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
478546
let value = source.read_usize()?;
479-
Ok(Self::new(value))
547+
Self::new(value)
548+
}
549+
}
550+
551+
#[cfg(feature = "serde")]
552+
impl<'de> serde::Deserialize<'de> for Forest {
553+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
554+
where
555+
D: serde::Deserializer<'de>,
556+
{
557+
let value = usize::deserialize(deserializer)?;
558+
Self::new(value).map_err(serde::de::Error::custom)
480559
}
481560
}
482561

0 commit comments

Comments
 (0)