From 2d9100a138b0a06d80f76b00dfa5d2eb7c22aad4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 28 May 2026 22:51:13 +0000 Subject: [PATCH 1/3] js_parser: guard stack depth when parsing nested JSX elements parse_jsx_element recurses directly for each nested child element (...), but unlike the other recursive parse entry points it never consulted the parser's stack guard. A source like "() =>
" repeated thousands of times nests that many
children (the "() =>" between each pair is parsed as JSX text), so the unbounded recursion ran off the end of the stack and the process died on the guard page with a bare SIGSEGV. Add the standard is_safe_to_recurse() check at the top of parse_jsx_element; the parse entry points already convert the resulting StackOverflow error into a "Maximum call stack size exceeded" diagnostic. --- src/js_parser/parse/parse_jsx.rs | 5 ++ .../jsx-deep-nesting-stack-overflow.test.ts | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts diff --git a/src/js_parser/parse/parse_jsx.rs b/src/js_parser/parse/parse_jsx.rs index c40e9917b55..083595de43d 100644 --- a/src/js_parser/parse/parse_jsx.rs +++ b/src/js_parser/parse/parse_jsx.rs @@ -16,6 +16,11 @@ use bun_core::err; impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> { pub fn parse_jsx_element(&mut self, loc: bun_ast::Loc) -> Result { let p = self; + // Nested child elements (`...`) recurse back into this function, + // so guard the stack the same way the other recursive parse entry points do. + if !p.stack_check.is_safe_to_recurse() { + return Err(err!("StackOverflow")); + } if SCAN_ONLY { p.needs_jsx_import = true; } diff --git a/test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts b/test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts new file mode 100644 index 00000000000..443a0e9820b --- /dev/null +++ b/test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; +import path from "node:path"; + +// Regression test for a stack overflow in the TSX parser, found by fuzzing. +// +// `parse_jsx_element` recurses directly for every nested child element +// (`...`), but unlike the other recursive parse entry points it never +// consulted the parser's stack guard. A source like `() =>
` repeated +// thousands of times nests that many `
` children (the `() =>` between each +// pair is parsed as JSX text), so the unbounded recursion ran off the end of +// the stack and the process died on the guard page with a bare SIGSEGV — no +// crash handler, no error message. +// +// With the guard in place the parser stops and reports "Maximum call stack size +// exceeded" instead of crashing. The transpile runs in a child process so a +// regression fails these assertions rather than taking down the test runner. +test("deeply nested arrow/JSX does not overflow the stack", async () => { + // Each `() =>
` adds one arrow frame and one JSX-element frame. This is + // far deeper than the fuzzer's ~23k repetitions so the guard fires well + // before the real stack end on both release and the larger debug frames. + const source = "() =>
".repeat(50_000); + + using dir = tempDir("jsx-deep-nesting-stack-overflow", { + "input.tsx": source, + "run.ts": ` + const src = require("node:fs").readFileSync(${JSON.stringify(path.join("input.tsx"))}, "latin1"); + try { + new Bun.Transpiler({ + loader: "tsx", + target: "bun", + minifyWhitespace: true, + deadCodeElimination: true, + }).transformSync(src); + console.log("NO ERROR"); + } catch (e) { + console.error(String((e as Error).message)); + } + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "run.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Must terminate on its own, not be killed by the stack-guard-page SIGSEGV. + expect(proc.signalCode).toBeNull(); + // The parser bounds the recursion and throws a catchable SyntaxError. + expect(stderr).toContain("Maximum call stack size exceeded"); + expect(stdout).not.toContain("NO ERROR"); + expect(exitCode).toBe(0); +}); From 6c2bc6fc15ffacdb67df0864e1f00e1cee1f25ff Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 28 May 2026 23:06:37 +0000 Subject: [PATCH 2/3] test: address review nits (inline literal, Buffer.alloc fill) --- .../transpiler/jsx-deep-nesting-stack-overflow.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts b/test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts index 443a0e9820b..9510e4e3497 100644 --- a/test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts +++ b/test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts @@ -1,6 +1,5 @@ import { expect, test } from "bun:test"; import { bunEnv, bunExe, tempDir } from "harness"; -import path from "node:path"; // Regression test for a stack overflow in the TSX parser, found by fuzzing. // @@ -19,12 +18,14 @@ test("deeply nested arrow/JSX does not overflow the stack", async () => { // Each `() =>
` adds one arrow frame and one JSX-element frame. This is // far deeper than the fuzzer's ~23k repetitions so the guard fires well // before the real stack end on both release and the larger debug frames. - const source = "() =>
".repeat(50_000); + // (`Buffer.alloc` fill over `.repeat` — the latter is very slow in debug JSC.) + const unit = "() =>
"; + const source = Buffer.alloc(unit.length * 50_000, unit).toString(); using dir = tempDir("jsx-deep-nesting-stack-overflow", { "input.tsx": source, "run.ts": ` - const src = require("node:fs").readFileSync(${JSON.stringify(path.join("input.tsx"))}, "latin1"); + const src = require("node:fs").readFileSync("input.tsx", "latin1"); try { new Bun.Transpiler({ loader: "tsx", From e99279bd52d871ff5fb9041f0470b8b1cd40dbac Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 28 May 2026 23:33:42 +0000 Subject: [PATCH 3/3] ci: retrigger