Skip to content

Regression-test deep-nesting reply decode (depth-safe, no abort) - #147

Open
aterga wants to merge 2 commits into
mainfrom
security/reply-decode-depth-regression
Open

Regression-test deep-nesting reply decode (depth-safe, no abort)#147
aterga wants to merge 2 commits into
mainfrom
security/reply-decode-depth-regression

Conversation

@aterga

@aterga aterga commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

What

Adds decode_reply_rejects_a_deep_opt_chain_instead_of_aborting — the depth counterpart to the existing vec null breadth-bomb test. No production code changes; the behavior is already correct, and this pins it.

Why

A security scan flagged a possible CWE-674 process abort: an attacker-chosen canister can return a compact reply that nests hundreds of thousands of opt levels (type t = opt t, one wire byte per level). candid's decoder, IDLArgs' Display, and the decoded tree's recursive Drop each recurse once per level, so an unbounded chain could overflow the 64 MiB deep-stack thread — an uncatchable abort that drops every concurrent session.

Triage found the finding does not reproduce: REPLY_DECODING_QUOTA bounds breadth (allocation), and depth is bounded by candid's own stacker::remaining_stack() recursion guard (candid/src/utils.rs), which fires on every compound value level (opt / vec / record / variant / tuple) and returns an ordinary decode error before the stack is exhausted — on any stack size. That guard landed in candid PR #417 (2023), well before this crate's 0.10.31 floor, and was hardened in #700 (2026), which is in the locked 0.10.35.

The test

Builds the exact attack — a ~300 KB type-less reply nesting 300,000 opt levels — and asserts decode_reply returns the ordinary "not decodable" error instead of aborting. It regression-guards against a candid downgrade or a change that drops the recursion guard.

test decode_reply_rejects_a_deep_opt_chain_instead_of_aborting ... ok
184 lib tests pass; clippy clean.

Notes

MVP, no stability commitment (https://mcp.internetcomputer.org/terms). Test-only; documents that reply-decode depth-safety depends on candid's recursion guard, so the candid floor must stay at a stacker-guarded version.

🤖 Generated with Claude Code

Adds decode_reply_rejects_a_deep_opt_chain_instead_of_aborting: the DEPTH
counterpart to the existing vec-null breadth-bomb test. A compact reply can
nest hundreds of thousands of `opt` levels (`type t = opt t`, one wire byte
per level); candid's decoder, Display, and the tree's recursive Drop each
recurse per level, so an unbounded chain would overflow the deep-stack
thread and abort the process (dropping every session).

REPLY_DECODING_QUOTA bounds breadth, not depth. Depth is bounded by candid's
own stacker::remaining_stack() recursion guard, which returns an ordinary
decode error before the stack is exhausted, on any stack size. The test pins
that a 300k-deep opt reply degrades to "not decodable" rather than crashing,
guarding against a candid downgrade/regression that would drop the guard.

No production code change — the behavior is already correct; this locks it in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aterga
aterga requested review from a team and a balanced review from Copilot August 20, 2026 11:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a regression test ensuring deeply nested Candid replies fail safely rather than aborting.

Changes:

  • Constructs a 300,000-level recursive opt reply.
  • Exercises type-less reply decoding and checks for graceful failure.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/calls.rs Outdated
… fallback

Review fix: the assertion accepted `on_deep_stack`'s "(could not spawn …)"
fallback, which never reaches candid's recursion guard — so a thread-spawn
failure would pass the test without exercising the behavior it pins. Require
"not decodable" (as the breadth-bomb test does), so the test only passes when
candid's guard actually rejected the deep chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 20, 2026 14:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/calls.rs:1539

  • This new explanation contradicts the security model documented for decode_reply at src/calls.rs:730-737 and for its existing regression test at src/calls.rs:2252-2260, which still state that candid has no depth bound and that the 64 MiB worker stack prevents an attacker-triggered abort. Since this PR establishes that candid's recursion guard is the actual unbounded-depth protection, update those comments too so future changes do not rely on the larger stack as the safety boundary.
    // dropping every session. `REPLY_DECODING_QUOTA` bounds BREADTH, not depth;
    // DEPTH is bounded by candid's own `stacker::remaining_stack()` recursion
    // guard, which returns a decode error before the stack is exhausted (on any
    // stack size). This pins that: a 300k-deep `opt` reply must degrade to the
    // ordinary "not decodable" error, never a crash. It regression-guards against

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.

3 participants