Limit instruction nesting depth in the wast parser - #2815
Open
Nishuuzz wants to merge 1 commit into
Open
Conversation
The wast parser is recursive descent, so each level of nested instructions costs several stack frames: ParseInstrList -> ParseInstr -> ParseExpr -> ParseBlock -> ParseInstrList Deeply nested but otherwise well-formed text therefore exhausts the stack and crashes instead of producing a diagnostic. A file of ~3000 nested blocks is enough to fault wat2wasm on Windows (0xC00000FD STATUS_STACK_OVERFLOW), and ~2000 is enough under ASAN on Linux. The binary reader already bounds this with BinaryReaderIR's kMaxNestingDepth, but it tracks nesting in an explicit label stack, so its limit can be much higher than a recursive parser can afford. Add the equivalent bound to the text parser, chosen to stay within the smallest default stack we build against (1MB on MSVC). Exceeding the limit is reported once and is not recoverable: without that, the callers resynchronize and walk straight back into the same too-deep input, reporting the same error once per level. On a 200k-deep input that was ~600k diagnostics and 7.8s; it is now 6 lines and 114ms. Fixes WebAssembly#2377.
sbc100
reviewed
Aug 9, 2026
| static constexpr int kMaxNestingDepth = 1000; | ||
|
|
||
| // Increments the parser's nesting depth for as long as it is in scope. | ||
| class NestingGuard { |
Member
There was a problem hiding this comment.
Maybe just make this a struct and skip the public: line?
sbc100
reviewed
Aug 9, 2026
| // Increments the parser's nesting depth for as long as it is in scope. | ||
| class NestingGuard { | ||
| public: | ||
| explicit NestingGuard(WastParser* parser) : parser_(parser) { |
Member
There was a problem hiding this comment.
Should this be a const reference maybe?
sbc100
reviewed
Aug 9, 2026
| // stack, this parser is recursive descent: every level of nested | ||
| // instructions costs several stack frames. The limit is therefore much | ||
| // lower than BinaryReaderIR's kMaxNestingDepth, and is chosen to stay | ||
| // within the smallest default stack we build against (1MB on MSVC). |
Member
There was a problem hiding this comment.
This assume ~1kb per stack frame, is that right?
sbc100
approved these changes
Aug 9, 2026
sbc100
left a comment
Member
There was a problem hiding this comment.
LGTM, I wonder if real world module will blow through this limit or not?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every tool that goes through the wast parser (
wat2wasm,wast2json,wat-desugar) crashes on deeply nested — but otherwise well-formed — text input, instead of reporting an error:0xC00000FD(STATUS_STACK_OVERFLOW)ERROR: AddressSanitizer: stack-overflowThis is #2377, where the reporter hit it on machine-generated WAT containing a long chain of nested
ifs. It needs no unusual input — just depth.Cause
The parser is recursive descent, so each nesting level costs several stack frames:
Measured depth at which
(block …)nesting faults:Every syntactic form that nests is affected: folded
(block …)/(loop …), unfoldedblock … end, folded(if … (then …)), and folded operands such as(i32.eqz (i32.eqz …)).The binary reader is already bounded here —
BinaryReaderIR::kMaxNestingDepth(16384) makeswasm2watreportlabel stack exceeds max nesting depthrather than fault. It can afford a far higher limit because it tracks nesting in an explicit label stack instead of on the C++ stack. So today wabt accepts a 16383-deep module in binary but faults on the equivalent text.Fix
Bound nesting in the text parser as well, using an RAII guard on the two list parsers that every nesting cycle passes through (
ParseInstrListandParseExprList). The limit is 1000, picked to stay inside the smallest default stack we build against (1 MB on MSVC) — hence much lower than the binary reader's.Exceeding it is reported once and is deliberately not recoverable. Without that, the callers resynchronize and walk straight back into the same too-deep input, reporting the error once per level: on the 200k-deep file above that was ~600k diagnostics in 7.8 s, versus 6 lines in 0.11 s now.
Testing
test/parse/expr/bad-nesting-depth.txt(expected output generated withrun-tests.py --rebase).test/run-tests.pysuite run against a Debug + ASAN/UBSAN build, with all submodules checked out.scripts/clang-format-diff.sh mainis clean.Relationship to #2748
#2748 touches the same symptom on Windows by raising the linker stack reserve. The two are complementary rather than competing: a bigger stack raises the threshold, while this bounds the recursion so the tools report an error instead of faulting on every platform and build configuration.
The constant is a judgement call — happy to change the value, or to move the guard, if you'd prefer it somewhere else.
Fixes #2377.