Skip to content

Limit instruction nesting depth in the wast parser - #2815

Open
Nishuuzz wants to merge 1 commit into
WebAssembly:mainfrom
Nishuuzz:fix/wast-parser-nesting-depth
Open

Limit instruction nesting depth in the wast parser#2815
Nishuuzz wants to merge 1 commit into
WebAssembly:mainfrom
Nishuuzz:fix/wast-parser-nesting-depth

Conversation

@Nishuuzz

@Nishuuzz Nishuuzz commented Aug 8, 2026

Copy link
Copy Markdown

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:

$ python3 -c "n=200000; open('deep.wat','w').write('(module (func\n'+'(block '*n+')'*n+'\n))\n')"
$ wat2wasm deep.wat -o /dev/null
  • Windows: exits 0xC00000FD (STATUS_STACK_OVERFLOW)
  • Linux + ASAN: ERROR: AddressSanitizer: stack-overflow

This 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:

ParseInstrList -> ParseInstr -> ParseExpr -> ParseBlock -> ParseInstrList

Measured depth at which (block …) nesting faults:

build stack faults at
MinGW GCC 16, Release 2 MB between 2500 and 3000
Linux GCC 15, Debug + ASAN 8 MB between 1000 and 2000

Every syntactic form that nests is affected: folded (block …) / (loop …), unfolded block … end, folded (if … (then …)), and folded operands such as (i32.eqz (i32.eqz …)).

The binary reader is already bounded here — BinaryReaderIR::kMaxNestingDepth (16384) makes wasm2wat report label stack exceeds max nesting depth rather 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 (ParseInstrList and ParseExprList). 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.

$ wat2wasm deep.wat -o /dev/null
deep.wat:2:7001: error: instruction nesting depth exceeds max of 1000

Testing

  • Added test/parse/expr/bad-nesting-depth.txt (expected output generated with run-tests.py --rebase).
  • Verified all five nesting forms now produce the diagnostic instead of faulting, and that input just under the limit still parses.
  • Full test/run-tests.py suite run against a Debug + ASAN/UBSAN build, with all submodules checked out.
  • scripts/clang-format-diff.sh main is 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.

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.
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.

wat2wasm segfaults on .wat file with many nested if statements

1 participant