Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-0003: Maximum Nesting Depth Limit (256)

  • Status: Accepted
  • Date: 2026-02-06
  • Applies to: tealeaf-core (parser, binary reader)

Context

TeaLeaf accepts untrusted input in production — user-supplied .tl files, .tlbx binaries from external sources, and JSON strings from APIs. Recursive data structures (arrays, objects, maps, tagged values) create call stacks proportional to input nesting depth. Without a limit, an attacker can craft a payload like key: [[[[... with thousands of levels, causing a stack overflow and process termination.

Two constants enforce the limit:

ConstantFileValue
MAX_PARSE_DEPTHparser.rs256
MAX_DECODE_DEPTHreader.rs256

Both constants are set to the same value to ensure text-binary parity: any document that parses successfully from .tl text can also round-trip through .tlbx binary without hitting a different depth ceiling.

The limit is checked at every recursive entry point:

  • Parser: parse_value() — arrays, objects, maps, tuples, tagged values
  • Reader: decode_value(), decode_array(), decode_object(), decode_struct(), decode_struct_array(), decode_map()

When exceeded, both paths return a descriptive error rather than panicking or overflowing the stack.

Ecosystem Comparison

Parser / LibraryDefault Max DepthConfigurable?
TeaLeaf256No (compile-time constant)
serde_json (Rust)128Yes (disable_recursion_limit)
serde_yaml (Rust)128No
System.Text.Json (.NET)64Yes (MaxDepth)
ASP.NET Core (default)32Yes
Jackson (Java)1000 (v2), 500 (v3)Yes
Go encoding/json10,000No
Python json (stdlib)~1,000 (interpreter limit)Via sys.setrecursionlimit
Protocol Buffers (Java/C++)100Yes
Protocol Buffers (Go)10,000Yes
rmp-serde (MessagePack)1,024Yes
CBOR (ciborium, Rust)128Yes
toml (Rust)NoneNo (vulnerable to stack overflow)

Observations

  • Conservative defaults are trending down. Jackson reduced from 1,000 to 500 in v3. .NET defaults to 64. Protocol Buffers targets 100.
  • 128 is the most common Rust ecosystem default (serde_json, serde_yaml, ciborium).
  • No production data format needs > 100 levels. Deeply nested structures indicate either machine-generated intermediate representations or adversarial input.
  • Formats without limits have CVEs. The toml crate’s lack of depth limiting is tracked as an open issue. Python’s reliance on interpreter limits has caused production crashes.

Decision

Set MAX_PARSE_DEPTH and MAX_DECODE_DEPTH to 256.

Why 256 over 128?

TeaLeaf schemas add implicit nesting. A @struct with an array of @struct-typed objects creates 3 levels of nesting (object → array → object) for what the user perceives as one level of structure. With schema compositions, 128 could be reached in complex but legitimate documents. 256 provides a 2x margin above the Rust ecosystem default while remaining well within safe stack bounds.

Why not configurable?

  • Simplicity. A compile-time constant is zero-cost at runtime (no configuration plumbing, no state to manage).
  • Consistent behavior. All TeaLeaf implementations (Rust, FFI, .NET) enforce the same limit. A configurable limit would require coordination across language boundaries.
  • 256 is generous enough. No known use case requires deeper nesting. If a legitimate need arises, the constant can be bumped in a patch release without breaking any public API.

Stack safety margin

On x86-64 Linux with the default 8 MB stack, each recursive call uses roughly 200–400 bytes of stack frame. At 256 depth, the worst case is ~100 KB — well under 2% of the available stack. This leaves ample room for the caller’s own stack frames and for platforms with smaller stacks (e.g., 1 MB thread stacks).

Test Coverage

TestLocationWhat it verifies
test_parse_depth_256_succeedsparser.rs200-level nesting parses successfully
test_fuzz_deeply_nested_arrays_no_stack_overflowparser.rs500-level nesting returns error (no crash)
parse_deep_nesting_okadversarial.rs7-level nesting succeeds in adversarial harness
fuzz_structured depth=3fuzz_structured.rsStructure-aware fuzzer bounds depth to 3
canonical/large_data.tlCanonical suiteDeep nesting fixture round-trips correctly

Consequences

Positive

  • Stack overflow protection. Malicious or malformed input with extreme nesting is rejected with a clear error message instead of crashing the process.
  • Text-binary parity. The same limit in parser and reader means any document that parses from text will also decode from binary, and vice versa.
  • Predictable resource usage. Callers can reason about maximum stack consumption without inspecting input.

Negative

  • Theoretical limitation. Documents with more than 256 levels of nesting are rejected. In practice, no known data format use case requires this depth.
  • Not configurable. Users who need deeper nesting must rebuild from source with a modified constant. This is an intentional trade-off for simplicity.

Neutral

  • No performance cost. The depth check is a single integer comparison per recursive call — unmeasurable relative to the cost of decoding a value.