Skip to content

Commit 0845231

Browse files
Fix pipeline stack errors on Windows (#21800)
### What does this PR do? ### How did you verify your code works? --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 7dd85f9 commit 0845231

File tree

11 files changed

+161
-25
lines changed

11 files changed

+161
-25
lines changed

src/bun.js/api/bun/spawn/stdio.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pub const Stdio = union(enum) {
22
inherit,
3-
capture: struct { fd: bun.FileDescriptor, buf: *bun.ByteList },
3+
capture: struct { buf: *bun.ByteList },
44
ignore,
55
fd: bun.FileDescriptor,
66
dup2: struct {

src/bun.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ pub const Output = @import("./output.zig");
190190
pub const Global = @import("./Global.zig");
191191

192192
pub const FD = @import("./fd.zig").FD;
193+
pub const MovableIfWindowsFd = @import("./fd.zig").MovableIfWindowsFd;
193194

194195
/// Deprecated: Use `FD` instead.
195196
pub const FileDescriptor = FD;

src/fd.zig

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ pub const FD = packed struct(backing_int) {
225225
/// In debug, fd assertion failure can print where the FD was actually
226226
/// closed.
227227
pub fn close(fd: FD) void {
228-
bun.debugAssert(fd.closeAllowingBadFileDescriptor(@returnAddress()) == null); // use after close!
228+
const err = fd.closeAllowingBadFileDescriptor(@returnAddress());
229+
bun.debugAssert(err == null); // use after close!
229230
}
230231

231232
/// fd function will NOT CLOSE stdin/stdout/stderr.
@@ -652,6 +653,81 @@ pub fn uv_open_osfhandle(in: libuv.uv_os_fd_t) error{SystemFdQuotaExceeded}!c_in
652653
return out;
653654
}
654655

656+
/// On Windows we use libuv and often pass file descriptors to functions
657+
/// like `uv_pipe_open`, `uv_tty_init`.
658+
///
659+
/// But `uv_pipe` and `uv_tty` **take ownership of the file descriptor**.
660+
///
661+
/// This can easily cause use-after-frees, double closing the FD, etc.
662+
///
663+
/// So this type represents an FD that could possibly be moved to libuv.
664+
///
665+
/// Note that on Posix, this is just a wrapper over FD and does nothing.
666+
pub const MovableIfWindowsFd = union(enum) {
667+
const Self = @This();
668+
669+
_inner: if (bun.Environment.isWindows) ?FD else FD,
670+
671+
pub fn init(fd: FD) Self {
672+
return .{ ._inner = fd };
673+
}
674+
675+
pub fn get(self: *const Self) ?FD {
676+
return self._inner;
677+
}
678+
679+
pub fn getPosix(self: *const Self) FD {
680+
if (comptime bun.Environment.isWindows)
681+
@compileError("MovableIfWindowsFd.getPosix is not available on Windows");
682+
683+
return self._inner;
684+
}
685+
686+
pub fn close(self: *Self) void {
687+
if (comptime bun.Environment.isPosix) {
688+
self._inner.close();
689+
self._inner = FD.invalid;
690+
return;
691+
}
692+
if (self._inner) |fd| {
693+
fd.close();
694+
self._inner = null;
695+
}
696+
}
697+
698+
pub fn isValid(self: *const Self) bool {
699+
if (comptime bun.Environment.isPosix) return self._inner.isValid();
700+
return self._inner != null and self._inner.?.isValid();
701+
}
702+
703+
pub fn isOwned(self: *const Self) bool {
704+
if (comptime bun.Environment.isPosix) return true;
705+
return self._inner != null;
706+
}
707+
708+
/// Takes the FD, leaving `self` in a "moved-from" state. Only available on Windows.
709+
pub fn take(self: *Self) ?FD {
710+
if (comptime bun.Environment.isPosix) {
711+
@compileError("MovableIfWindowsFd.take is not available on Posix");
712+
}
713+
const result = self._inner;
714+
self._inner = null;
715+
return result;
716+
}
717+
718+
pub fn format(self: *const Self, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
719+
if (comptime bun.Environment.isPosix) {
720+
try writer.print("{}", .{self.get().?});
721+
return;
722+
}
723+
if (self._inner) |fd| {
724+
try writer.print("{}", .{fd});
725+
return;
726+
}
727+
try writer.print("[moved]", .{});
728+
}
729+
};
730+
655731
pub var windows_cached_fd_set: if (Environment.isDebug) bool else void = if (Environment.isDebug) false;
656732
pub var windows_cached_stdin: FD = undefined;
657733
pub var windows_cached_stdout: FD = undefined;

src/io/PipeWriter.zig

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,13 @@ pub fn PosixBufferedWriter(Parent: type, function_table: anytype) type {
339339
}
340340
}
341341

342-
pub fn start(this: *PosixWriter, fd: bun.FileDescriptor, pollable: bool) bun.sys.Maybe(void) {
342+
pub fn start(this: *PosixWriter, rawfd: anytype, pollable: bool) bun.sys.Maybe(void) {
343+
const FDType = @TypeOf(rawfd);
344+
const fd = switch (FDType) {
345+
bun.FileDescriptor => rawfd,
346+
*bun.MovableIfWindowsFd, bun.MovableIfWindowsFd => rawfd.getPosix(),
347+
else => @compileError("Expected `bun.FileDescriptor`, `*bun.MovableIfWindowsFd` or `bun.MovableIfWindowsFd` but got: " ++ @typeName(rawfd)),
348+
};
343349
this.pollable = pollable;
344350
if (!pollable) {
345351
bun.assert(this.handle != .poll);
@@ -888,12 +894,27 @@ fn BaseWindowsPipeWriter(
888894
return this.startWithCurrentPipe();
889895
}
890896

891-
pub fn start(this: *WindowsPipeWriter, fd: bun.FileDescriptor, _: bool) bun.sys.Maybe(void) {
897+
pub fn start(this: *WindowsPipeWriter, rawfd: anytype, _: bool) bun.sys.Maybe(void) {
898+
const FDType = @TypeOf(rawfd);
899+
const fd = switch (FDType) {
900+
bun.FileDescriptor => rawfd,
901+
*bun.MovableIfWindowsFd => rawfd.get().?,
902+
else => @compileError("Expected `bun.FileDescriptor` or `*bun.MovableIfWindowsFd` but got: " ++ @typeName(rawfd)),
903+
};
892904
bun.assert(this.source == null);
893905
const source = switch (Source.open(uv.Loop.get(), fd)) {
894906
.result => |source| source,
895907
.err => |err| return .{ .err = err },
896908
};
909+
// Creating a uv_pipe/uv_tty takes ownership of the file descriptor
910+
// TODO: Change the type of the parameter and update all places to
911+
// use MovableFD
912+
if (switch (source) {
913+
.pipe, .tty => true,
914+
else => false,
915+
} and FDType == *bun.MovableIfWindowsFd) {
916+
_ = rawfd.take();
917+
}
897918
source.setData(this);
898919
this.source = source;
899920
this.setParent(this.parent);

src/shell/IO.zig

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,21 @@ pub const OutKind = union(enum) {
138138
return switch (this) {
139139
.fd => |val| brk: {
140140
shellio.* = val.writer.refSelf();
141-
break :brk if (val.captured) |cap| .{ .capture = .{ .buf = cap, .fd = val.writer.fd } } else .{ .fd = val.writer.fd };
141+
break :brk if (val.captured) |cap| .{
142+
.capture = .{
143+
.buf = cap,
144+
},
145+
} else .{
146+
// Windows notes:
147+
// Since `val.writer.fd` is `MovableFD`, it could
148+
// technically be moved to libuv for ownership.
149+
//
150+
// But since this file descriptor never going to be touched by this
151+
// process, except to hand off to the subprocess when we
152+
// spawn it, we don't really care if the file descriptor
153+
// ends up being invalid.
154+
.fd = val.writer.fd.get().?,
155+
};
142156
},
143157
.pipe => .pipe,
144158
.ignore => .ignore,

src/shell/IOReader.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ pub fn onReadChunk(ptr: *anyopaque, chunk: []const u8, has_more: bun.io.ReadStat
159159
}
160160

161161
pub fn onReaderError(this: *IOReader, err: bun.sys.Error) void {
162+
log("IOReader(0x{x}.onReaderError({err}) ", .{ @intFromPtr(this), err });
162163
this.setReading(false);
163164
this.err = err.toShellSystemError();
164165
for (this.readers.slice()) |r| {

src/shell/IOWriter.zig

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ pub const ref = RefCount.ref;
1919
pub const deref = RefCount.deref;
2020

2121
ref_count: RefCount,
22-
writer: WriterImpl = if (bun.Environment.isWindows) .{} else .{ .close_fd = false },
23-
fd: bun.FileDescriptor,
22+
writer: WriterImpl = if (bun.Environment.isWindows) .{
23+
// Tell the Windows PipeWriter impl to *not* close the file descriptor,
24+
// unfortunately this won't work if it creates a uv_pipe or uv_tty as those
25+
// types own their file descriptor
26+
.owns_fd = false,
27+
} else .{ .close_fd = false },
28+
fd: MovableIfWindowsFd,
2429
writers: Writers = .{ .inlined = .{} },
2530
buf: std.ArrayListUnmanaged(u8) = .{},
2631
/// quick hack to get windows working
@@ -81,7 +86,7 @@ pub const Flags = packed struct(u8) {
8186
pub fn init(fd: bun.FileDescriptor, flags: Flags, evtloop: jsc.EventLoopHandle) *IOWriter {
8287
const this = bun.new(IOWriter, .{
8388
.ref_count = .init(),
84-
.fd = fd,
89+
.fd = MovableIfWindowsFd.init(fd),
8590
.evtloop = evtloop,
8691
.concurrent_task = jsc.EventLoopTask.fromEventLoop(evtloop),
8792
.concurrent_task2 = jsc.EventLoopTask.fromEventLoop(evtloop),
@@ -96,8 +101,9 @@ pub fn init(fd: bun.FileDescriptor, flags: Flags, evtloop: jsc.EventLoopHandle)
96101
}
97102

98103
pub fn __start(this: *IOWriter) Maybe(void) {
104+
bun.assert(this.fd.isOwned());
99105
debug("IOWriter(0x{x}, fd={}) __start()", .{ @intFromPtr(this), this.fd });
100-
if (this.writer.start(this.fd, this.flags.pollable).asErr()) |e_| {
106+
if (this.writer.start(&this.fd, this.flags.pollable).asErr()) |e_| {
101107
const e: bun.sys.Error = e_;
102108
if (bun.Environment.isPosix) {
103109
// We get this if we pass in a file descriptor that is not
@@ -140,7 +146,7 @@ pub fn __start(this: *IOWriter) Maybe(void) {
140146
this.flags.pollable = false;
141147
this.flags.nonblocking = false;
142148
this.flags.is_socket = false;
143-
return this.writer.startWithFile(this.fd);
149+
return this.writer.startWithFile(this.fd.get().?);
144150
}
145151
}
146152
return .{ .err = e };
@@ -157,6 +163,10 @@ pub fn __start(this: *IOWriter) Maybe(void) {
157163
}
158164
}
159165

166+
if (comptime bun.Environment.isWindows) {
167+
log("IOWriter(0x{x}, {}) starting with source={s}", .{ @intFromPtr(this), this.fd, if (this.writer.source) |src| @tagName(src) else "no source lol" });
168+
}
169+
160170
return .success;
161171
}
162172

@@ -637,6 +647,7 @@ pub fn enqueueFmt(
637647

638648
fn asyncDeinit(this: *@This()) void {
639649
debug("IOWriter(0x{x}, fd={}) asyncDeinit", .{ @intFromPtr(this), this.fd });
650+
bun.assert(!this.is_writing);
640651
this.async_deinit.enqueue();
641652
}
642653

@@ -648,7 +659,10 @@ pub fn deinitOnMainThread(this: *IOWriter) void {
648659
if (this.writer.handle == .poll and this.writer.handle.poll.isRegistered()) {
649660
this.writer.handle.closeImpl(null, {}, false);
650661
}
651-
} else this.winbuf.deinit(bun.default_allocator);
662+
} else {
663+
this.writer.close();
664+
this.winbuf.deinit(bun.default_allocator);
665+
}
652666
if (this.fd.isValid()) this.fd.close();
653667
this.writer.disableKeepingProcessAlive(this.evtloop);
654668
bun.destroy(this);
@@ -760,14 +774,15 @@ fn tryWriteWithWriteFn(fd: bun.FileDescriptor, buf: []const u8, comptime write_f
760774
}
761775

762776
pub fn drainBufferedData(parent: *IOWriter, buf: []const u8, max_write_size: usize, received_hup: bool) bun.io.WriteResult {
777+
bun.assert(bun.Environment.isPosix);
763778
_ = received_hup;
764779

765780
const trimmed = if (max_write_size < buf.len and max_write_size > 0) buf[0..max_write_size] else buf;
766781

767782
var drained: usize = 0;
768783

769784
while (drained < trimmed.len) {
770-
const attempt = tryWriteWithWriteFn(parent.fd, buf, bun.sys.write);
785+
const attempt = tryWriteWithWriteFn(parent.fd.get().?, buf, bun.sys.write);
771786
switch (attempt) {
772787
.pending => |pending| {
773788
drained += pending;
@@ -840,6 +855,7 @@ const log = bun.Output.scoped(.IOWriter, .hidden);
840855
const std = @import("std");
841856

842857
const bun = @import("bun");
858+
const MovableIfWindowsFd = bun.MovableIfWindowsFd;
843859
const assert = bun.assert;
844860
const jsc = bun.jsc;
845861
const Maybe = bun.sys.Maybe;

src/shell/Yield.zig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub const Yield = union(enum) {
7676
bun.debugAssert(_dbg_catch_exec_within_exec <= MAX_DEPTH);
7777
if (comptime Environment.isDebug) _dbg_catch_exec_within_exec += 1;
7878
defer {
79-
if (comptime Environment.isDebug) log("Yield({s}) _dbg_catch_exec_within_exec = {d} - 1 = {d}", .{ @tagName(this), _dbg_catch_exec_within_exec, _dbg_catch_exec_within_exec + 1 });
79+
if (comptime Environment.isDebug) log("Yield({s}) _dbg_catch_exec_within_exec = {d} - 1 = {d}", .{ @tagName(this), _dbg_catch_exec_within_exec, _dbg_catch_exec_within_exec - 1 });
8080
if (comptime Environment.isDebug) _dbg_catch_exec_within_exec -= 1;
8181
}
8282

@@ -108,6 +108,7 @@ pub const Yield = union(enum) {
108108
}
109109
continue :state x.next();
110110
}
111+
bun.assert_eql(std.mem.indexOfScalar(*Pipeline, pipeline_stack.items, x), null);
111112
pipeline_stack.append(x) catch bun.outOfMemory();
112113
continue :state x.next();
113114
},

src/shell/states/Pipeline.zig

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,10 @@ pub fn deinit(this: *Pipeline) void {
282282
fn initializePipes(pipes: []Pipe, set_count: *u32) Maybe(void) {
283283
for (pipes) |*pipe| {
284284
if (bun.Environment.isWindows) {
285-
var fds: [2]uv.uv_file = undefined;
286-
if (uv.uv_pipe(&fds, 0, 0).errEnum()) |e| {
287-
return .{ .err = Syscall.Error.fromCode(e, .pipe) };
288-
}
289-
pipe[0] = .fromUV(fds[0]);
290-
pipe[1] = .fromUV(fds[1]);
285+
pipe.* = switch (bun.sys.pipe()) {
286+
.result => |p| p,
287+
.err => |e| return .{ .err = e },
288+
};
291289
} else {
292290
switch (bun.sys.socketpairForShell(
293291
// switch (bun.sys.socketpair(
@@ -353,9 +351,5 @@ const Subshell = bun.shell.Interpreter.Subshell;
353351

354352
const Pipe = bun.shell.interpret.Pipe;
355353
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
356-
const Syscall = bun.shell.interpret.Syscall;
357354
const closefd = bun.shell.interpret.closefd;
358355
const log = bun.shell.interpret.log;
359-
360-
const windows = bun.windows;
361-
const uv = windows.libuv;

src/sys.zig

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3371,12 +3371,24 @@ pub fn disableLinger(fd: bun.FileDescriptor) void {
33713371

33723372
pub fn pipe() Maybe([2]bun.FileDescriptor) {
33733373
if (comptime Environment.isWindows) {
3374-
@panic("TODO: Implement `pipe()` for Windows");
3374+
const uv = bun.windows.libuv;
3375+
var out: [2]bun.FileDescriptor = undefined;
3376+
var fds: [2]uv.uv_file = undefined;
3377+
if (uv.uv_pipe(&fds, 0, 0).errEnum()) |e| {
3378+
const err = Error.fromCode(e, .pipe);
3379+
log("pipe() = {}", .{err});
3380+
return .{ .err = err };
3381+
}
3382+
out[0] = .fromUV(fds[0]);
3383+
out[1] = .fromUV(fds[1]);
3384+
log("pipe() = [{}, {}]", .{ out[0], out[1] });
3385+
return .{ .result = out };
33753386
}
33763387

33773388
var fds: [2]i32 = undefined;
33783389
const rc = syscall.pipe(&fds);
33793390
if (Maybe([2]bun.FileDescriptor).errnoSys(rc, .pipe)) |err| {
3391+
log("pipe() = {}", .{err});
33803392
return err;
33813393
}
33823394
log("pipe() = [{d}, {d}]", .{ fds[0], fds[1] });

0 commit comments

Comments
 (0)