Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
c7925173c4 fix(shell): fix use-after-free in runFromJS when setupIOBeforeRun fails
When `setupIOBeforeRun()` fails in the JS path (e.g., stdout/stderr dup
fails), the code was calling `#deinitFromExec()` which frees the
interpreter struct via `allocator.destroy(this)`. However, the GC still
holds a reference to the JS wrapper object and will later call
`deinitFromFinalizer()` on the already-freed memory, causing a segfault.

Fix: Use `#derefRootShellAndIOIfNeeded(true)` instead to clean up
IO/shell resources, and let the GC finalizer handle the final struct
deallocation via `deinitFromFinalizer`.

Closes #26894

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-11 09:15:19 +00:00
2 changed files with 45 additions and 1 deletions

View File

@@ -1154,7 +1154,11 @@ pub const Interpreter = struct {
_ = callframe; // autofix
if (this.setupIOBeforeRun().asErr()) |e| {
defer this.#deinitFromExec();
// Clean up IO/shell resources but do NOT free the interpreter struct.
// The GC still holds a reference to the JS wrapper object and will call
// deinitFromFinalizer later, which handles allocator.destroy(this).
// Using #deinitFromExec here would cause a use-after-free. (GH-26894)
this.#derefRootShellAndIOIfNeeded(true);
const shellerr = bun.shell.ShellErr.newSys(e);
return try throwShellErr(&shellerr, .{ .js = globalThis.bunVM().event_loop });
}

View File

@@ -0,0 +1,40 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// Regression test for https://github.com/oven-sh/bun/issues/26894
// When setupIOBeforeRun() fails (e.g., stdout dup fails), the shell interpreter
// would free itself via #deinitFromExec, but the GC still held a reference to
// the JS wrapper object. When the GC later finalized it, it would access freed memory.
test("shell interpreter does not crash when stdout is closed", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const fs = require("fs");
// Close stdout so that the shell's dup(stdout) fails in setupIOBeforeRun
fs.closeSync(1);
try {
// This should throw a shell error, not segfault
await Bun.$\`echo hello\`;
} catch (e) {
// Expected: shell error due to failed dup of stdout
}
// Force GC to trigger finalization of the shell interpreter object.
// Before the fix, this would segfault due to use-after-free.
Bun.gc(true);
Bun.gc(true);
// Write to stderr to signal success (stdout is closed)
fs.writeSync(2, "OK\\n");
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("OK");
expect(exitCode).toBe(0);
});