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,...@@ -64,7 +64,7 @@ stderr_writer_initialized: bool = false,
64argv0: Argv0,64argv0: Argv0,
65environ: Environ,65environ: Environ,
6666
67nul_handle: if (is_windows) ?windows.HANDLE else void = if (is_windows) null else {},67null_file: NullFile = .{},
6868
69pub const Argv0 = switch (native_os) {69pub const Argv0 = switch (native_os) {
70 .openbsd, .haiku => struct {70 .openbsd, .haiku => struct {
...@@ -123,6 +123,34 @@ const Environ = struct {...@@ -123,6 +123,34 @@ const Environ = struct {
123 };123 };
124};124};
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
126pub const Pid = if (native_os == .linux) enum(posix.pid_t) {154pub const Pid = if (native_os == .linux) enum(posix.pid_t) {
127 unknown = 0,155 unknown = 0,
128 _,156 _,
...@@ -1249,18 +1277,14 @@ pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {...@@ -1249,18 +1277,14 @@ pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
12491277
1250pub fn deinit(t: *Threaded) void {1278pub fn deinit(t: *Threaded) void {
1251 t.join();1279 t.join();
1252 if (is_windows) {1280 if (is_windows and t.wsa.status == .initialized) {
1253 if (t.wsa.status == .initialized) {1281 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
1254 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
1255 }
1256 if (t.nul_handle) |handle| {
1257 windows.CloseHandle(handle);
1258 }
1259 }1282 }
1260 if (posix.Sigaction != void and t.have_signal_handler) {1283 if (posix.Sigaction != void and t.have_signal_handler) {
1261 if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);1284 if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);
1262 if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null);1285 if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null);
1263 }1286 }
1287 t.null_file.deinit();
1264 t.* = undefined;1288 t.* = undefined;
1265}1289}
12661290
...@@ -1429,9 +1453,9 @@ pub fn io(t: *Threaded) Io {...@@ -1429,9 +1453,9 @@ pub fn io(t: *Threaded) Io {
1429 .unlockStderr = unlockStderr,1453 .unlockStderr = unlockStderr,
1430 .processSetCurrentDir = processSetCurrentDir,1454 .processSetCurrentDir = processSetCurrentDir,
1431 .processReplace = processReplace,1455 .processReplace = processReplace,
1432 .processReplacePath = processReplacePath, // TODO audit for cancelation and unreachable1456 .processReplacePath = processReplacePath,
1433 .processSpawn = processSpawn, // TODO audit for cancelation and unreachable1457 .processSpawn = processSpawn,
1434 .processSpawnPath = processSpawnPath, // TODO audit for cancelation and unreachable1458 .processSpawnPath = processSpawnPath,
1435 .childWait = childWait, // TODO audit for cancelation and unreachable1459 .childWait = childWait, // TODO audit for cancelation and unreachable
1436 .childKill = childKill, // TODO audit for cancelation and unreachable1460 .childKill = childKill, // TODO audit for cancelation and unreachable
14371461
...@@ -1656,6 +1680,7 @@ const have_wait4 = switch (native_os) {...@@ -1656,6 +1680,7 @@ const have_wait4 = switch (native_os) {
1656 else => false,1680 else => false,
1657};1681};
16581682
1683const open_sym = if (posix.lfs64_abi) posix.system.open64 else posix.system.open;
1659const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;1684const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;
1660const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;1685const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;
1661const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat;1686const 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...@@ -12856,51 +12881,30 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp
12856 // turns out, we `dup2` everything anyway, so there's no need!12881 // turns out, we `dup2` everything anyway, so there's no need!
12857 const pipe_flags: posix.O = .{ .CLOEXEC = true };12882 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;
12860 errdefer if (options.stdin == .pipe) {12885 errdefer if (options.stdin == .pipe) {
12861 destroyPipe(stdin_pipe);12886 destroyPipe(stdin_pipe);
12862 };12887 };
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;
12865 errdefer if (options.stdout == .pipe) {12890 errdefer if (options.stdout == .pipe) {
12866 destroyPipe(stdout_pipe);12891 destroyPipe(stdout_pipe);
12867 };12892 };
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;
12870 errdefer if (options.stderr == .pipe) {12895 errdefer if (options.stderr == .pipe) {
12871 destroyPipe(stderr_pipe);12896 destroyPipe(stderr_pipe);
12872 };12897 };
1287312898
12874 const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore);12899 const any_ignore = (options.stdin == .ignore or options.stdout == .ignore or options.stderr == .ignore);
12875 // TODO: cache file handle of /dev/null!12900 const dev_null_fd = if (any_ignore) try getDevNullFd(t) else undefined;
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 }
1289712901
12898 const prog_pipe: [2]posix.fd_t = p: {12902 const prog_pipe: [2]posix.fd_t = p: {
12899 if (options.progress_node.index == .none) {12903 if (options.progress_node.index == .none) {
12900 break :p .{ -1, -1 };12904 break :p .{ -1, -1 };
12901 } else {12905 } else {
12902 // We use CLOEXEC for the same reason as in `pipe_flags`.12906 // 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 });
12904 }12908 }
12905 };12909 };
12906 errdefer destroyPipe(prog_pipe);12910 errdefer destroyPipe(prog_pipe);
...@@ -12938,13 +12942,23 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp...@@ -12938,13 +12942,23 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp
1293812942
12939 // This pipe communicates to the parent errors in the child between `fork` and `execvpe`.12943 // This pipe communicates to the parent errors in the child between `fork` and `execvpe`.
12940 // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds.12944 // 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 });
12942 errdefer destroyPipe(err_pipe);12946 errdefer destroyPipe(err_pipe);
1294312947
12944 t.scanEnviron(); // for PATH12948 t.scanEnviron(); // for PATH
12945 const PATH = t.environ.string.PATH orelse default_PATH;12949 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
12948 if (pid_result == 0) {12962 if (pid_result == 0) {
12949 // We are the child.12963 // We are the child.
12950 if (Thread.current) |current_thread| current_thread.cancel_protection = .blocked;12964 if (Thread.current) |current_thread| current_thread.cancel_protection = .blocked;
...@@ -13030,6 +13044,45 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp...@@ -13030,6 +13044,45 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp
13030 };13044 };
13031}13045}
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
13033fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child {13086fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) process.SpawnError!process.Child {
13034 const t: *Threaded = @ptrCast(@alignCast(userdata));13087 const t: *Threaded = @ptrCast(@alignCast(userdata));
13035 const spawned = try spawnPosix(t, options);13088 const spawned = try spawnPosix(t, options);
...@@ -13639,7 +13692,7 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {...@@ -13639,7 +13692,7 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {
13639 {13692 {
13640 t.mutex.lock();13693 t.mutex.lock();
13641 defer t.mutex.unlock();13694 defer t.mutex.unlock();
13642 if (t.nul_handle) |handle| return handle;13695 if (t.null_file.handle) |handle| return handle;
13643 }13696 }
1364413697
13645 const device_path = [_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' };13698 const device_path = [_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' };
...@@ -13686,11 +13739,11 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {...@@ -13686,11 +13739,11 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {
13686 syscall.finish();13739 syscall.finish();
13687 t.mutex.lock(); // Another thread might have won the race.13740 t.mutex.lock(); // Another thread might have won the race.
13688 defer t.mutex.unlock();13741 defer t.mutex.unlock();
13689 if (t.nul_handle) |prev_handle| {13742 if (t.null_file.handle) |prev_handle| {
13690 windows.CloseHandle(fresh_handle);13743 windows.CloseHandle(fresh_handle);
13691 return prev_handle;13744 return prev_handle;
13692 } else {13745 } else {
13693 t.nul_handle = fresh_handle;13746 t.null_file.handle = fresh_handle;
13694 return fresh_handle;13747 return fresh_handle;
13695 }13748 }
13696 },13749 },
...@@ -15177,3 +15230,62 @@ fn unpark(tids: []const UnparkTid, addr_hint: ?*const anyopaque) void {...@@ -15177,3 +15230,62 @@ fn unpark(tids: []const UnparkTid, addr_hint: ?*const anyopaque) void {
15177 else => comptime unreachable,15230 else => comptime unreachable,
15178 }15231 }
15179}15232}
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" {...@@ -187,7 +187,7 @@ test "cancel blocked read from pipe" {
187 .bInheritHandle = std.os.windows.FALSE,187 .bInheritHandle = std.os.windows.FALSE,
188 }),188 }),
189 else => {189 else => {
190 const pipe = try std.posix.pipe();190 const pipe = try std.Io.Threaded.pipe2(.{});
191 read_end = .{ .handle = pipe[0] };191 read_end = .{ .handle = pipe[0] };
192 write_end = .{ .handle = pipe[1] };192 write_end = .{ .handle = pipe[1] };
193 },193 },
lib/std/os/linux/IoUring/test.zig+1-1
...@@ -280,7 +280,7 @@ test "splice/read" {...@@ -280,7 +280,7 @@ test "splice/read" {
280 var buffer_read = [_]u8{98} ** 20;280 var buffer_read = [_]u8{98} ** 20;
281 try file_src.writeStreamingAll(io, &buffer_write);281 try file_src.writeStreamingAll(io, &buffer_write);
282282
283 const fds = try posix.pipe();283 const fds = try std.Io.Threaded.pipe2(.{});
284 const pipe_offset: u64 = std.math.maxInt(u64);284 const pipe_offset: u64 = std.math.maxInt(u64);
285285
286 const sqe_splice_to_pipe = try ring.splice(0x11111111, fd_src, 0, fds[1], pipe_offset, buffer_write.len);286 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 {...@@ -2105,80 +2105,6 @@ pub fn msync(memory: []align(page_size_min) u8, flags: i32) MSyncError!void {
2105 }2105 }
2106}2106}
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
2182pub const SysCtlError = error{2108pub const SysCtlError = error{
2183 PermissionDenied,2109 PermissionDenied,
2184 SystemResources,2110 SystemResources,
lib/std/posix/test.zig+1-1
...@@ -131,7 +131,7 @@ test "pipe" {...@@ -131,7 +131,7 @@ test "pipe" {
131 if (native_os == .windows or native_os == .wasi)131 if (native_os == .windows or native_os == .wasi)
132 return error.SkipZigTest;132 return error.SkipZigTest;
133133
134 const fds = try posix.pipe();134 const fds = try std.Io.Threaded.pipe2(.{});
135 try expect((try posix.write(fds[1], "hello")) == 5);135 try expect((try posix.write(fds[1], "hello")) == 5);
136 var buf: [16]u8 = undefined;136 var buf: [16]u8 = undefined;
137 try expect((try posix.read(fds[0], buf[0..])) == 5);137 try expect((try posix.read(fds[0], buf[0..])) == 5);