Skip to content

Bound extract_from_meta's descent into nested rain meta documents - #288

Merged
thedavidmeister merged 1 commit into
mainfrom
2026-08-25-issue-226
Aug 26, 2026
Merged

Bound extract_from_meta's descent into nested rain meta documents#288
thedavidmeister merged 1 commit into
mainfrom
2026-08-25-issue-226

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes #226.

The defect, confirmed

OrderBuilderStateV1::extract_from_meta recursed into any item carrying
KnownMagic::RainMetaDocumentV1 with nothing bounding the descent. Its input is
metaboard bytes, which IDescribedByMetaV1 states come from emitters that are
untrusted and "NOT expected to even be aware of the contract", so the nesting
depth of that input is chosen by whoever wrote the meta. Depth N drives N
frames; the payload grows only ~25 bytes per level, so a few hundred kilobytes
reaches thousands of levels and the process aborts on a stack overflow instead
of returning Err.

Confirmed on this branch by mutating the bound back out (remaining_depth fed
usize::MAX) and rerunning the new tests:

test ..._past_the_nesting_bound ... FAILED
thread '<unknown>' (2272741) has overflowed its stack
fatal runtime error: stack overflow, aborting
... (signal: 6, SIGABRT: process abort signal)

With the bound in place the same 18 tests pass.

What changed

crates/cli/src/meta/types/dotrain/order_builder_state_v1.rs

extract_from_meta is now a thin entry point that seeds a depth budget of
MAX_NESTED_DOCUMENT_DEPTH (32) into a private extract_from_meta_within,
which carries the recursion. Each descent into a nested document spends one unit
of budget through checked_sub(1); spending the last one yields
Error::MetaNestingTooDeep. The public signature, and the behaviour for every
input at or under the bound, are unchanged.

Nesting stays a first-class shape, per #281. #226 is about the unbounded
descent, not the descent itself, so the fix bounds it rather than removing it:
the pre-existing test_extract_from_meta_nested_rain_document and the codec's
test_document_magic_item_carries_a_nested_document both still pass untouched,
and a new test walks 32 full levels of nesting to an instance and finds it.

32 is far above anything the "meta about other meta" graph asks for in practice
(the live shapes are one or two levels) and far below any depth that troubles a
stack.

crates/cli/src/error/mod.rs

New MetaNestingTooDeep(usize) variant. It carries the bound it was measured
against so the rendered message cannot drift from the constant:
"nested meta documents deeper than 32 levels". Display is pinned by a test
alongside the existing Display tests.

Tests

Three new tests in order_builder_state_v1.rs, plus one in error/mod.rs:

  • test_extract_from_meta_at_the_nesting_bound — an instance under exactly
    MAX_NESTED_DOCUMENT_DEPTH levels of nesting is still found. Legitimate
    nesting keeps working right up to the edge.
  • test_extract_from_meta_past_the_nesting_bound — one level further is
    Err(MetaNestingTooDeep(32)), and the variant carries the bound.
  • test_extract_from_meta_deep_nesting_does_not_exhaust_the_stack — the DoS
    itself. 5000 levels, run on a 512 KiB stack that could not hold a fraction of
    the frames the unbounded walk would need, and the call must return an error
    rather than abort. This is the test that aborts the whole binary if the bound
    is ever removed again, which is the point: a stack overflow is not a catchable
    panic.
  • test_display_meta_nesting_too_deep_carries_the_bound.

QA

  • Discriminating tests: test_extract_from_meta_past_the_nesting_bound, test_extract_from_meta_deep_nesting_does_not_exhaust_the_stack — each fails on base (base's behaviour reproduced in-tree by seeding the budget with usize::MAX, which restores the unbounded recursion exactly: the first FAILED, the second killed the whole binary with thread '<unknown>' has overflowed its stack / fatal runtime error: stack overflow, aborting, SIGABRT). The other two cannot fail on base and are pinned by mutation instead: test_extract_from_meta_at_the_nesting_bound PASSES on base — it is the no-regression guard that the fix does not delete legitimate nesting (The document magic as an item magic is a nested document, not a defect #281) — and test_display_meta_nesting_too_deep_carries_the_bound names a variant base does not have.
  • Mutations applied: extract_from_meta budget seed MAX_NESTED_DOCUMENT_DEPTHusize::MAX (base behaviour) → killed by test_extract_from_meta_past_the_nesting_bound (FAILED) and by test_extract_from_meta_deep_nesting_does_not_exhaust_the_stack (SIGABRT); same seed → MAX_NESTED_DOCUMENT_DEPTH - 1 (off-by-one) → killed by test_extract_from_meta_at_the_nesting_bound alone, the other 17 still pass, so the at-bound test is not vacuous; Display arm text "... {} levels""... {} tiers" → killed by test_display_meta_nesting_too_deep_carries_the_bound.
  • Oracle: the issue's stated property plus the two documents it rests on, not the implementation. IDescribedByMetaV1 states meta emitters are untrusted and "NOT expected to even be aware of the contract", which is what makes the depth attacker-chosen; The document magic as an item magic is a nested document, not a defect #281 adjudicated that a RainMetaDocumentV1 magic on an item means a nested document, which fixes what "legitimate nesting" means and therefore that the fix must bound the descent rather than remove it. Expected outcomes derived from that: an instance at any depth within the bound is still found; past the bound the call returns Err; a deep hostile chain returns rather than aborts. The stack-overflow evidence is the runtime's own abort message, not a value read off the code.
  • Category check: issue asks (a) unbounded recursion on attacker-influenceable nesting must not overflow the stack, (b) it should return Err instead, (c) triage note that a depth bound may be the answer, (d) implicit from The document magic as an item magic is a nested document, not a defect #281 and the task framing, legitimate nesting must keep working. Covered a (..._deep_nesting_does_not_exhaust_the_stack, 5000 levels on a 512 KiB stack), b (..._past_the_nesting_bound asserts Err(MetaNestingTooDeep(32))), c (the bound is the fix), d (..._at_the_nesting_bound walks 32 full levels to an instance; pre-existing test_extract_from_meta_nested_rain_document and The document magic as an item magic is a nested document, not a defect #281's meta::tests::test_document_magic_item_carries_a_nested_document untouched and still passing).

Also clean locally: cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, 18 pass in order_builder_state_v1, 7 in error, 2 in meta::tests::test_document_magic_*.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Added protection against excessively deep nested metadata.
    • Metadata nesting is now limited to 32 levels, with a clear error message when the limit is exceeded.
    • Improved handling of deeply nested or potentially malicious input without exhausting system resources.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e4747eca-0512-4a95-8d05-9ae8ac58989e

📥 Commits

Reviewing files that changed from the base of the PR and between c5a1cb0 and 03b7b92.

📒 Files selected for processing (2)
  • crates/cli/src/error/mod.rs
  • crates/cli/src/meta/types/dotrain/order_builder_state_v1.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The CLI adds Error::MetaNestingTooDeep(usize) and a public nesting limit of 32. extract_from_meta tracks remaining depth while traversing nested Rain metadata documents. Tests cover the limit, over-limit input, and 5,000 nested documents.

Changes

Metadata nesting validation

Layer / File(s) Summary
Nesting-depth error contract
crates/cli/src/error/mod.rs
Adds Error::MetaNestingTooDeep(usize), formats the configured limit, and tests the rendered message.
Bounded metadata traversal
crates/cli/src/meta/types/dotrain/order_builder_state_v1.rs
Defines MAX_NESTED_DOCUMENT_DEPTH as 32, applies a depth budget during nested traversal, and tests boundary, over-limit, and deeply nested inputs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 03b7b

The PR bounds nested metadata traversal and returns an error instead of allowing excessive nesting to exhaust the stack, while preserving supported nesting up to the documented limit. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: limiting extract_from_meta recursion into nested Rain metadata documents.
Linked Issues check ✅ Passed The changes address issue #226 by adding a recursion limit, returning MetaNestingTooDeep when the limit is exceeded, preserving supported nesting, and testing deeply nested input without stack exhaust…
Out of Scope Changes check ✅ Passed The error variant, public depth constant, bounded extraction, and related tests directly support the linked issue and PR objective. No unrelated code changes are identified.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 2 files.
Full details: Linked Issues check

Explanation

The changes address issue #226 by adding a recursion limit, returning MetaNestingTooDeep when the limit is exceeded, preserving supported nesting, and testing deeply nested input without stack exhaustion.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-08-25-issue-226

Warning

Some tools did not complete. Review the errors below.

🔧 Clippy (1.97.1)

Clippy execution timed out


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister
thedavidmeister merged commit 8048c2e into main Aug 26, 2026
11 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai assess this PR size classification for the totality of the PR with the following criterias and report it in your comment:

S/M/L PR Classification Guidelines:

This guide helps classify merged pull requests by effort and complexity rather than just line count. The goal is to assess the difficulty and scope of changes after they have been completed.

Small (S)

Characteristics:

  • Simple bug fixes, typos, or minor refactoring
  • Single-purpose changes affecting 1-2 files
  • Documentation updates
  • Configuration tweaks
  • Changes that require minimal context to review

Review Effort: Would have taken 5-10 minutes

Examples:

  • Fix typo in variable name
  • Update README with new instructions
  • Adjust configuration values
  • Simple one-line bug fixes
  • Import statement cleanup

Medium (M)

Characteristics:

  • Feature additions or enhancements
  • Refactoring that touches multiple files but maintains existing behavior
  • Breaking changes with backward compatibility
  • Changes requiring some domain knowledge to review

Review Effort: Would have taken 15-30 minutes

Examples:

  • Add new feature or component
  • Refactor common utility functions
  • Update dependencies with minor breaking changes
  • Add new component with tests
  • Performance optimizations
  • More complex bug fixes

Large (L)

Characteristics:

  • Major feature implementations
  • Breaking changes or API redesigns
  • Complex refactoring across multiple modules
  • New architectural patterns or significant design changes
  • Changes requiring deep context and multiple review rounds

Review Effort: Would have taken 45+ minutes

Examples:

  • Complete new feature with frontend/backend changes
  • Protocol upgrades or breaking changes
  • Major architectural refactoring
  • Framework or technology upgrades

Additional Factors to Consider

When deciding between sizes, also consider:

  • Test coverage impact: More comprehensive test changes lean toward larger classification
  • Risk level: Changes to critical systems bump up a size category
  • Team familiarity: Novel patterns or technologies increase complexity

Notes:

  • the assessment must be for the totality of the PR, that means comparing the base branch to the last commit of the PR
  • the assessment output must be exactly one of: S, M or L (single-line comment) in format of: SIZE={S/M/L}
  • do not include any additional text, only the size classification
  • your assessment comment must not include tips or additional sections
  • do NOT tag me or anyone else on your comment

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.

OrderBuilderStateV1::extract_from_meta recurses unboundedly on nested RainMetaDocumentV1 payloads

1 participant