mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary This PR fixes infinite recursion and stack overflow crashes when error objects have circular references in their properties, particularly when `error.stack = error`. ### The Problem When an error object's stack property references itself or creates a circular reference chain, Bun would enter infinite recursion and crash. Common patterns that triggered this: ```javascript const error = new Error(); error.stack = error; // Crash! console.log(error); // Or circular cause chains: error1.cause = error2; error2.cause = error1; // Crash! ``` ### The Solution Added proper circular reference detection at three levels: 1. **C++ bindings layer** (`bindings.cpp`): Skip processing if `stack` property equals the error object itself 2. **VirtualMachine layer** (`VirtualMachine.zig`): Track visited errors when printing error instances and their causes 3. **ConsoleObject layer** (`ConsoleObject.zig`): Properly coordinate visited map between formatters Circular references are now safely detected and printed as `[Circular]` instead of causing crashes. ## Test plan Added comprehensive tests in `test/regression/issue/circular-error-stack.test.ts`: - ✅ `error.stack = error` circular reference - ✅ Nested circular references via error properties - ✅ Circular cause chains (`error1.cause = error2; error2.cause = error1`) All tests pass: ``` bun test circular-error-stack.test.ts ✓ error with circular stack reference should not cause infinite recursion ✓ error with nested circular references should not cause infinite recursion ✓ error with circular reference in cause chain ``` Manual testing: ```javascript // Before: Stack overflow crash // After: Prints error normally const error = new Error("Test"); error.stack = error; console.log(error); // error: Test ``` 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, tempDir } from "harness";
|
|
|
|
test("error with circular stack reference should not cause infinite recursion", async () => {
|
|
using dir = tempDir("circular-error-stack", {
|
|
"index.js": `
|
|
const error = new Error("Test error");
|
|
error.stack = error;
|
|
console.log(error);
|
|
console.log("after error print");
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "index.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stdout).toContain("after error print");
|
|
expect(stdout).not.toContain("Maximum call stack");
|
|
expect(stderr).not.toContain("Maximum call stack");
|
|
});
|
|
|
|
test("error with nested circular references should not cause infinite recursion", async () => {
|
|
using dir = tempDir("nested-circular-error", {
|
|
"index.js": `
|
|
const error1 = new Error("Error 1");
|
|
const error2 = new Error("Error 2");
|
|
error1.stack = error2;
|
|
error2.stack = error1;
|
|
console.log(error1);
|
|
console.log("after error print");
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "index.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stdout).toContain("after error print");
|
|
expect(stdout).not.toContain("Maximum call stack");
|
|
expect(stderr).not.toContain("Maximum call stack");
|
|
});
|
|
|
|
test("error with circular reference in cause chain", async () => {
|
|
using dir = tempDir("circular-error-cause", {
|
|
"index.js": `
|
|
const error1 = new Error("Error 1");
|
|
const error2 = new Error("Error 2");
|
|
error1.cause = error2;
|
|
error2.cause = error1;
|
|
console.log(error1);
|
|
console.log("after error print");
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "index.js"],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(stdout).toContain("after error print");
|
|
expect(stdout).not.toContain("Maximum call stack");
|
|
expect(stderr).not.toContain("Maximum call stack");
|
|
});
|