Summary
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.
Repro
// nested_json.ts
function main(): void {
let s = "0";
for (let i = 0; i < 300000; i++) s = "[" + s + "]";
const v = JSON.parse(s);
let d = 0; let cur: any = v;
while (Array.isArray(cur)) { d++; cur = cur[0]; }
console.log(d);
}
main();
export PERRY_RUNTIME_DIR=<build>/target/release
PERRY_NO_AUTO_OPTIMIZE=1 <build>/target/release/perry nested_json.ts -o p_nested_json
./p_nested_json; echo "exit=$?" # exit=139, no output
node --experimental-strip-types nested_json.ts # 300000
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.
Summary
JSON.parseon a deeply-nested document segfaults (SIGSEGV, exit 139) instead of throwing. Node parses the same document successfully; scriptc raises a catchableRangeError. Perry is the only one of the three that dies.Threshold
Bisected by nesting depth (the
300000literal below):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.
Repro
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/catcharoundJSON.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.stringifyon a deep object,structuredClone, the regex engine, deeputil.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.