Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
96212b457b fix(io): prevent segfault from libuv pipe EOF callback after reader cleanup
When a pipe is closed via closeImpl, libuv may still have pending read
callbacks (e.g. EOF notification) queued. Previously, closeImpl set
pipe.data to point to the pipe itself (for the close callback), but
pending onStreamRead/onStreamAlloc callbacks would cast this invalid
pointer to WindowsBufferedReader, causing a segfault.

Fix: Set pipe.data/tty.data to null in closeImpl, add null guards in
onStreamAlloc and onStreamRead via `orelse return`, and simplify the
onPipeClose/onTTYClose callbacks to use the handle parameter directly.

Closes #26832

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-09 20:51:51 +00:00
2 changed files with 38 additions and 9 deletions

View File

@@ -760,7 +760,7 @@ pub const WindowsBufferedReader = struct {
return Type.onReaderError(@as(*Type, @ptrCast(@alignCast(this))), err);
}
fn loop(this: *anyopaque) *Async.Loop {
return Type.loop(@as(*Type, @alignCast(@ptrCast(this))));
return Type.loop(@as(*Type, @ptrCast(@alignCast(this))));
}
};
return .{
@@ -955,14 +955,14 @@ pub const WindowsBufferedReader = struct {
}
fn onStreamAlloc(handle: *uv.Handle, suggested_size: usize, buf: *uv.uv_buf_t) callconv(.c) void {
var this = bun.cast(*WindowsBufferedReader, handle.data);
const this: *WindowsBufferedReader = @ptrCast(@alignCast(handle.data orelse return));
const result = this.getReadBufferWithStableMemoryAddress(suggested_size);
buf.* = uv.uv_buf_t.init(result);
}
fn onStreamRead(handle: *uv.uv_handle_t, nread: uv.ReturnCodeI64, buf: *const uv.uv_buf_t) callconv(.c) void {
const stream = bun.cast(*uv.uv_stream_t, handle);
var this = bun.cast(*WindowsBufferedReader, stream.data);
const this: *WindowsBufferedReader = @ptrCast(@alignCast(stream.data orelse return));
const nread_int = nread.int();
@@ -1157,7 +1157,7 @@ pub const WindowsBufferedReader = struct {
file.detach();
},
.pipe => |pipe| {
pipe.data = pipe;
pipe.data = null;
this.flags.is_paused = true;
pipe.close(onPipeClose);
},
@@ -1165,7 +1165,7 @@ pub const WindowsBufferedReader = struct {
if (Source.StdinTTY.isStdinTTY(tty)) {
// Node only ever closes stdin on process exit.
} else {
tty.data = tty;
tty.data = null;
tty.close(onTTYClose);
}
@@ -1199,13 +1199,11 @@ pub const WindowsBufferedReader = struct {
}
fn onPipeClose(handle: *uv.Pipe) callconv(.c) void {
const this = bun.cast(*uv.Pipe, handle.data);
bun.default_allocator.destroy(this);
bun.default_allocator.destroy(handle);
}
fn onTTYClose(handle: *uv.uv_tty_t) callconv(.c) void {
const this = bun.cast(*uv.uv_tty_t, handle.data);
bun.default_allocator.destroy(this);
bun.default_allocator.destroy(handle);
}
pub fn onRead(this: *WindowsBufferedReader, amount: bun.sys.Maybe(usize), slice: []u8, hasMore: ReadState) void {

View File

@@ -0,0 +1,31 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// Regression test for https://github.com/oven-sh/bun/issues/26832
// Segfault when libuv delivers pipe EOF callbacks after the reader is cleaned up.
// The crash occurs when spawning many subprocesses and closing them rapidly,
// causing a race between pipe close and pending EOF callbacks.
test("rapid subprocess spawn and close does not crash", async () => {
const iterations = 50;
const promises: Promise<void>[] = [];
for (let i = 0; i < iterations; i++) {
promises.push(
(async () => {
const proc = Bun.spawn({
cmd: [bunExe(), "-e", "process.stdout.write('x'); process.exit(0)"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
// Read and immediately discard - the key is rapid open/close cycles
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(exitCode).toBe(0);
})(),
);
}
await Promise.all(promises);
});