authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-03 19:04:28-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-04 00:27:09-08:00
logfa315b1060ac2550e3479775cd22871b3df164ad
tree57fa198fba1778ff62c29b28de7185cb3dbcfeb2
parent2c22c3dabf08156eee00b927baf7d6a845a4c98d

std.Io.Threaded: improve posix process creation

* cache /dev/null after opening * make opening /dev/null cancelable * avoid unreachable even when OS does something unexpected

5 files changed, 157 insertions(+), 119 deletions(-)

lib/std/Io/Threaded.zig+154-42
......@@ -64,7 +64,7 @@ stderr_writer_initialized: bool = false,
6464argv0: Argv0,
6565environ: Environ,
6666
67nul_handle: if (is_windows) ?windows.HANDLE else void = if (is_windows) null else {},
67null_file: NullFile = .{},
6868
6969pub const Argv0 = switch (native_os) {
7070 .openbsd, .haiku => struct {
......@@ -123,6 +123,34 @@ const Environ = struct {
123123 };
124124};
125125
126pub const NullFile = switch (native_os) {
127 .windows => struct {
128 handle: ?windows.HANDLE = null,
129
130 fn deinit(this: *@This()) void {
131 if (this.handle) |handle| {
132 windows.CloseHandle(handle);
133 this.handle = null;
134 }
135 }
136 },
137 .wasi, .ios, .tvos, .visionos, .watchos => struct {
138 fn deinit(this: @This()) void {
139 _ = this;
140 }
141 },
142 else => struct {
143 fd: posix.fd_t = -1,
144
145 fn deinit(this: *@This()) void {
146 if (this.fd >= 0) {
147 posix.close(this.fd);
148 this.fd = -1;
149 }
150 }
151 },
152};
153
126154pub const Pid = if (native_os == .linux) enum(posix.pid_t) {
127155 unknown = 0,
128156 _,
......@@ -1249,18 +1277,14 @@ pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
12491277
12501278pub fn deinit(t: *Threaded) void {
12511279 t.join();
1252 if (is_windows) {
1253 if (t.wsa.status == .initialized) {
1254 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
1255 }
1256 if (t.nul_handle) |handle| {
1257 windows.CloseHandle(handle);
1258 }
1280 if (is_windows and t.wsa.status == .initialized) {
1281 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
12591282 }
12601283 if (posix.Sigaction != void and t.have_signal_handler) {
12611284 if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);
12621285 if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null);
12631286 }
1287 t.null_file.deinit();
12641288 t.* = undefined;
12651289}
12661290
......@@ -1429,9 +1453,9 @@ pub fn io(t: *Threaded) Io {
14291453 .unlockStderr = unlockStderr,
14301454 .processSetCurrentDir = processSetCurrentDir,
14311455 .processReplace = processReplace,
1432 .processReplacePath = processReplacePath, // TODO audit for cancelation and unreachable
1433 .processSpawn = processSpawn, // TODO audit for cancelation and unreachable
1434 .processSpawnPath = processSpawnPath, // TODO audit for cancelation and unreachable
1456 .processReplacePath = processReplacePath,
1457 .processSpawn = processSpawn,
1458 .processSpawnPath = processSpawnPath,
14351459 .childWait = childWait, // TODO audit for cancelation and unreachable
14361460 .childKill = childKill, // TODO audit for cancelation and unreachable
14371461
......@@ -1656,6 +1680,7 @@ const have_wait4 = switch (native_os) {
16561680 else => false,
16571681};
16581682
1683const open_sym = if (posix.lfs64_abi) posix.system.open64 else posix.system.open;
16591684const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;
16601685const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;
16611686const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat;
......@@ -12856,51 +12881,30 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp
1285612881 // turns out, we `dup2` everything anyway, so there's no need!
1285712882 const pipe_flags: posix.O = .{ .CLOEXEC = true };
1285812883
12859 const stdin_pipe = if (options.stdin == .pipe) try posix.pipe2(pipe_flags) else undefined;
12884 const stdin_pipe = if (options.stdin == .pipe) try pipe2(pipe_flags) else undefined;
1286012885 errdefer if (options.stdin == .pipe) {
1286112886 destroyPipe(stdin_pipe);
1286212887 };
1286312888
12864 const stdout_pipe = if (options.stdout == .pipe) try posix.pipe2(pipe_flags) else undefined;
12889 const stdout_pipe = if (options.stdout == .pipe) try pipe2(pipe_flags) else undefined;
1286512890 errdefer if (options.stdout == .pipe) {
1286612891 destroyPipe(stdout_pipe);
1286712892 };
1286812893
12869 const stderr_pipe = if (options.stderr == .pipe) try posix.pipe2(pipe_flags) else undefined;
12894 const stderr_pipe = if (options.stderr == .pipe) try pipe2(pipe_flags) else undefined;
1287012895 errdefer if (options.stderr == .pipe) {
1287112896 destroyPipe(stderr_pipe);
1287212897 };
1287312898
1287412899 const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore);
12875 // TODO: cache file handle of /dev/null!
12876 const dev_null_fd = if (any_ignore)
12877 posix.openZ("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch |err| switch (err) {
12878 error.PathAlreadyExists => unreachable,
12879 error.NoSpaceLeft => unreachable,
12880 error.FileTooBig => unreachable,
12881 error.DeviceBusy => unreachable,
12882 error.FileLocksUnsupported => unreachable,
12883 error.BadPathName => unreachable, // Windows-only
12884 error.WouldBlock => unreachable,
12885 error.NetworkNotFound => unreachable, // Windows-only
12886 error.Canceled => unreachable, // temporarily in the posix error set
12887 error.SharingViolation => unreachable, // Windows-only
12888 error.PipeBusy => unreachable, // not a pipe
12889 error.AntivirusInterference => unreachable, // Windows-only
12890 else => |e| return e,
12891 }
12892 else
12893 undefined;
12894 defer {
12895 if (any_ignore) posix.close(dev_null_fd);
12896 }
12900 const dev_null_fd = if (any_ignore) try getDevNullFd(t) else undefined;
1289712901
1289812902 const prog_pipe: [2]posix.fd_t = p: {
1289912903 if (options.progress_node.index == .none) {
1290012904 break :p .{ -1, -1 };
1290112905 } else {
1290212906 // We use CLOEXEC for the same reason as in `pipe_flags`.
12903 break :p try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
12907 break :p try pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
1290412908 }
1290512909 };
1290612910 errdefer destroyPipe(prog_pipe);
......@@ -12938,13 +12942,23 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp
1293812942
1293912943 // This pipe communicates to the parent errors in the child between `fork` and `execvpe`.
1294012944 // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds.
12941 const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true });
12945 const err_pipe: [2]posix.fd_t = try pipe2(.{ .CLOEXEC = true });
1294212946 errdefer destroyPipe(err_pipe);
1294312947
1294412948 t.scanEnviron(); // for PATH
1294512949 const PATH = t.environ.string.PATH orelse default_PATH;
1294612950
12947 const pid_result = try posix.fork();
12951 const pid_result: posix.pid_t = fork: {
12952 const rc = posix.system.fork();
12953 switch (posix.errno(rc)) {
12954 .SUCCESS => break :fork @intCast(rc),
12955 .AGAIN => return error.SystemResources,
12956 .NOMEM => return error.SystemResources,
12957 .NOSYS => return error.OperationUnsupported,
12958 else => |err| return posix.unexpectedErrno(err),
12959 }
12960 };
12961
1294812962 if (pid_result == 0) {
1294912963 // We are the child.
1295012964 if (Thread.current) |current_thread| current_thread.cancel_protection = .blocked;
......@@ -13030,6 +13044,45 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp
1303013044 };
1303113045}
1303213046
13047fn getDevNullFd(t: *Threaded) !posix.fd_t {
13048 {
13049 t.mutex.lock();
13050 defer t.mutex.unlock();
13051 if (t.null_file.fd != -1) return t.null_file.fd;
13052 }
13053 const syscall: Syscall = try .start();
13054 while (true) {
13055 const rc = open_sym("/dev/null", .{ .ACCMODE = .RDWR }, 0);
13056 switch (posix.errno(rc)) {
13057 .SUCCESS => {
13058 syscall.finish();
13059 const fresh_fd: posix.fd_t = @intCast(rc);
13060 t.mutex.lock(); // Another thread might have won the race.
13061 defer t.mutex.unlock();
13062 if (t.null_file.fd != -1) {
13063 posix.close(fresh_fd);
13064 return t.null_file.fd;
13065 } else {
13066 t.null_file.fd = fresh_fd;
13067 return fresh_fd;
13068 }
13069 },
13070 .INTR => {
13071 try syscall.checkCancel();
13072 continue;
13073 },
13074 .ACCES => return syscall.fail(error.AccessDenied),
13075 .MFILE => return syscall.fail(error.ProcessFdQuotaExceeded),
13076 .NFILE => return syscall.fail(error.SystemFdQuotaExceeded),
13077 .NODEV => return syscall.fail(error.NoDevice),
13078 .NOENT => return syscall.fail(error.FileNotFound),
13079 .NOMEM => return syscall.fail(error.SystemResources),
13080 .PERM => return syscall.fail(error.PermissionDenied),
13081 else => |err| return syscall.unexpectedErrno(err),
13082 }
13083 }
13084}
13085
1303313086fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child {
1303413087 const t: *Threaded = @ptrCast(@alignCast(userdata));
1303513088 const spawned = try spawnPosix(t, options);
......@@ -13639,7 +13692,7 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {
1363913692 {
1364013693 t.mutex.lock();
1364113694 defer t.mutex.unlock();
13642 if (t.nul_handle) |handle| return handle;
13695 if (t.null_file.handle) |handle| return handle;
1364313696 }
1364413697
1364513698 const device_path = [_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' };
......@@ -13686,11 +13739,11 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {
1368613739 syscall.finish();
1368713740 t.mutex.lock(); // Another thread might have won the race.
1368813741 defer t.mutex.unlock();
13689 if (t.nul_handle) |prev_handle| {
13742 if (t.null_file.handle) |prev_handle| {
1369013743 windows.CloseHandle(fresh_handle);
1369113744 return prev_handle;
1369213745 } else {
13693 t.nul_handle = fresh_handle;
13746 t.null_file.handle = fresh_handle;
1369413747 return fresh_handle;
1369513748 }
1369613749 },
......@@ -15177,3 +15230,62 @@ fn unpark(tids: []const UnparkTid, addr_hint: ?*const anyopaque) void {
1517715230 else => comptime unreachable,
1517815231 }
1517915232}
15233
15234pub const PipeError = error{
15235 SystemFdQuotaExceeded,
15236 ProcessFdQuotaExceeded,
15237} || Io.UnexpectedError;
15238
15239pub fn pipe2(flags: posix.O) PipeError![2]posix.fd_t {
15240 var fds: [2]posix.fd_t = undefined;
15241
15242 if (@TypeOf(posix.system.pipe2) != void) {
15243 switch (posix.errno(posix.system.pipe2(&fds, flags))) {
15244 .SUCCESS => return fds,
15245 .INVAL => |err| return errnoBug(err), // Invalid flags
15246 .NFILE => return error.SystemFdQuotaExceeded,
15247 .MFILE => return error.ProcessFdQuotaExceeded,
15248 else => |err| return posix.unexpectedErrno(err),
15249 }
15250 }
15251
15252 switch (posix.errno(posix.system.pipe(&fds))) {
15253 .SUCCESS => {},
15254 .NFILE => return error.SystemFdQuotaExceeded,
15255 .MFILE => return error.ProcessFdQuotaExceeded,
15256 else => |err| return posix.unexpectedErrno(err),
15257 }
15258 errdefer {
15259 posix.close(fds[0]);
15260 posix.close(fds[1]);
15261 }
15262
15263 // https://github.com/ziglang/zig/issues/18882
15264 if (@as(u32, @bitCast(flags)) == 0) return fds;
15265
15266 // CLOEXEC is special, it's a file descriptor flag and must be set using
15267 // F.SETFD.
15268 if (flags.CLOEXEC) for (fds) |fd| {
15269 switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(u32, posix.FD_CLOEXEC)))) {
15270 .SUCCESS => {},
15271 else => |err| return posix.unexpectedErrno(err),
15272 }
15273 };
15274
15275 const new_flags: u32 = f: {
15276 var new_flags = flags;
15277 new_flags.CLOEXEC = false;
15278 break :f @bitCast(new_flags);
15279 };
15280
15281 // Set every other flag affecting the file status using F.SETFL.
15282 if (new_flags != 0) for (fds) |fd| {
15283 switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, new_flags))) {
15284 .SUCCESS => {},
15285 .INVAL => |err| return errnoBug(err),
15286 else => |err| return posix.unexpectedErrno(err),
15287 }
15288 };
15289
15290 return fds;
15291}
lib/std/Io/Threaded/test.zig+1-1
......@@ -187,7 +187,7 @@ test "cancel blocked read from pipe" {
187187 .bInheritHandle = std.os.windows.FALSE,
188188 }),
189189 else => {
190 const pipe = try std.posix.pipe();
190 const pipe = try std.Io.Threaded.pipe2(.{});
191191 read_end = .{ .handle = pipe[0] };
192192 write_end = .{ .handle = pipe[1] };
193193 },
lib/std/os/linux/IoUring/test.zig+1-1
......@@ -280,7 +280,7 @@ test "splice/read" {
280280 var buffer_read = [_]u8{98} ** 20;
281281 try file_src.writeStreamingAll(io, &buffer_write);
282282
283 const fds = try posix.pipe();
283 const fds = try std.Io.Threaded.pipe2(.{});
284284 const pipe_offset: u64 = std.math.maxInt(u64);
285285
286286 const sqe_splice_to_pipe = try ring.splice(0x11111111, fd_src, 0, fds[1], pipe_offset, buffer_write.len);
lib/std/posix.zig-74
......@@ -2105,80 +2105,6 @@ pub fn msync(memory: []align(page_size_min) u8, flags: i32) MSyncError!void {
21052105 }
21062106}
21072107
2108pub const PipeError = error{
2109 SystemFdQuotaExceeded,
2110 ProcessFdQuotaExceeded,
2111} || UnexpectedError;
2112
2113/// Creates a unidirectional data channel that can be used for interprocess communication.
2114pub fn pipe() PipeError![2]fd_t {
2115 var fds: [2]fd_t = undefined;
2116 switch (errno(system.pipe(&fds))) {
2117 .SUCCESS => return fds,
2118 .INVAL => unreachable, // Invalid parameters to pipe()
2119 .FAULT => unreachable, // Invalid fds pointer
2120 .NFILE => return error.SystemFdQuotaExceeded,
2121 .MFILE => return error.ProcessFdQuotaExceeded,
2122 else => |err| return unexpectedErrno(err),
2123 }
2124}
2125
2126pub fn pipe2(flags: O) PipeError![2]fd_t {
2127 if (@TypeOf(system.pipe2) != void) {
2128 var fds: [2]fd_t = undefined;
2129 switch (errno(system.pipe2(&fds, flags))) {
2130 .SUCCESS => return fds,
2131 .INVAL => unreachable, // Invalid flags
2132 .FAULT => unreachable, // Invalid fds pointer
2133 .NFILE => return error.SystemFdQuotaExceeded,
2134 .MFILE => return error.ProcessFdQuotaExceeded,
2135 else => |err| return unexpectedErrno(err),
2136 }
2137 }
2138
2139 const fds: [2]fd_t = try pipe();
2140 errdefer {
2141 close(fds[0]);
2142 close(fds[1]);
2143 }
2144
2145 // https://github.com/ziglang/zig/issues/18882
2146 if (@as(u32, @bitCast(flags)) == 0)
2147 return fds;
2148
2149 // CLOEXEC is special, it's a file descriptor flag and must be set using
2150 // F.SETFD.
2151 if (flags.CLOEXEC) {
2152 for (fds) |fd| {
2153 switch (errno(system.fcntl(fd, F.SETFD, @as(u32, FD_CLOEXEC)))) {
2154 .SUCCESS => {},
2155 .INVAL => unreachable, // Invalid flags
2156 .BADF => unreachable, // Always a race condition
2157 else => |err| return unexpectedErrno(err),
2158 }
2159 }
2160 }
2161
2162 const new_flags: u32 = f: {
2163 var new_flags = flags;
2164 new_flags.CLOEXEC = false;
2165 break :f @bitCast(new_flags);
2166 };
2167 // Set every other flag affecting the file status using F.SETFL.
2168 if (new_flags != 0) {
2169 for (fds) |fd| {
2170 switch (errno(system.fcntl(fd, F.SETFL, new_flags))) {
2171 .SUCCESS => {},
2172 .INVAL => unreachable, // Invalid flags
2173 .BADF => unreachable, // Always a race condition
2174 else => |err| return unexpectedErrno(err),
2175 }
2176 }
2177 }
2178
2179 return fds;
2180}
2181
21822108pub const SysCtlError = error{
21832109 PermissionDenied,
21842110 SystemResources,
lib/std/posix/test.zig+1-1
......@@ -131,7 +131,7 @@ test "pipe" {
131131 if (native_os == .windows or native_os == .wasi)
132132 return error.SkipZigTest;
133133
134 const fds = try posix.pipe();
134 const fds = try std.Io.Threaded.pipe2(.{});
135135 try expect((try posix.write(fds[1], "hello")) == 5);
136136 var buf: [16]u8 = undefined;
137137 try expect((try posix.read(fds[0], buf[0..])) == 5);