Files
bun-src/test/bundler/transpiler/jsx-deep-nesting-stack-overflow.test.ts
T
2026-08-27 21:09:14 +00:00

60 lines
2.4 KiB
TypeScript

import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// Regression test for a stack overflow in the TSX parser, found by fuzzing.
//
// `parse_jsx_element` recurses directly for every nested child element
// (`<a><b><c>...`), but unlike the other recursive parse entry points it never
// consulted the parser's stack guard. A source like `() => <div>` repeated
// thousands of times nests that many `<div>` 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 `() => <div>` 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.
// (`Buffer.alloc` fill over `.repeat` — the latter is very slow in debug JSC.)
const unit = "() => <div>";
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("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);
});