Skip to content

JSON.parse segfaults on deeply-nested input instead of throwing (node parses it; scriptc raises RangeError) #7792

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions