Skip to content

Raise wasm C stack size so deep recursion no longer traps (fixes #47)#48

Open
paulmanoni wants to merge 1 commit into
fastschema:masterfrom
paulmanoni:fix/wasm-stack-size-deep-recursion
Open

Raise wasm C stack size so deep recursion no longer traps (fixes #47)#48
paulmanoni wants to merge 1 commit into
fastschema:masterfrom
paulmanoni:fix/wasm-stack-size-deep-recursion

Conversation

@paulmanoni
Copy link
Copy Markdown

@paulmanoni paulmanoni commented May 29, 2026

Problem

Evaluating deeply‑nested expressions traps with wasm error: out of bounds memory access instead of completing (or returning a catchable error). It's a C call‑stack overflow: QuickJS's parser/evaluator recurse in C, and the qjswasm target linked with wasm‑ld's small default stack.

The ceiling is low enough to hit real code:

  • plain Eval of ~1000 nested parens traps;
  • via @vue/compiler-sfc (which adds its own recursion on top of the source AST) it traps at only ~15–20 levels of ordinary source nesting — array/object config literals with arrow callbacks, ternaries, optional chaining, etc.

Fixes #47.

Root cause

--stack-first and --initial-memory are added via add_link_options(...) after add_executable(qjswasm) in qjswasm.cmake, so they never apply to that target (add_link_options only affects targets created after it). The effective link therefore used wasm‑ld's default stack size.

Fix

Move the stack sizing into the target_link_options(qjswasm PRIVATE ...) block that actually applies to the target:

"LINKER:-z,stack-size=16777216"     # 16 MiB C stack (was wasm-ld's small default)
"LINKER:--initial-memory=20971520"  # 20 MiB — with --stack-first the stack lives at the
                                     # bottom of linear memory, so initial memory must
                                     # exceed stack-size + data

qjs.wasm regenerated via make + wasm-opt -O3 (WASI SDK 33).

Verification

  • Nested‑paren Eval now succeeds to depth 20000 (previously trapped at 1000).
  • A real 128‑component Vue app compiled through @vue/compiler-sfc on qjs: 10/128 files failed before, 0/128 fail now.
rt, _ := qjs.New(qjs.Option{MaxStackSize: 8 << 20, MemoryLimit: 1 << 30})
code := strings.Repeat("(", 5000) + "1" + strings.Repeat(")", 5000)
_, err := rt.Context().Eval("x.js", qjs.Code(code)) // before: panic; after: ok

Notes / follow‑ups (not in this PR)

  • 16 MiB is a pragmatic default; happy to tune.
  • Separately, it would be nice if a wasm trap from (*Runtime).call returned an error instead of panic-ing (today a single deep Eval can crash the host and poison the runtime), and if QuickJS's own JS_SetMaxStackSize guard were coupled just under the linker stack so true runaway recursion throws a catchable RangeError. Glad to do those in a follow‑up if you're interested.

QuickJS's parser/evaluator recurse in C. The qjswasm target linked with
wasm-ld's small default stack, so deeply (but ordinarily) nested source
overflowed it and surfaced as "wasm error: out of bounds memory access"
— e.g. ~1000 nested parens via plain Eval, and as few as ~15-20 levels
of source nesting when running @vue/compiler-sfc (which adds its own
recursion on top). Fixes fastschema#47.

Root cause: the `--stack-first` / `--initial-memory` add_link_options
sit AFTER add_executable(qjswasm), so they never applied to that target.
Put the stack sizing in the target_link_options(qjswasm ...) block that
actually takes effect:

  -z stack-size=16777216   (16 MiB C stack; was wasm-ld's small default)
  --initial-memory=20971520 (20 MiB; with --stack-first the stack lives
                             at the bottom of linear memory, so initial
                             memory must exceed stack-size + data)

Verified: nested-paren Eval now succeeds to depth 20000 (was trapping at
1000), and a real 128-component Vue app that previously failed on 10
files now compiles all 128. qjs.wasm regenerated via `make` + wasm-opt -O3.
@paulmanoni paulmanoni changed the title fix(wasm): raise C stack size so deep recursion no longer traps Raise wasm C stack size so deep recursion no longer traps (fixes #47) May 29, 2026
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.

Deeply nested expressions trap with "out of bounds memory access" (WASM stack overflow) instead of a catchable error

2 participants