Files
bun.sh/test/regression/issue/circular-error-stack-edge-cases.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

100 lines
2.8 KiB
TypeScript

import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("error.stack getter that throws should not crash", async () => {
using dir = tempDir("throwing-stack-getter", {
"index.js": `
const error = new Error("Test error");
Object.defineProperty(error, "stack", {
get() {
throw new Error("Stack getter throws!");
}
});
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("Stack getter throws");
expect(stderr).not.toContain("Stack getter throws");
});
test("error.stack getter returning circular reference", async () => {
using dir = tempDir("circular-stack-getter", {
"index.js": `
const error = new Error("Test error");
Object.defineProperty(error, "stack", {
get() {
return error; // Return the error itself
}
});
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 multiple throwing getters", async () => {
using dir = tempDir("multiple-throwing-getters", {
"index.js": `
const error = new Error("Test error");
Object.defineProperty(error, "stack", {
get() {
throw new Error("Stack throws!");
}
});
Object.defineProperty(error, "cause", {
get() {
throw new Error("Cause throws!");
}
});
error.normalProp = "works";
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).toContain("normalProp");
expect(stdout).not.toContain("Stack throws");
expect(stdout).not.toContain("Cause throws");
});