Skip to content
Merged
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
84 changes: 80 additions & 4 deletions pkgs/id/crates/id-core/src/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ const ESC: u8 = 0xFF;
/// so it cannot be confused with an escaped NUL (`0x00 0xFF`).
const TERM: [u8; 2] = [NULL, NULL];

/// Maximum nesting depth [`decode`] will descend into nested tuples.
///
/// Decoded keys can come from peers (iroh-docs entry keys decoded as tag
/// keys), so unbounded recursion is a stack-overflow / process-abort vector:
/// ~20k nested tags in a 40 KB key overflow a 2 MiB thread stack (#2121).
/// The cap matches the peer-CBOR `RECURSION_LIMIT` (64) in `chain.rs`.
/// Inputs nested deeper than this return a normal decode error instead.
///
/// Asymmetry: [`TupleEncoder`] is infallible and does not enforce this cap —
/// `array()` / `build()` can encode tuples nested deeper than [`decode`]
/// will accept. That asymmetry is kept deliberately: changing the encoder's
/// public infallible signature to reject deep nesting would be a breaking
/// API change, and locally produced keys are trusted in a way peer-supplied
/// keys are not. Callers encoding untrusted structure should keep nesting
/// at or below this cap.
pub const MAX_TUPLE_NESTING_DEPTH: usize = 64;

// ============================================================================
// Value type
// ============================================================================
Expand Down Expand Up @@ -166,6 +183,9 @@ impl TupleValue {
///
/// Use `*_prefix()` methods to omit terminators for prefix-match queries.
///
/// Note: the encoder is infallible and does not enforce the decoder's
/// nesting cap — see [`MAX_TUPLE_NESTING_DEPTH`] for the asymmetry.
///
/// # Examples
///
/// ```
Expand Down Expand Up @@ -376,12 +396,13 @@ fn push_f64(buf: &mut Vec<u8>, v: f64) {
/// # Errors
///
/// Returns error for malformed data: truncated fields, invalid UTF-8 in
/// string elements, or unknown type tags.
/// string elements, unknown type tags, or tuple nesting deeper than
/// [`MAX_TUPLE_NESTING_DEPTH`].
pub fn decode(bytes: &[u8]) -> Result<Vec<TupleValue>> {
let mut pos = 0;
let mut out = Vec::new();
while pos < bytes.len() {
match decode_one(bytes, &mut pos, false)? {
match decode_one(bytes, &mut pos, false, 0)? {
Some(v) => out.push(v),
None => bail!("unexpected end-of-tuple marker at top level"),
}
Expand All @@ -390,7 +411,12 @@ pub fn decode(bytes: &[u8]) -> Result<Vec<TupleValue>> {
}

/// Decode a single element. Returns `None` for end-of-nested-tuple marker.
fn decode_one(data: &[u8], pos: &mut usize, nested: bool) -> Result<Option<TupleValue>> {
fn decode_one(
data: &[u8],
pos: &mut usize,
nested: bool,
depth: usize,
) -> Result<Option<TupleValue>> {
ensure!(*pos < data.len(), "unexpected end of tuple data");
let tag = data[*pos];
*pos += 1;
Expand Down Expand Up @@ -426,10 +452,17 @@ fn decode_one(data: &[u8], pos: &mut usize, nested: bool) -> Result<Option<Tuple

// Nested tuple / array
NESTED => {
// Reject before recursing further: `depth` counts the nested
// tuples already enclosing this one, so this element would be
// nesting level depth + 1.
ensure!(
depth < MAX_TUPLE_NESTING_DEPTH,
"tuple nesting exceeds maximum decode depth of {MAX_TUPLE_NESTING_DEPTH}"
);
let mut elements = Vec::new();
loop {
ensure!(*pos < data.len(), "unterminated nested tuple");
match decode_one(data, pos, true)? {
match decode_one(data, pos, true, depth + 1)? {
Some(v) => elements.push(v),
None => break,
}
Expand Down Expand Up @@ -1224,6 +1257,49 @@ mod tests {
assert!(decode(&[BYTES, 0x00, 0x01, 0x00, 0x00]).is_err());
}

// --- Nesting depth limit tests (#2121) ---

/// Bytes for `depth` nested tuples wrapping a single int element:
/// `NESTED` * depth, int 1, `NULL` * depth.
fn nested_key(depth: usize) -> Vec<u8> {
let mut key = vec![NESTED; depth];
key.extend_from_slice(&[POS_INT_1, 0x01]);
key.extend(std::iter::repeat_n(NULL, depth));
key
}

#[test]
fn test_decode_at_nesting_limit_roundtrips() {
let key = nested_key(MAX_TUPLE_NESTING_DEPTH);
let vals = decode(&key).unwrap();
let mut cur = &vals[0];
for _ in 0..MAX_TUPLE_NESTING_DEPTH {
let elems = cur.as_tuple().unwrap();
assert_eq!(elems.len(), 1);
cur = &elems[0];
}
assert_eq!(cur.as_int(), Some(1));
}

#[test]
fn test_decode_one_over_nesting_limit_errors() {
let key = nested_key(MAX_TUPLE_NESTING_DEPTH + 1);
assert!(decode(&key).is_err());
}

#[test]
fn test_decode_hostile_depth_errors_on_small_stack() {
// ~50k nested tags in a 100 KB key aborted the process with a stack
// overflow before the cap (#2121). On a 2 MiB stack the capped
// decoder must return an ordinary Err instead.
let hostile = vec![NESTED; 50_000];
let handle = std::thread::Builder::new()
.stack_size(2 * 1024 * 1024)
.spawn(move || decode(&hostile).is_err())
.unwrap();
assert!(handle.join().unwrap());
}

// --- Accessor tests ---

#[test]
Expand Down
Loading