Skip to content

fix(wire): recursive Serialize/Drop on FuncSpace lack the #700 de-recursion #1056

Description

@dekobon

Summary

Issues #700 / #709 systematically converted every AST traversal to an
explicit work stack, with regression tests pinned to small (256-512 KiB)
stacks. That work did not cover the recursive types, which still recurse
one frame per nesting level with no depth cap.

Sites

  • src/wire.rs:411-423impl From<&spaces::FuncSpace> for wire::FuncSpace
    is directly recursive:
    spaces: f.spaces.iter().map(FuncSpace::from).collect(),
  • Derived Serialize on FuncSpace { spaces: Vec<FuncSpace> }
    (src/spaces.rs:207) and on wire::Ops (src/wire.rs:425).
  • Compiler-generated Drop glue for FuncSpace and AstNode
    (src/ast.rs:151) — no hand-written iterative Drop.

Nesting depth is attacker-controlled via nested functions / classes /
closures, which are ordinary legal constructs in every supported language.

Deserialize is protected by serde_json's built-in 128-depth recursion
limit. Serialize has no such guard.

Why file this now

Fixing #1052 may unmask it. Today the depth needed to overflow the stack
cannot be reached, because the O(leaves x depth^2) cost in
src/metrics/tokens.rs makes such input time out long before serialization
runs. Once the metric cost is linear, deep inputs will actually complete and
reach these recursive paths. The DoS is currently masking the stack hazard.

This was not demonstrated to overflow — the attempt timed out first, exactly
as described above. Treat it as a real but currently-unreachable hazard, and
re-test after #1052 lands.

Precedent in-tree

The project already knows the shape of this problem and works around it
elsewhere: src/output/dump_metrics.rs:368-375 and
src/output/dump_ops.rs:245-252 flatten trees before drop specifically to
avoid stack overflow, and big-code-analysis-cli/src/markdown_report.rs:1489-1493
resorts to std::mem::forget past depth 50,000. The production
FuncSpace / AstNode paths do none of this.

Suggested direction


Scope correction (see comments)

The Drop half of this title is not implicated. bca check builds and
drops a 10 000-deep FuncSpace chain without overflowing; bca metrics -O json aborts at 1 000. The bug is the recursive Serialize path —
wire.rs's From<&spaces::FuncSpace> and the derived Serialize on
FuncSpace { spaces: Vec<FuncSpace> }.

Reproducer: 1 000 nested fns (~11 KB) -> fatal runtime error: stack overflow, aborting, exit 134. Scales with FuncSpace nesting, not AST
depth
— nested parens at depth 200 000 (400 KB) finish in 272 ms.

SIGABRT, not a catchable panic, so bca-web's spawn_blocking panic
handling does not contain it: the whole process dies with every in-flight
request.

The three existing small-stack tests from #700/#709 all pass — they cover the
dump walk and construct FuncSpace values directly, so nothing exercises
serialization.


Fix in review — #1082

Fixed in 3fd01c70, workspace bumped to 2.1.0 in 7b8f3fd6, both on
fix/1056-recursive-serialize-drop. Open for review as #1082; this
issue closes on merge. See the
findings comment for measurements and
verification.

  • Iterative Drop for FuncSpace and AstNodeand for Ops,
    wire::FuncSpace, wire::Ops. The scope-correction note above was
    wrong to write Drop off: re-measured, it aborts at ~16 000 levels
    in a debug build and ~65 000 in release, which fits inside
    bca-web's 4 MiB body cap.
  • A depth cap for the wire conversion and the Serialize impls.
    The conversion (From<&spaces::FuncSpace>) needed no cap — it is
    now iterative, and was in fact the site of the reported
    1 000-level abort. Serialize cannot be de-recursed through
    serde, so it is bounded by wire::MAX_SPACE_SERIALIZE_DEPTH
    (128) and MAX_AST_SERIALIZE_DEPTH (512).
  • Small-stack regression tests covering serialize and drop, each
    verified to fail against the pre-fix code.

Also fixed, not in the original list: the derived Serialize on
AstNode (src/ast.rs). /ast aborted on an 80 KB payload of
nested parentheses — cheaper to reach than the FuncSpace path, since
AST depth costs no space nesting.

Deliberate SemVer exception: the Drop impls forbid moving fields
out of these types by value (E0509). Landed under a minor bump rather
than held for 3.0; recorded under (breaking) in CHANGELOG.md and
in STABILITY.md.

Left open (not reachable today): the derived Clone on FuncSpace /
Ops is still recursive. No production path clones a whole tree.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingoutputReport / serialization output surfacesecuritySecurity-relevant finding

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions