Files
bun.sh/test/regression/issue/circular-error-stack.test.ts
robobun e555702653 Fix infinite recursion when error.stack is a circular reference (#22863)
## 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>
2025-09-22 22:22:58 -07:00

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");
});