You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
JSON.parse segfaults on deeply-nested input instead of throwing (node parses it; scriptc raises RangeError) #7792
JSON.parse on a deeply-nested document segfaults (SIGSEGV, exit 139) instead of throwing. Node parses the same document successfully; scriptc raises a catchable RangeError. Perry is the only one of the three that dies.
node --experimental-strip-types nested_json.ts → 300000
scriptc 0.0.23 → Uncaught RangeError: Maximum call stack size exceeded (exit 1)
perry 0.5.1455 (0a2bf15bd) → SIGSEGV, no output ← CRASH
Threshold
Bisected by nesting depth (the 300000 literal below):
depth
1 000
5 000
10 000
50 000
100 000
300 000
perry
0
0
0
139
139
139
node
0
0
0
0
0
0
So the cliff is between 10k and 50k nesting levels. Node handles 300k. Perry produces no output at all — the crash is during the parse, not at teardown.
Committed at gc-handoff/m0810/deep/nested_json.ts.
Why this matters beyond the synthetic case
Deeply-nested JSON is a well-known untrusted-input shape — it is the classic "billion laughs"-adjacent parser DoS. A segfault on attacker-controlled input is a materially worse outcome than a thrown error: it cannot be caught by a try/catch around JSON.parse, so a server handling a hostile request body goes down rather than returning 400.
The expected behaviour is a catchable RangeError (what scriptc does), or successful parsing (what Node does). Either is fine; crashing is not.
Likely mechanism
A recursive-descent parser recursing once per nesting level and overflowing the 8 MB stack, with no depth limit and no stack-guard check. Note this repo has fixed one instance of this class before — #5065, "try/catch nesting deeper than 128 panics the process instead of throwing" — so a depth guard that converts overflow into a throw is an established pattern here.
Worth checking whether the same shape exists in the other recursive consumers of untrusted input (JSON.stringify on a deep object, structuredClone, the regex engine, deep util.inspect).
Found by
The realistic-program corpus sweep of 2026-08-10 (perry 0.5.1455 @ 0a2bf15bd), while probing whether scriptc's recursive-refcount-drop depth cliff generalizes. It does — scriptc segfaults on deep linked lists, deep unbalanced trees, and deep closure chains, where Perry is clean. This is the one deep-structure shape where Perry is the one that crashes.
Heads-up for anyone picking this up: the crash described here is already fixed on main, and there is an open PR that supersedes the fix. Posting the pointers so the work is not done twice.
Landed:196c8111b — fix(json): throw instead of crashing on deeply nested JSON.parse input (#7792) (#7816), in the 2026-08-11 batch. JSON.parse now measures nesting depth with a single non-recursive scan before either parser descends, and throws a catchable RangeError past 1,000 levels. Notes worth knowing from that change:
Both parsers recurse — the syntax-validation pass and the value parser — so the guard had to run ahead of validation, which means the depth scan sees malformed input and copes with it (brackets inside strings do not count; a stray closer clamps instead of underflowing).
The limit is 1,000 because it must be safe on the smallest stack in the process, not the main thread's. A first attempt at 10,000 came from a main-thread measurement and crashed the harness at 9,999 on a 2 MiB worker stack.
All three entry points share the one guard; js_json_parse and js_json_parse_result are separate copies of the same flow, and guarding only the latter covered nothing a compiled program reaches.
That is outcome 2 from the issue (scriptc's behaviour: catchable RangeError), not outcome 1 (Node's: parse it).
Open and unmerged: PR #7832, branch fix/7817-iterative-json-parser — fix(json): parse deep nesting with a heap-backed stack. That is the upgrade from outcome 2 to outcome 1.
So the remaining question on this issue is only whether Perry should match Node and parse 300k levels rather than throw at 1,000 — which is what #7832 is for. The segfault itself is gone from main.
Found while working #7838 (unrelated); I am not taking this issue.
That meets this issue's stated acceptance condition (catchable RangeError or successful parsing). Full iterative parsing/parity is tracked separately by #7817 / PR #7832.
Activity
fix(json): throw instead of crashing on deeply nested JSON.parse input (
proggeramlug commentedon Aug 11, 2026
Heads-up for anyone picking this up: the crash described here is already fixed on
main, and there is an open PR that supersedes the fix. Posting the pointers so the work is not done twice.Landed:
196c8111b— fix(json): throw instead of crashing on deeply nested JSON.parse input (#7792) (#7816), in the 2026-08-11 batch.JSON.parsenow measures nesting depth with a single non-recursive scan before either parser descends, and throws a catchableRangeErrorpast 1,000 levels. Notes worth knowing from that change:js_json_parseandjs_json_parse_resultare separate copies of the same flow, and guarding only the latter covered nothing a compiled program reaches.That is outcome 2 from the issue (scriptc's behaviour: catchable
RangeError), not outcome 1 (Node's: parse it).Open and unmerged: PR #7832, branch
fix/7817-iterative-json-parser— fix(json): parse deep nesting with a heap-backed stack. That is the upgrade from outcome 2 to outcome 1.So the remaining question on this issue is only whether Perry should match Node and parse 300k levels rather than throw at 1,000 — which is what #7832 is for. The segfault itself is gone from
main.Found while working #7838 (unrelated); I am not taking this issue.
proggeramlug commentedon Aug 11, 2026
Verified fixed on current main at 48c5e9a by merged PR #7816.
Fresh verification:
Focused runtime boundary test: 1 passed, 0 failed.
Built perry, perry-runtime-static, and perry-stdlib-static with a fresh optimized target: exit 0.
Ran the issue's 300,000-level shape through the freshly linked binary: exit 0.
Output confirmed the RangeError was catchable and execution continued:
caught true RangeError: JSON.parse: input nested deeper than 1000 levels
alive
That meets this issue's stated acceptance condition (catchable RangeError or successful parsing). Full iterative parsing/parity is tracked separately by #7817 / PR #7832.