authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-03 20:10:13-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-04 00:27:09-08:00
log854c076ff7560de9b3f05ee15e1e30a90d738839
treebaa345e1a493f263dd1195cdd33d286163a5e776
parentfa315b1060ac2550e3479775cd22871b3df164ad

std.Io.Threaded: improve posix spawning

* avoid unreachable when the OS does something unexpected * make waiting for the fork/exec error report cancelable

4 files changed, 129 insertions(+), 208 deletions(-)

lib/std/Io/Threaded.zig+126-62
......@@ -1456,8 +1456,8 @@ pub fn io(t: *Threaded) Io {
14561456 .processReplacePath = processReplacePath,
14571457 .processSpawn = processSpawn,
14581458 .processSpawnPath = processSpawnPath,
1459 .childWait = childWait, // TODO audit for cancelation and unreachable
1460 .childKill = childKill, // TODO audit for cancelation and unreachable
1459 .childWait = childWait,
1460 .childKill = childKill,
14611461
14621462 .progressParentFile = progressParentFile,
14631463
......@@ -11853,38 +11853,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
1185311853 };
1185411854 }
1185511855
11856 if (dir.handle == posix.AT.FDCWD) return;
11857
11858 const syscall: Syscall = try .start();
11859 while (true) {
11860 switch (posix.errno(posix.system.fchdir(dir.handle))) {
11861 .SUCCESS => return syscall.finish(),
11862 .INTR => {
11863 try syscall.checkCancel();
11864 continue;
11865 },
11866 .ACCES => {
11867 syscall.finish();
11868 return error.AccessDenied;
11869 },
11870 .BADF => |err| {
11871 syscall.finish();
11872 return errnoBug(err);
11873 },
11874 .NOTDIR => {
11875 syscall.finish();
11876 return error.NotDir;
11877 },
11878 .IO => {
11879 syscall.finish();
11880 return error.FileSystem;
11881 },
11882 else => |err| {
11883 syscall.finish();
11884 return posix.unexpectedErrno(err);
11885 },
11886 }
11887 }
11856 return fchdir(dir.handle);
1188811857}
1188911858
1189011859pub const PosixAddress = extern union {
......@@ -12960,57 +12929,64 @@ fn spawnPosix(t: *Threaded, options: process.SpawnOptions) process.SpawnError!Sp
1296012929 };
1296112930
1296212931 if (pid_result == 0) {
12963 // We are the child.
12932 defer comptime unreachable; // We are the child.
1296412933 if (Thread.current) |current_thread| current_thread.cancel_protection = .blocked;
12934 const ep1 = err_pipe[1];
1296512935
12966 setUpChildIo(options.stdin, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err);
12967 setUpChildIo(options.stdout, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err);
12968 setUpChildIo(options.stderr, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkBail(err_pipe[1], err);
12936 setUpChildIo(options.stdin, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkBail(ep1, err);
12937 setUpChildIo(options.stdout, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkBail(ep1, err);
12938 setUpChildIo(options.stderr, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkBail(ep1, err);
1296912939
1297012940 if (options.cwd_dir) |cwd| {
12971 posix.fchdir(cwd.handle) catch |err| forkBail(err_pipe[1], err);
12941 fchdir(cwd.handle) catch |err| forkBail(ep1, err);
1297212942 } else if (options.cwd) |cwd| {
12973 posix.chdir(cwd) catch |err| forkBail(err_pipe[1], err);
12943 chdir(cwd) catch |err| forkBail(ep1, err);
1297412944 }
1297512945
1297612946 // Must happen after fchdir above, the cwd file descriptor might be
1297712947 // equal to prog_fileno and be clobbered by this dup2 call.
12978 if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkBail(err_pipe[1], err);
12948 if (prog_pipe[1] != -1) dup2(prog_pipe[1], prog_fileno) catch |err| forkBail(ep1, err);
1297912949
1298012950 if (options.gid) |gid| {
12981 posix.setregid(gid, gid) catch |err| forkBail(err_pipe[1], err);
12951 switch (posix.errno(posix.system.setregid(gid, gid))) {
12952 .SUCCESS => {},
12953 .AGAIN => forkBail(ep1, error.ResourceLimitReached),
12954 .INVAL => forkBail(ep1, error.InvalidUserId),
12955 .PERM => forkBail(ep1, error.PermissionDenied),
12956 else => forkBail(ep1, error.Unexpected),
12957 }
1298212958 }
1298312959
1298412960 if (options.uid) |uid| {
1298512961 switch (posix.errno(posix.system.setreuid(uid, uid))) {
1298612962 .SUCCESS => {},
12987 .AGAIN => forkBail(err_pipe[1], error.ResourceLimitReached),
12988 .INVAL => forkBail(err_pipe[1], error.InvalidUserId),
12989 .PERM => forkBail(err_pipe[1], error.PermissionDenied),
12990 else => forkBail(err_pipe[1], error.Unexpected),
12963 .AGAIN => forkBail(ep1, error.ResourceLimitReached),
12964 .INVAL => forkBail(ep1, error.InvalidUserId),
12965 .PERM => forkBail(ep1, error.PermissionDenied),
12966 else => forkBail(ep1, error.Unexpected),
1299112967 }
1299212968 }
1299312969
1299412970 if (options.pgid) |pid| {
1299512971 switch (posix.errno(posix.system.setpgid(0, pid))) {
1299612972 .SUCCESS => {},
12997 .ACCES => forkBail(err_pipe[1], error.ProcessAlreadyExec),
12998 .INVAL => forkBail(err_pipe[1], error.InvalidProcessGroupId),
12999 .PERM => forkBail(err_pipe[1], error.PermissionDenied),
13000 else => forkBail(err_pipe[1], error.Unexpected),
12973 .ACCES => forkBail(ep1, error.ProcessAlreadyExec),
12974 .INVAL => forkBail(ep1, error.InvalidProcessGroupId),
12975 .PERM => forkBail(ep1, error.PermissionDenied),
12976 else => forkBail(ep1, error.Unexpected),
1300112977 }
1300212978 }
1300312979
1300412980 if (options.start_suspended) {
1300512981 switch (posix.errno(posix.system.kill(posix.system.getpid(), .STOP))) {
1300612982 .SUCCESS => {},
13007 .PERM => forkBail(err_pipe[1], error.PermissionDenied),
13008 else => forkBail(err_pipe[1], error.Unexpected),
12983 .PERM => forkBail(ep1, error.PermissionDenied),
12984 else => forkBail(ep1, error.Unexpected),
1300912985 }
1301012986 }
1301112987
1301212988 const err = posixExecv(options.expand_arg0, argv_buf.ptr[0].?, argv_buf.ptr, envp, PATH);
13013 forkBail(err_pipe[1], err);
12989 forkBail(ep1, err);
1301412990 }
1301512991
1301612992 const pid: posix.pid_t = @intCast(pid_result); // We are the parent.
......@@ -13050,9 +13026,10 @@ fn getDevNullFd(t: *Threaded) !posix.fd_t {
1305013026 defer t.mutex.unlock();
1305113027 if (t.null_file.fd != -1) return t.null_file.fd;
1305213028 }
13029 const mode: u32 = 0;
1305313030 const syscall: Syscall = try .start();
1305413031 while (true) {
13055 const rc = open_sym("/dev/null", .{ .ACCMODE = .RDWR }, 0);
13032 const rc = open_sym("/dev/null", .{ .ACCMODE = .RDWR }, mode);
1305613033 switch (posix.errno(rc)) {
1305713034 .SUCCESS => {
1305813035 syscall.finish();
......@@ -13089,10 +13066,15 @@ fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) proce
1308913066 defer posix.close(spawned.err_fd);
1309013067
1309113068 // Wait for the child to report any errors in or before `execvpe`.
13092 if (readIntFd(t, spawned.err_fd)) |child_err_int| {
13069 if (readIntFd(spawned.err_fd)) |child_err_int| {
1309313070 const child_err: process.SpawnError = @errorCast(@errorFromInt(child_err_int));
1309413071 return child_err;
1309513072 } else |read_err| switch (read_err) {
13073 error.Canceled => {
13074 // We don't want to wait for the error to be reported, but we do
13075 // need to return the child so that it can be cleaned up.
13076 recancelInner();
13077 },
1309613078 error.EndOfStream => {
1309713079 // Write end closed by CLOEXEC at the time of the `execvpe` call,
1309813080 // indicating success.
......@@ -13159,7 +13141,7 @@ fn childKillWindows(t: *Threaded, child: *process.Child, exit_code: windows.UINT
1315913141fn childWaitWindows(child: *process.Child) process.Child.WaitError!process.Child.Term {
1316013142 const handle = child.id.?;
1316113143
13162 var syscall: Syscall = try .start();
13144 const syscall: Syscall = try .start();
1316313145 while (true) switch (windows.kernel32.WaitForSingleObjectEx(handle, windows.INFINITE, windows.FALSE)) {
1316413146 windows.WAIT_OBJECT_0 => break syscall.finish(),
1316513147 windows.WAIT_ABANDONED, windows.WAIT_TIMEOUT => {
......@@ -13393,21 +13375,25 @@ fn writeIntFd(fd: posix.fd_t, value: ErrInt) !void {
1339313375 }
1339413376}
1339513377
13396fn readIntFd(t: *Threaded, fd: posix.fd_t) !ErrInt {
13397 _ = t; // TODO cancelation
13378fn readIntFd(fd: posix.fd_t) !ErrInt {
1339813379 var buffer: [8]u8 = undefined;
1339913380 var i: usize = 0;
13381 const syscall: Syscall = try .start();
1340013382 while (true) {
1340113383 const rc = posix.system.read(fd, buffer[i..].ptr, buffer.len - i);
1340213384 switch (posix.errno(rc)) {
1340313385 .SUCCESS => {
13386 syscall.finish();
1340413387 const n: usize = @intCast(rc);
1340513388 if (n == 0) break;
1340613389 i += n;
1340713390 continue;
1340813391 },
13409 .INTR => continue,
13410 else => |err| return posix.unexpectedErrno(err),
13392 .INTR => {
13393 try syscall.checkCancel();
13394 continue;
13395 },
13396 else => |err| return syscall.unexpectedErrno(err),
1341113397 }
1341213398 }
1341313399 if (buffer.len - i != 0) return error.EndOfStream;
......@@ -13423,10 +13409,10 @@ fn destroyPipe(pipe: [2]posix.fd_t) void {
1342313409
1342413410fn setUpChildIo(stdio: process.SpawnOptions.StdIo, pipe_fd: i32, std_fileno: i32, dev_null_fd: i32) !void {
1342513411 switch (stdio) {
13426 .pipe => try posix.dup2(pipe_fd, std_fileno),
13412 .pipe => try dup2(pipe_fd, std_fileno),
1342713413 .close => posix.close(std_fileno),
1342813414 .inherit => {},
13429 .ignore => try posix.dup2(dev_null_fd, std_fileno),
13415 .ignore => try dup2(dev_null_fd, std_fileno),
1343013416 .file => @panic("TODO implement setUpChildIo when file is used"),
1343113417 }
1343213418}
......@@ -15289,3 +15275,81 @@ pub fn pipe2(flags: posix.O) PipeError![2]posix.fd_t {
1528915275
1529015276 return fds;
1529115277}
15278
15279pub const DupError = error{
15280 ProcessFdQuotaExceeded,
15281 SystemResources,
15282} || Io.UnexpectedError || Io.Cancelable;
15283
15284pub fn dup2(old_fd: posix.fd_t, new_fd: posix.fd_t) DupError!void {
15285 const syscall: Syscall = try .start();
15286 while (true) switch (posix.errno(posix.system.dup2(old_fd, new_fd))) {
15287 .SUCCESS => return syscall.finish(),
15288 .BUSY, .INTR => {
15289 try syscall.checkCancel();
15290 continue;
15291 },
15292 .INVAL => |err| return syscall.errnoBug(err), // invalid parameters
15293 .BADF => |err| return syscall.errnoBug(err), // use after free
15294 .MFILE => return syscall.fail(error.ProcessFdQuotaExceeded),
15295 .NOMEM => return syscall.fail(error.SystemResources),
15296 else => |err| return syscall.unexpectedErrno(err),
15297 };
15298}
15299
15300pub const FchdirError = error{
15301 AccessDenied,
15302 NotDir,
15303 FileSystem,
15304} || Io.Cancelable || Io.UnexpectedError;
15305
15306pub fn fchdir(fd: posix.fd_t) FchdirError!void {
15307 if (fd == posix.AT.FDCWD) return;
15308 const syscall: Syscall = try .start();
15309 while (true) switch (posix.errno(posix.system.fchdir(fd))) {
15310 .SUCCESS => return syscall.finish(),
15311 .INTR => {
15312 try syscall.checkCancel();
15313 continue;
15314 },
15315 .ACCES => return syscall.fail(error.AccessDenied),
15316 .NOTDIR => return syscall.fail(error.NotDir),
15317 .IO => return syscall.fail(error.FileSystem),
15318 .BADF => |err| return syscall.errnoBug(err),
15319 else => |err| return syscall.unexpectedErrno(err),
15320 };
15321}
15322
15323pub const ChdirError = error{
15324 AccessDenied,
15325 FileSystem,
15326 SymLinkLoop,
15327 NameTooLong,
15328 FileNotFound,
15329 SystemResources,
15330 NotDir,
15331 BadPathName,
15332} || Io.Cancelable || Io.UnexpectedError;
15333
15334pub fn chdir(dir_path: []const u8) ChdirError!void {
15335 var path_buffer: [posix.PATH_MAX]u8 = undefined;
15336 const dir_path_posix = try pathToPosix(dir_path, &path_buffer);
15337 const syscall: Syscall = try .start();
15338 while (true) switch (posix.errno(posix.system.chdir(dir_path_posix))) {
15339 .SUCCESS => return syscall.finish(),
15340 .INTR => {
15341 try syscall.checkCancel();
15342 continue;
15343 },
15344 .ACCES => return syscall.fail(error.AccessDenied),
15345 .IO => return syscall.fail(error.FileSystem),
15346 .LOOP => return syscall.fail(error.SymLinkLoop),
15347 .NAMETOOLONG => return syscall.fail(error.NameTooLong),
15348 .NOENT => return syscall.fail(error.FileNotFound),
15349 .NOMEM => return syscall.fail(error.SystemResources),
15350 .NOTDIR => return syscall.fail(error.NotDir),
15351 .ILSEQ => return syscall.fail(error.BadPathName),
15352 .FAULT => |err| return syscall.errnoBug(err),
15353 else => |err| return syscall.unexpectedErrno(err),
15354 };
15355}
lib/std/posix.zig-112
......@@ -772,29 +772,6 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: O, mode: mode_t) O
772772 }
773773}
774774
775pub fn dup(old_fd: fd_t) !fd_t {
776 const rc = system.dup(old_fd);
777 return switch (errno(rc)) {
778 .SUCCESS => return @intCast(rc),
779 .MFILE => error.ProcessFdQuotaExceeded,
780 .BADF => unreachable, // invalid file descriptor
781 else => |err| return unexpectedErrno(err),
782 };
783}
784
785pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void {
786 while (true) {
787 switch (errno(system.dup2(old_fd, new_fd))) {
788 .SUCCESS => return,
789 .BUSY, .INTR => continue,
790 .MFILE => return error.ProcessFdQuotaExceeded,
791 .INVAL => unreachable, // invalid parameters passed to dup2
792 .BADF => unreachable, // invalid file descriptor
793 else => |err| return unexpectedErrno(err),
794 }
795 }
796}
797
798775pub fn getppid() pid_t {
799776 return system.getppid();
800777}
......@@ -832,85 +809,6 @@ pub fn getcwd(out_buffer: []u8) GetCwdError![]u8 {
832809 }
833810}
834811
835/// Same as `mkdir` but the parameter is null-terminated.
836/// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
837/// On WASI, `dir_path` should be encoded as valid UTF-8.
838/// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding.
839pub const ChangeCurDirError = error{
840 AccessDenied,
841 FileSystem,
842 SymLinkLoop,
843 NameTooLong,
844 FileNotFound,
845 SystemResources,
846 NotDir,
847 /// WASI: file paths must be valid UTF-8.
848 /// Windows: file paths provided by the user must be valid WTF-8.
849 /// https://wtf-8.codeberg.page/
850 BadPathName,
851} || UnexpectedError;
852
853/// Changes the current working directory of the calling process.
854/// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
855/// On WASI, `dir_path` should be encoded as valid UTF-8.
856/// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding.
857pub fn chdir(dir_path: []const u8) ChangeCurDirError!void {
858 if (native_os == .wasi and !builtin.link_libc) {
859 @compileError("unsupported OS");
860 } else if (native_os == .windows) {
861 @compileError("unsupported OS");
862 } else {
863 const dir_path_c = try toPosixPath(dir_path);
864 return chdirZ(&dir_path_c);
865 }
866}
867
868/// Same as `chdir` except the parameter is null-terminated.
869/// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
870/// On WASI, `dir_path` should be encoded as valid UTF-8.
871/// On other platforms, `dir_path` is an opaque sequence of bytes with no particular encoding.
872pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void {
873 if (native_os == .windows) {
874 @compileError("unsupported OS");
875 } else if (native_os == .wasi and !builtin.link_libc) {
876 @compileError("unsupported OS");
877 }
878 switch (errno(system.chdir(dir_path))) {
879 .SUCCESS => return,
880 .ACCES => return error.AccessDenied,
881 .FAULT => unreachable,
882 .IO => return error.FileSystem,
883 .LOOP => return error.SymLinkLoop,
884 .NAMETOOLONG => return error.NameTooLong,
885 .NOENT => return error.FileNotFound,
886 .NOMEM => return error.SystemResources,
887 .NOTDIR => return error.NotDir,
888 .ILSEQ => return error.BadPathName,
889 else => |err| return unexpectedErrno(err),
890 }
891}
892
893pub const FchdirError = error{
894 AccessDenied,
895 NotDir,
896 FileSystem,
897} || UnexpectedError;
898
899pub fn fchdir(dirfd: fd_t) FchdirError!void {
900 if (dirfd == AT.FDCWD) return;
901 while (true) {
902 switch (errno(system.fchdir(dirfd))) {
903 .SUCCESS => return,
904 .ACCES => return error.AccessDenied,
905 .BADF => unreachable,
906 .NOTDIR => return error.NotDir,
907 .INTR => continue,
908 .IO => return error.FileSystem,
909 else => |err| return unexpectedErrno(err),
910 }
911 }
912}
913
914812pub const SetEidError = error{
915813 InvalidUserId,
916814 PermissionDenied,
......@@ -956,16 +854,6 @@ pub fn setegid(uid: uid_t) SetEidError!void {
956854 }
957855}
958856
959pub fn setregid(rgid: gid_t, egid: gid_t) SetIdError!void {
960 switch (errno(system.setregid(rgid, egid))) {
961 .SUCCESS => return,
962 .AGAIN => return error.ResourceLimitReached,
963 .INVAL => return error.InvalidUserId,
964 .PERM => return error.PermissionDenied,
965 else => |err| return unexpectedErrno(err),
966 }
967}
968
969857pub fn getuid() uid_t {
970858 return system.getuid();
971859}
lib/std/posix/test.zig-31
......@@ -433,37 +433,6 @@ test "sigset add/del" {
433433 }
434434}
435435
436test "dup & dup2" {
437 switch (native_os) {
438 .linux, .illumos => {},
439 else => return error.SkipZigTest,
440 }
441
442 const io = testing.io;
443
444 var tmp = tmpDir(.{});
445 defer tmp.cleanup();
446
447 {
448 var file = try tmp.dir.createFile(io, "os_dup_test", .{});
449 defer file.close(io);
450
451 var duped = Io.File{ .handle = try posix.dup(file.handle) };
452 defer duped.close(io);
453 try duped.writeStreamingAll(io, "dup");
454
455 // Tests aren't run in parallel so using the next fd shouldn't be an issue.
456 const new_fd = duped.handle + 1;
457 try posix.dup2(file.handle, new_fd);
458 var dup2ed = Io.File{ .handle = new_fd };
459 defer dup2ed.close(io);
460 try dup2ed.writeStreamingAll(io, "dup2");
461 }
462
463 var buffer: [8]u8 = undefined;
464 try expectEqualStrings("dupdup2", try tmp.dir.readFile(io, "os_dup_test", &buffer));
465}
466
467436test "getpid" {
468437 if (native_os == .wasi) return error.SkipZigTest;
469438 if (native_os == .windows) return error.SkipZigTest;
test/standalone/posix/cwd.zig+3-3
......@@ -31,7 +31,7 @@ fn test_chdir_self() !void {
3131 const old_cwd = try std.posix.getcwd(old_cwd_buf[0..]);
3232
3333 // Try changing to the current directory
34 try std.posix.chdir(old_cwd);
34 try std.Io.Threaded.chdir(old_cwd);
3535 try expect_cwd(old_cwd);
3636}
3737
......@@ -42,7 +42,7 @@ fn test_chdir_absolute() !void {
4242 const parent = std.fs.path.dirname(old_cwd) orelse unreachable; // old_cwd should be absolute
4343
4444 // Try changing to the parent via a full path
45 try std.posix.chdir(parent);
45 try std.Io.Threaded.chdir(parent);
4646
4747 try expect_cwd(parent);
4848}
......@@ -63,7 +63,7 @@ fn test_chdir_relative(gpa: Allocator, io: Io) !void {
6363 defer gpa.free(expected_path);
6464
6565 // change current working directory to new test directory
66 try std.posix.chdir(relative_dir_name);
66 try std.Io.Threaded.chdir(relative_dir_name);
6767
6868 var new_cwd_buf: [path_max]u8 = undefined;
6969 const new_cwd = try std.posix.getcwd(new_cwd_buf[0..]);