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-423 — impl 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.
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.
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-423—impl From<&spaces::FuncSpace> for wire::FuncSpaceis directly recursive:
SerializeonFuncSpace { spaces: Vec<FuncSpace> }(
src/spaces.rs:207) and onwire::Ops(src/wire.rs:425).Dropglue forFuncSpaceandAstNode(
src/ast.rs:151) — no hand-written iterativeDrop.Nesting depth is attacker-controlled via nested functions / classes /
closures, which are ordinary legal constructs in every supported language.
Deserializeis protected by serde_json's built-in 128-depth recursionlimit.
Serializehas 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.rsmakes such input time out long before serializationruns. 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-375andsrc/output/dump_ops.rs:245-252flatten trees before drop specifically toavoid stack overflow, and
big-code-analysis-cli/src/markdown_report.rs:1489-1493resorts to
std::mem::forgetpast depth 50,000. The productionFuncSpace/AstNodepaths do none of this.Suggested direction
DropforFuncSpaceandAstNode, matching the existingflatten-before-drop pattern.
wireconversion andSerializeimpls, mirroring serde_json'sDeserializeguard.serialize and drop, not just traversal — re-run after perf(metrics/tokens): O(leaves x depth^2) ancestor walk is a DoS vector #1052.
Scope correction (see comments)
The
Drophalf of this title is not implicated.bca checkbuilds anddrops a 10 000-deep
FuncSpacechain without overflowing;bca metrics -O jsonaborts at 1 000. The bug is the recursiveSerializepath —wire.rs'sFrom<&spaces::FuncSpace>and the derivedSerializeonFuncSpace { spaces: Vec<FuncSpace> }.Reproducer: 1 000 nested
fns (~11 KB) ->fatal runtime error: stack overflow, aborting, exit 134. Scales withFuncSpacenesting, not ASTdepth — nested parens at depth 200 000 (400 KB) finish in 272 ms.
SIGABRT, not a catchable panic, sobca-web'sspawn_blockingpanichandling 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
FuncSpacevalues directly, so nothing exercisesserialization.
Fix in review — #1082
Fixed in
3fd01c70, workspace bumped to2.1.0in7b8f3fd6, both onfix/1056-recursive-serialize-drop. Open for review as #1082; thisissue closes on merge. See the
findings comment for measurements and
verification.
DropforFuncSpaceandAstNode— and forOps,wire::FuncSpace,wire::Ops. The scope-correction note above waswrong to write
Dropoff: re-measured, it aborts at ~16 000 levelsin a debug build and ~65 000 in release, which fits inside
bca-web's 4 MiB body cap.wireconversion and theSerializeimpls.The conversion (
From<&spaces::FuncSpace>) needed no cap — it isnow iterative, and was in fact the site of the reported
1 000-level abort.
Serializecannot be de-recursed throughserde, so it is bounded bywire::MAX_SPACE_SERIALIZE_DEPTH(128) and
MAX_AST_SERIALIZE_DEPTH(512).verified to fail against the pre-fix code.
Also fixed, not in the original list: the derived
SerializeonAstNode(src/ast.rs)./astaborted on an 80 KB payload ofnested parentheses — cheaper to reach than the
FuncSpacepath, sinceAST depth costs no space nesting.
Deliberate SemVer exception: the
Dropimpls forbid moving fieldsout of these types by value (
E0509). Landed under a minor bump ratherthan held for
3.0; recorded under (breaking) inCHANGELOG.mdandin
STABILITY.md.Left open (not reachable today): the derived
CloneonFuncSpace/Opsis still recursive. No production path clones a whole tree.