From 8448ccd54b7ad552588a3773fd7acfe858e672d8 Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Tue, 31 Dec 2024 11:42:14 +0800 Subject: [PATCH 01/13] std.process.Child: use clone() instead of fork() --- lib/std/process/Child.zig | 176 +++++++++++++++++++++++++++++--------- 1 file changed, 135 insertions(+), 41 deletions(-) diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index f9dc28aaaf05..85ce70b187f0 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -73,7 +73,7 @@ cwd: ?[]const u8, /// Once that is done, `cwd` will be deprecated in favor of this field. cwd_dir: ?fs.Dir = null, -err_pipe: if (native_os == .windows) void else ?posix.fd_t, +err_pipe: ?if (native_os == .windows) void else posix.fd_t, expand_arg0: Arg0Expand, @@ -501,6 +501,29 @@ fn cleanupStreams(self: *ChildProcess) void { } } +fn cleanupAfterWait(self: *ChildProcess, status: u32) !Term { + if (native_os != .linux) { + if (self.err_pipe) |err_pipe| { + defer destroyPipe(err_pipe); + + // Write maxInt(ErrInt) to the write end of the err_pipe. This is after + // waitpid, so this write is guaranteed to be after the child + // pid potentially wrote an error. This way we can do a blocking + // read on the error pipe and either get maxInt(ErrInt) (no error) or + // an error code. + try writeIntFd(err_pipe[1], maxInt(ErrInt)); + const err_int = try readIntFd(err_pipe[0]); + // Here we potentially return the fork child's error from the parent + // pid. + if (err_int != maxInt(ErrInt)) { + return @as(SpawnError, @errorCast(@errorFromInt(err_int))); + } + } + } + + return statusToTerm(status); +} + fn statusToTerm(status: u32) Term { return if (posix.W.IFEXITED(status)) Term{ .Exited = posix.W.EXITSTATUS(status) } @@ -512,6 +535,57 @@ fn statusToTerm(status: u32) Term { Term{ .Unknown = status }; } +const RetErr = if (native_os == .linux) ?SpawnError else posix.fd_t; + +const ChildArg = struct { + self: *ChildProcess, + stdin_pipe_0: posix.fd_t, + stdout_pipe_1: posix.fd_t, + stderr_pipe_1: posix.fd_t, + prog_pipe_1: posix.fd_t, + dev_null_fd: posix.fd_t, + argv_buf: [:null]?[*:0]const u8, + envp: [*:null]const ?[*:0]const u8, + ret_err: RetErr, +}; + +fn spawnPosixChildHelper(arg: usize) callconv(.c) u8 { + const child_arg: *ChildArg = @ptrFromInt(arg); + const prog_fileno = 3; + + setUpChildIo(child_arg.self.stdin_behavior, child_arg.stdin_pipe_0, posix.STDIN_FILENO, child_arg.dev_null_fd) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + setUpChildIo(child_arg.self.stdout_behavior, child_arg.stdout_pipe_1, posix.STDOUT_FILENO, child_arg.dev_null_fd) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + setUpChildIo(child_arg.self.stderr_behavior, child_arg.stderr_pipe_1, posix.STDERR_FILENO, child_arg.dev_null_fd) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + + if (child_arg.self.cwd_dir) |cwd| { + posix.fchdir(cwd.fd) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + } else if (child_arg.self.cwd) |cwd| { + posix.chdir(cwd) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + } + + // Must happen after fchdir above, the cwd file descriptor might be + // equal to prog_fileno and be clobbered by this dup2 call. + if (child_arg.prog_pipe_1 != -1) posix.dup2(child_arg.prog_pipe_1, prog_fileno) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + + if (child_arg.self.gid) |gid| { + posix.setregid(gid, gid) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + } + + if (child_arg.self.uid) |uid| { + posix.setreuid(uid, uid) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + } + + if (child_arg.self.pgid) |pid| { + posix.setpgid(0, pid) catch |err| return forkChildErrReport(&child_arg.ret_err, err); + } + + const err = switch (child_arg.self.expand_arg0) { + .expand => posix.execvpeZ_expandArg0(.expand, child_arg.argv_buf.ptr[0].?, child_arg.argv_buf.ptr, child_arg.envp), + .no_expand => posix.execvpeZ_expandArg0(.no_expand, child_arg.argv_buf.ptr[0].?, child_arg.argv_buf.ptr, child_arg.envp), + }; + return forkChildErrReport(&child_arg.ret_err, err); +} + fn spawnPosix(self: *ChildProcess) SpawnError!void { // The child process does need to access (one end of) these pipes. However, // we must initially set CLOEXEC to avoid a race condition. If another thread @@ -610,45 +684,54 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { } }; - // This pipe communicates to the parent errors in the child between `fork` and `execvpe`. - // It is closed by the child (via CLOEXEC) without writing if `execvpe` succeeds. - const err_pipe: [2]posix.fd_t = try posix.pipe2(.{ .CLOEXEC = true }); - errdefer destroyPipe(err_pipe); - - const pid_result = try posix.fork(); - if (pid_result == 0) { - // we are the child - setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); - setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); - setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err); - - if (self.cwd_dir) |cwd| { - posix.fchdir(cwd.fd) catch |err| forkChildErrReport(err_pipe[1], err); - } else if (self.cwd) |cwd| { - posix.chdir(cwd) catch |err| forkChildErrReport(err_pipe[1], err); - } - - // Must happen after fchdir above, the cwd file descriptor might be - // equal to prog_fileno and be clobbered by this dup2 call. - if (prog_pipe[1] != -1) posix.dup2(prog_pipe[1], prog_fileno) catch |err| forkChildErrReport(err_pipe[1], err); - - if (self.gid) |gid| { - posix.setregid(gid, gid) catch |err| forkChildErrReport(err_pipe[1], err); + // This pipe is used to communicate errors between the time of fork + // and execve from the child process to the parent process. + const err_pipe = blk: { + if (native_os != .linux) { + break :blk try posix.pipe2(.{ .CLOEXEC = true }); + } else { + break :blk [_]posix.fd_t{ -1, -1 }; } + }; + errdefer destroyPipe(err_pipe); - if (self.uid) |uid| { - posix.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err); - } + var child_arg = ChildArg{ + .self = self, + .stdin_pipe_0 = stdin_pipe[0], + .stdout_pipe_1 = stdout_pipe[1], + .stderr_pipe_1 = stderr_pipe[1], + .prog_pipe_1 = prog_pipe[1], + .dev_null_fd = dev_null_fd, + .argv_buf = argv_buf, + .envp = envp, + .ret_err = undefined, + }; - if (self.pgid) |pid| { - posix.setpgid(0, pid) catch |err| forkChildErrReport(err_pipe[1], err); + var pid_result: posix.pid_t = undefined; + if (native_os != .linux) { + child_arg.ret_err = err_pipe[1]; + pid_result = try posix.fork(); + if (pid_result == 0) { + immediateExit(spawnPosixChildHelper(@intFromPtr(&child_arg))); } - - const err = switch (self.expand_arg0) { - .expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), - .no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp), + } else { + child_arg.ret_err = null; + // Although the stack is fixed sized, we alloc it here, + // because stack-smashing protection may have higher overhead than allocation. + const stack_size = 0x8000; + // On aarch64, stack address must be a multiple of 16. + const stack = try self.allocator.alignedAlloc(u8, 16, stack_size); + defer self.allocator.free(stack); + const rc = linux.clone(spawnPosixChildHelper, @intFromPtr(stack.ptr) + stack_size, linux.CLONE.VM | linux.CLONE.VFORK | linux.SIG.CHLD, @intFromPtr(&child_arg), null, 0, null); + pid_result = switch (posix.errno(rc)) { + .SUCCESS => @intCast(rc), + .AGAIN => return error.SystemResources, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), }; - forkChildErrReport(err_pipe[1], err); + if (child_arg.ret_err) |err| { + return err; + } } // we are the parent @@ -675,6 +758,9 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { } self.id = pid; + if (native_os != .linux or builtin.zig_backend == .stage2_c) { + self.err_pipe = err_pipe; + } self.term = null; if (self.stdin_behavior == .Pipe) { @@ -987,19 +1073,27 @@ fn destroyPipe(pipe: [2]posix.fd_t) void { if (pipe[0] != pipe[1]) posix.close(pipe[1]); } -// Child of fork calls this to report an error to the fork parent. -// Then the child exits. -fn forkChildErrReport(fd: i32, err: ChildProcess.SpawnError) noreturn { - writeIntFd(fd, @as(ErrInt, @intFromError(err))) catch {}; +fn immediateExit(exitcode: u8) noreturn { // If we're linking libc, some naughty applications may have registered atexit handlers // which we really do not want to run in the fork child. I caught LLVM doing this and // it caused a deadlock instead of doing an exit syscall. In the words of Avril Lavigne, // "Why'd you have to go and make things so complicated?" if (builtin.link_libc) { // The _exit(2) function does nothing but make the exit syscall, unlike exit(3) - std.c._exit(1); + std.c._exit(exitcode); + } + posix.exit(exitcode); +} + +// Child of fork calls this to report an error to the fork parent. +// Returns exit code. +fn forkChildErrReport(retErr: *RetErr, err: ChildProcess.SpawnError) u8 { + if (native_os != .linux) { + writeIntFd(retErr.*, @as(ErrInt, @intFromError(err))) catch {}; + } else { + retErr.* = err; } - posix.exit(1); + return 1; } fn writeIntFd(fd: i32, value: ErrInt) !void { From ecc118017d9ac224fa2bf88b35dc781bd6141c04 Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Tue, 31 Dec 2024 12:42:45 +0800 Subject: [PATCH 02/13] std.process.Child: Fix stage2_c --- lib/std/process/Child.zig | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 85ce70b187f0..8e3d6c189b5a 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -15,6 +15,8 @@ const native_os = builtin.os.tag; const Allocator = std.mem.Allocator; const ChildProcess = @This(); +const use_clone = native_os == .linux and builtin.zig_backend != .stage2_c; + pub const Id = switch (native_os) { .windows => windows.HANDLE, .wasi => void, @@ -502,7 +504,7 @@ fn cleanupStreams(self: *ChildProcess) void { } fn cleanupAfterWait(self: *ChildProcess, status: u32) !Term { - if (native_os != .linux) { + if (!use_clone) { if (self.err_pipe) |err_pipe| { defer destroyPipe(err_pipe); @@ -535,7 +537,7 @@ fn statusToTerm(status: u32) Term { Term{ .Unknown = status }; } -const RetErr = if (native_os == .linux) ?SpawnError else posix.fd_t; +const RetErr = if (use_clone) ?SpawnError else posix.fd_t; const ChildArg = struct { self: *ChildProcess, @@ -687,7 +689,7 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { // This pipe is used to communicate errors between the time of fork // and execve from the child process to the parent process. const err_pipe = blk: { - if (native_os != .linux) { + if (!use_clone) { break :blk try posix.pipe2(.{ .CLOEXEC = true }); } else { break :blk [_]posix.fd_t{ -1, -1 }; @@ -708,7 +710,7 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { }; var pid_result: posix.pid_t = undefined; - if (native_os != .linux) { + if (!use_clone) { child_arg.ret_err = err_pipe[1]; pid_result = try posix.fork(); if (pid_result == 0) { @@ -758,7 +760,7 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { } self.id = pid; - if (native_os != .linux or builtin.zig_backend == .stage2_c) { + if (!use_clone) { self.err_pipe = err_pipe; } self.term = null; @@ -1088,7 +1090,7 @@ fn immediateExit(exitcode: u8) noreturn { // Child of fork calls this to report an error to the fork parent. // Returns exit code. fn forkChildErrReport(retErr: *RetErr, err: ChildProcess.SpawnError) u8 { - if (native_os != .linux) { + if (!use_clone) { writeIntFd(retErr.*, @as(ErrInt, @intFromError(err))) catch {}; } else { retErr.* = err; From f5ccd1cecc139fd3ec425cd4d7dbf593d6ee77ac Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Tue, 31 Dec 2024 14:45:58 +0800 Subject: [PATCH 03/13] std.process.Child: block signals during clone() --- lib/std/process/Child.zig | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 8e3d6c189b5a..9c82f93ff09d 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -548,6 +548,7 @@ const ChildArg = struct { dev_null_fd: posix.fd_t, argv_buf: [:null]?[*:0]const u8, envp: [*:null]const ?[*:0]const u8, + sigmask: ?*posix.sigset_t, ret_err: RetErr, }; @@ -581,6 +582,28 @@ fn spawnPosixChildHelper(arg: usize) callconv(.c) u8 { posix.setpgid(0, pid) catch |err| return forkChildErrReport(&child_arg.ret_err, err); } + if (native_os == .linux and child_arg.sigmask != null) { + std.debug.assert(linux.SIG.DFL == null); + for (1..linux.NSIG) |sig| { + if (sig == posix.SIG.KILL or sig == posix.SIG.STOP) { + continue; + } + if (sig > std.math.maxInt(u6)) { + // XXX: We cannot disable all signals. + // sigaction accepts u6, which is too narrow. + break; + } + var old_act: posix.Sigaction = undefined; + const new_act = mem.zeroes(posix.Sigaction); + // Do not use posix.sigaction. It reaches unreachable. + _ = linux.sigaction(@intCast(sig), &new_act, &old_act); + if (old_act.handler.handler == linux.SIG.IGN) { + _ = linux.sigaction(@intCast(sig), &old_act, null); + } + } + posix.sigprocmask(posix.SIG.SETMASK, child_arg.sigmask, null); + } + const err = switch (child_arg.self.expand_arg0) { .expand => posix.execvpeZ_expandArg0(.expand, child_arg.argv_buf.ptr[0].?, child_arg.argv_buf.ptr, child_arg.envp), .no_expand => posix.execvpeZ_expandArg0(.no_expand, child_arg.argv_buf.ptr[0].?, child_arg.argv_buf.ptr, child_arg.envp), @@ -706,6 +729,7 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { .dev_null_fd = dev_null_fd, .argv_buf = argv_buf, .envp = envp, + .sigmask = null, .ret_err = undefined, }; @@ -717,6 +741,10 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { immediateExit(spawnPosixChildHelper(@intFromPtr(&child_arg))); } } else { + var old_mask: posix.sigset_t = undefined; + posix.sigprocmask(posix.SIG.SETMASK, &linux.all_mask, &old_mask); + defer posix.sigprocmask(posix.SIG.SETMASK, &old_mask, null); + child_arg.sigmask = &old_mask; child_arg.ret_err = null; // Although the stack is fixed sized, we alloc it here, // because stack-smashing protection may have higher overhead than allocation. From 048215a55946c204427dec28a2d2b6f43a2c90ad Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Tue, 31 Dec 2024 17:01:23 +0800 Subject: [PATCH 04/13] std.process.Child: use clone3 on x86 and x86_64 --- lib/std/os/linux.zig | 34 ++++++++++++++++++++++++++++++++++ lib/std/os/linux/x86.zig | 32 +++++++++++++++++++++++++++++++- lib/std/os/linux/x86_64.zig | 21 +++++++++++++++++++++ lib/std/process/Child.zig | 37 +++++++++++++++++++++++++++---------- 4 files changed, 113 insertions(+), 11 deletions(-) diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 26bc031df472..b5cd518a6820 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -87,6 +87,40 @@ pub fn clone( ) callconv(.c) usize, @ptrCast(&syscall_bits.clone))(func, stack, flags, arg, ptid, tp, ctid); } +pub const clone_args = extern struct { + flags: u64, + pidfd: u64, + child_tid: u64, + parent_tid: u64, + exit_signal: u64, + stack: u64, + stack_size: u64, + tls: u64, + set_tid: u64, + set_tid_size: u64, + cgroup: u64, +}; + +pub fn clone3( + cl_args: *const clone_args, + size: usize, + func: *const fn (arg: usize) callconv(.C) u8, + arg: usize, +) usize { + // TODO: write asm for other arch. + if (@hasDecl(syscall_bits, "clone3")) { + // Can't directly call a naked function; cast to C calling convention first. + return @as(*const fn ( + cl_args: *const clone_args, + size: usize, + func: *const fn (arg: usize) callconv(.C) u8, + arg: usize, + ) callconv(.C) usize, @ptrCast(&syscall_bits.clone3))(cl_args, size, func, arg); + } else { + return @bitCast(-@as(isize, @intFromEnum(E.NOSYS))); + } +} + pub const ARCH = arch_bits.ARCH; pub const Elf_Symndx = arch_bits.Elf_Symndx; pub const F = arch_bits.F; diff --git a/lib/std/os/linux/x86.zig b/lib/std/os/linux/x86.zig index 4b4c76a4cab3..71bbb70bb6b3 100644 --- a/lib/std/os/linux/x86.zig +++ b/lib/std/os/linux/x86.zig @@ -172,7 +172,37 @@ pub fn clone() callconv(.naked) usize { ); } -pub fn restore() callconv(.naked) noreturn { +pub fn clone3() callconv(.Naked) usize { + asm volatile ( + \\ pushl %%ebx + \\ pushl %%esi + \\ movl 12(%%esp),%%ebx + \\ movl 16(%%esp),%%ecx + \\ movl 20(%%esp),%%edx + \\ movl 24(%%esp),%%esi + \\ movl $435,%%eax // SYS_clone3 + \\ int $128 + \\ testl %%eax,%%eax + \\ jz 1f + \\ popl %%esi + \\ popl %%ebx + \\ retl + \\ + \\1: + \\ .cfi_undefined %%eip + \\ xorl %%ebp,%%ebp + \\ + \\ andl $-16,%%esp + \\ subl $12,%%esp + \\ pushl %%esi + \\ calll *%%edx + \\ movl %%eax,%%ebx + \\ movl $1,%%eax // SYS_exit + \\ int $128 + ); +} + +pub fn restore() callconv(.Naked) noreturn { switch (@import("builtin").zig_backend) { .stage2_c => asm volatile ( \\ movl %[number], %%eax diff --git a/lib/std/os/linux/x86_64.zig b/lib/std/os/linux/x86_64.zig index 90147876c817..bf89a1c149aa 100644 --- a/lib/std/os/linux/x86_64.zig +++ b/lib/std/os/linux/x86_64.zig @@ -135,6 +135,27 @@ pub fn clone() callconv(.naked) usize { ); } +pub fn clone3() callconv(.Naked) usize { + asm volatile ( + \\ movl $435,%%eax // SYS_clone3 + \\ movq %%rcx,%%r8 + \\ syscall + \\ testq %%rax,%%rax + \\ jz 1f + \\ retq + \\ + \\1: .cfi_undefined %%rip + \\ xorl %%ebp,%%ebp + \\ + \\ movq %%r8,%%rdi + \\ callq *%%rdx + \\ movl %%eax,%%edi + \\ movl $60,%%eax // SYS_exit + \\ syscall + \\ + ); +} + pub const restore = restore_rt; pub fn restore_rt() callconv(.naked) noreturn { diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 9c82f93ff09d..c56c4ecc3efe 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -741,10 +741,6 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { immediateExit(spawnPosixChildHelper(@intFromPtr(&child_arg))); } } else { - var old_mask: posix.sigset_t = undefined; - posix.sigprocmask(posix.SIG.SETMASK, &linux.all_mask, &old_mask); - defer posix.sigprocmask(posix.SIG.SETMASK, &old_mask, null); - child_arg.sigmask = &old_mask; child_arg.ret_err = null; // Although the stack is fixed sized, we alloc it here, // because stack-smashing protection may have higher overhead than allocation. @@ -752,13 +748,34 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { // On aarch64, stack address must be a multiple of 16. const stack = try self.allocator.alignedAlloc(u8, 16, stack_size); defer self.allocator.free(stack); - const rc = linux.clone(spawnPosixChildHelper, @intFromPtr(stack.ptr) + stack_size, linux.CLONE.VM | linux.CLONE.VFORK | linux.SIG.CHLD, @intFromPtr(&child_arg), null, 0, null); - pid_result = switch (posix.errno(rc)) { - .SUCCESS => @intCast(rc), - .AGAIN => return error.SystemResources, - .NOMEM => return error.SystemResources, + + var clone_args = mem.zeroes(linux.clone_args); + clone_args.flags = linux.CLONE.VM | linux.CLONE.VFORK | linux.CLONE.CLEAR_SIGHAND; + clone_args.exit_signal = linux.SIG.CHLD; + clone_args.stack = @intFromPtr(stack.ptr); + clone_args.stack_size = stack_size; + var rc = linux.clone3(&clone_args, @sizeOf(linux.clone_args), spawnPosixChildHelper, @intFromPtr(&child_arg)); + switch (linux.E.init(rc)) { + .SUCCESS => {}, + .AGAIN, .NOMEM => return error.SystemResources, + .INVAL, .NOSYS => { + // Fallback to use clone(). + // We need to block signals here because we share VM with child before exec. + // Signal handlers may mess up our memory. + var old_mask: posix.sigset_t = undefined; + posix.sigprocmask(posix.SIG.SETMASK, &linux.all_mask, &old_mask); + defer posix.sigprocmask(posix.SIG.SETMASK, &old_mask, null); + child_arg.sigmask = &old_mask; + rc = linux.clone(spawnPosixChildHelper, @intFromPtr(stack.ptr) + stack_size, linux.CLONE.VM | linux.CLONE.VFORK | linux.SIG.CHLD, @intFromPtr(&child_arg), null, 0, null); + switch (linux.E.init(rc)) { + .SUCCESS => {}, + .AGAIN, .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + }, else => |err| return posix.unexpectedErrno(err), - }; + } + pid_result = @intCast(rc); if (child_arg.ret_err) |err| { return err; } From a07a1290eb6c704ed003fcd0acbbb00922805622 Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Tue, 31 Dec 2024 16:05:32 +0000 Subject: [PATCH 05/13] std.process.Child: use clone3 on arm, arm64, and thumb --- lib/std/os/linux/aarch64.zig | 19 +++++++++++++++++++ lib/std/os/linux/arm.zig | 22 +++++++++++++++++++++- lib/std/os/linux/thumb.zig | 1 + 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/std/os/linux/aarch64.zig b/lib/std/os/linux/aarch64.zig index 649e474310fc..e08eff515023 100644 --- a/lib/std/os/linux/aarch64.zig +++ b/lib/std/os/linux/aarch64.zig @@ -139,6 +139,25 @@ pub fn clone() callconv(.naked) usize { ); } +pub fn clone3() callconv(.Naked) usize { + asm volatile ( + \\ mov x8,#435 // SYS_clone3 + \\ svc #0 + \\ + \\ cbz x0,1f + \\ ret + \\ + \\1: .cfi_undefined lr + \\ mov fp, 0 + \\ mov lr, 0 + \\ + \\ mov x0,x3 + \\ blr x2 + \\ mov x8,#93 // SYS_exit + \\ svc #0 + ); +} + pub const restore = restore_rt; pub fn restore_rt() callconv(.naked) noreturn { diff --git a/lib/std/os/linux/arm.zig b/lib/std/os/linux/arm.zig index 10561da27077..74307a77e8e9 100644 --- a/lib/std/os/linux/arm.zig +++ b/lib/std/os/linux/arm.zig @@ -134,7 +134,27 @@ pub fn clone() callconv(.naked) usize { ); } -pub fn restore() callconv(.naked) noreturn { +pub fn clone3() callconv(.Naked) usize { + asm volatile ( + \\ stmfd sp!,{r7} + \\ mov r7,#435 // SYS_clone3 + \\ svc 0 + \\ tst r0,r0 + \\ beq 1f + \\ ldmfd sp!,{r7} + \\ bx lr + \\ + \\ // https://github.com/llvm/llvm-project/issues/115891 + \\1: mov r11, #0 + \\ mov lr, #0 + \\ mov r0,r3 + \\ bx r2 + \\ mov r7,#1 // SYS_exit + \\ svc 0 + ); +} + +pub fn restore() callconv(.Naked) noreturn { switch (@import("builtin").zig_backend) { .stage2_c => asm volatile ( \\ mov r7, %[number] diff --git a/lib/std/os/linux/thumb.zig b/lib/std/os/linux/thumb.zig index 6816b47c26e1..99ae4dbf2ce8 100644 --- a/lib/std/os/linux/thumb.zig +++ b/lib/std/os/linux/thumb.zig @@ -142,6 +142,7 @@ pub fn syscall6( } pub const clone = @import("arm.zig").clone; +pub const clone3 = @import("arm.zig").clone3; pub fn restore() callconv(.naked) noreturn { asm volatile ( From 4982fe9619f50aafc8067351a9301a23877c97da Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Wed, 1 Jan 2025 12:45:57 +0800 Subject: [PATCH 06/13] std.os.linux: accept u8 in sigaction --- lib/std/os/linux.zig | 23 ++++++++++------------- lib/std/process/Child.zig | 9 --------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index b5cd518a6820..a77ae9a89c88 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -1758,24 +1758,21 @@ pub fn sigprocmask(flags: u32, noalias set: ?*const sigset_t, noalias oldset: ?* return syscall4(.rt_sigprocmask, flags, @intFromPtr(set), @intFromPtr(oldset), NSIG / 8); } -pub fn sigaction(sig: u6, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) usize { - assert(sig >= 1); - assert(sig != SIG.KILL); - assert(sig != SIG.STOP); - +pub fn sigaction(sig: u8, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) usize { var ksa: k_sigaction = undefined; var oldksa: k_sigaction = undefined; const mask_size = @sizeOf(@TypeOf(ksa.mask)); if (act) |new| { - const restorer_fn = if ((new.flags & SA.SIGINFO) != 0) &restore_rt else &restore; - ksa = k_sigaction{ - .handler = new.handler.handler, - .flags = new.flags | SA.RESTORER, - .mask = undefined, - .restorer = @ptrCast(restorer_fn), - }; - @memcpy(@as([*]u8, @ptrCast(&ksa.mask))[0..mask_size], @as([*]const u8, @ptrCast(&new.mask))); + ksa.handler = new.handler.handler; + if (ksa.handler == SIG.DFL or ksa.handler == SIG.IGN) { + ksa.flags = new.flags; + } else { + const restorer_fn = if ((new.flags & SA.SIGINFO) != 0) &restore_rt else &restore; + ksa.flags = new.flags | SA.RESTORER; + ksa.restorer = @ptrCast(restorer_fn); + @memcpy(@as([*]u8, @ptrCast(&ksa.mask))[0..mask_size], @as([*]const u8, @ptrCast(&new.mask))); + } } const ksa_arg = if (act != null) @intFromPtr(&ksa) else 0; diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index c56c4ecc3efe..88a39232f69c 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -585,17 +585,8 @@ fn spawnPosixChildHelper(arg: usize) callconv(.c) u8 { if (native_os == .linux and child_arg.sigmask != null) { std.debug.assert(linux.SIG.DFL == null); for (1..linux.NSIG) |sig| { - if (sig == posix.SIG.KILL or sig == posix.SIG.STOP) { - continue; - } - if (sig > std.math.maxInt(u6)) { - // XXX: We cannot disable all signals. - // sigaction accepts u6, which is too narrow. - break; - } var old_act: posix.Sigaction = undefined; const new_act = mem.zeroes(posix.Sigaction); - // Do not use posix.sigaction. It reaches unreachable. _ = linux.sigaction(@intCast(sig), &new_act, &old_act); if (old_act.handler.handler == linux.SIG.IGN) { _ = linux.sigaction(@intCast(sig), &old_act, null); From acfcdce27bb47fc222093e8d873ba3b528387f7c Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Wed, 1 Jan 2025 13:27:54 +0800 Subject: [PATCH 07/13] std.process.Child: use pid_fd --- lib/std/process/Child.zig | 63 ++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 88a39232f69c..53a519e8019d 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -29,6 +29,9 @@ pub const Id = switch (native_os) { id: Id, thread_handle: if (native_os == .windows) windows.HANDLE else void, +/// Linux only. May be unavailable on older kernel versions. +pid_fd: ?posix.fd_t, + allocator: mem.Allocator, /// The writing end of the child process's standard input pipe. @@ -217,7 +220,8 @@ pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess { .argv = argv, .id = undefined, .thread_handle = undefined, - .err_pipe = if (native_os == .windows) {} else null, + .pid_fd = null, + .err_pipe = null, .term = null, .env_map = null, .cwd = null, @@ -295,10 +299,22 @@ pub fn killPosix(self: *ChildProcess) !Term { self.cleanupStreams(); return term; } - posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) { - error.ProcessNotFound => return error.AlreadyTerminated, - else => return err, - }; + if (self.pid_fd) |pid_fd| { + if (native_os == .linux) { + switch (linux.E.init(linux.pidfd_send_signal(pid_fd, posix.SIG.TERM, null, 0))) { + .SUCCESS => {}, + .SRCH => return error.AlreadyTerminated, + else => |err| return posix.unexpectedErrno(err), + } + } else { + unreachable; + } + } else { + posix.kill(self.id, posix.SIG.TERM) catch |err| switch (err) { + error.ProcessNotFound => return error.AlreadyTerminated, + else => return err, + }; + } self.waitUnwrappedPosix(); return self.term.?; } @@ -341,6 +357,7 @@ pub fn wait(self: *ChildProcess) WaitError!Term { else => self.waitUnwrappedPosix(), } self.id = undefined; + self.pid_fd = null; return self.term.?; } @@ -465,6 +482,34 @@ fn waitUnwrappedWindows(self: *ChildProcess) WaitError!void { fn waitUnwrappedPosix(self: *ChildProcess) void { const res: posix.WaitPidResult = res: { + if (self.pid_fd) |pid_fd| { + if (native_os == .linux) { + var info: linux.siginfo_t = undefined; + var ru: linux.rusage = undefined; + while (true) { + switch (linux.E.init(linux.syscall5(.waitid, @intFromEnum(linux.P.PIDFD), @intCast(pid_fd), @intFromPtr(&info), linux.W.EXITED, @intFromPtr(&ru)))) { + .SUCCESS => break, + .INTR => continue, + else => unreachable, + } + } + if (self.request_resource_usage_statistics) { + self.resource_usage_statistics.rusage = ru; + } + const status: u32 = @bitCast(info.fields.common.second.sigchld.status); + break :res posix.WaitPidResult{ + .pid = info.fields.common.first.piduid.pid, + .status = switch (info.code) { + 1 => (status & 0xff) << 8, // CLD_EXITED + 2, 3 => status & 0x7f, // CLD_KILLED, CLD_DUMPED + else => unreachable, + }, + }; + } else { + unreachable; + } + } + if (self.request_resource_usage_statistics) { switch (native_os) { .linux, .macos, .ios => { @@ -741,13 +786,17 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { defer self.allocator.free(stack); var clone_args = mem.zeroes(linux.clone_args); - clone_args.flags = linux.CLONE.VM | linux.CLONE.VFORK | linux.CLONE.CLEAR_SIGHAND; + var pid_fd: posix.fd_t = undefined; + clone_args.flags = linux.CLONE.VM | linux.CLONE.VFORK | linux.CLONE.CLEAR_SIGHAND | linux.CLONE.PIDFD; clone_args.exit_signal = linux.SIG.CHLD; clone_args.stack = @intFromPtr(stack.ptr); clone_args.stack_size = stack_size; + clone_args.pidfd = @intFromPtr(&pid_fd); var rc = linux.clone3(&clone_args, @sizeOf(linux.clone_args), spawnPosixChildHelper, @intFromPtr(&child_arg)); switch (linux.E.init(rc)) { - .SUCCESS => {}, + .SUCCESS => { + self.pid_fd = pid_fd; + }, .AGAIN, .NOMEM => return error.SystemResources, .INVAL, .NOSYS => { // Fallback to use clone(). From f21fffe52e1ca335255ac3dbada7db2a7f2e29a9 Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Mon, 6 Jan 2025 03:45:43 +0800 Subject: [PATCH 08/13] std.os.linux: generate compile error if clone3 is not implemented --- lib/std/os/linux.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index a77ae9a89c88..48b941c0d734 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -117,7 +117,7 @@ pub fn clone3( arg: usize, ) callconv(.C) usize, @ptrCast(&syscall_bits.clone3))(cl_args, size, func, arg); } else { - return @bitCast(-@as(isize, @intFromEnum(E.NOSYS))); + @compileError("clone3() implementation has not been written for this target"); } } From 1ff4aea225a90a7c96d94d61272ffdc2b8f86224 Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Mon, 6 Jan 2025 04:32:35 +0800 Subject: [PATCH 09/13] std.process.Child: use linux.sigprocmask instead of posix.sigprocmask otherwise libc may silently ignore some RT signals for NPTL --- lib/std/process/Child.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 53a519e8019d..e791357973ed 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -630,14 +630,14 @@ fn spawnPosixChildHelper(arg: usize) callconv(.c) u8 { if (native_os == .linux and child_arg.sigmask != null) { std.debug.assert(linux.SIG.DFL == null); for (1..linux.NSIG) |sig| { - var old_act: posix.Sigaction = undefined; + var old_act: linux.Sigaction = undefined; const new_act = mem.zeroes(posix.Sigaction); _ = linux.sigaction(@intCast(sig), &new_act, &old_act); if (old_act.handler.handler == linux.SIG.IGN) { _ = linux.sigaction(@intCast(sig), &old_act, null); } } - posix.sigprocmask(posix.SIG.SETMASK, child_arg.sigmask, null); + std.debug.assert(linux.sigprocmask(linux.SIG.SETMASK, child_arg.sigmask, null) == 0); } const err = switch (child_arg.self.expand_arg0) { @@ -803,8 +803,8 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { // We need to block signals here because we share VM with child before exec. // Signal handlers may mess up our memory. var old_mask: posix.sigset_t = undefined; - posix.sigprocmask(posix.SIG.SETMASK, &linux.all_mask, &old_mask); - defer posix.sigprocmask(posix.SIG.SETMASK, &old_mask, null); + std.debug.assert(linux.sigprocmask(linux.SIG.SETMASK, &linux.all_mask, &old_mask) == 0); + defer std.debug.assert(linux.sigprocmask(linux.SIG.SETMASK, &old_mask, null) == 0); child_arg.sigmask = &old_mask; rc = linux.clone(spawnPosixChildHelper, @intFromPtr(stack.ptr) + stack_size, linux.CLONE.VM | linux.CLONE.VFORK | linux.SIG.CHLD, @intFromPtr(&child_arg), null, 0, null); switch (linux.E.init(rc)) { From 50303dcc03db49a5811bb76fa5173a994cadda9f Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Wed, 2 Apr 2025 17:55:24 +0800 Subject: [PATCH 10/13] std.process.Child: c backend can also use clone3 now --- lib/std/process/Child.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index e791357973ed..de7e0eef7cac 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -15,7 +15,7 @@ const native_os = builtin.os.tag; const Allocator = std.mem.Allocator; const ChildProcess = @This(); -const use_clone = native_os == .linux and builtin.zig_backend != .stage2_c; +const use_clone = native_os == .linux; pub const Id = switch (native_os) { .windows => windows.HANDLE, From 2adabedd47b3aabb3675e71ece5e8befe2cc0723 Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Wed, 2 Apr 2025 18:40:54 +0800 Subject: [PATCH 11/13] std.process.Child: fix build on non-linux --- lib/std/process/Child.zig | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index de7e0eef7cac..4f2b12d99c98 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -824,8 +824,10 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { // we are the parent errdefer comptime unreachable; // The child is forked; we must not error from now on - posix.close(err_pipe[1]); // make sure only the child holds the write end open - self.err_pipe = err_pipe[0]; + if (!use_clone) { + posix.close(err_pipe[1]); // make sure only the child holds the write end open + self.err_pipe = err_pipe[0]; + } const pid: i32 = @intCast(pid_result); if (self.stdin_behavior == .Pipe) { @@ -845,9 +847,6 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { } self.id = pid; - if (!use_clone) { - self.err_pipe = err_pipe; - } self.term = null; if (self.stdin_behavior == .Pipe) { From 35bdc58f3cec19ab01ba825442560f55b9932aa1 Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Wed, 2 Apr 2025 18:44:06 +0800 Subject: [PATCH 12/13] std.os.linux: remove comments in asm The comment syntax is not supported by both gcc and zig --- lib/std/os/linux/aarch64.zig | 8 ++++---- lib/std/os/linux/arm.zig | 8 ++++---- lib/std/os/linux/x86.zig | 8 ++++---- lib/std/os/linux/x86_64.zig | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/std/os/linux/aarch64.zig b/lib/std/os/linux/aarch64.zig index e08eff515023..533dd20a4877 100644 --- a/lib/std/os/linux/aarch64.zig +++ b/lib/std/os/linux/aarch64.zig @@ -115,7 +115,7 @@ pub fn clone() callconv(.naked) usize { \\ mov x2,x4 \\ mov x3,x5 \\ mov x4,x6 - \\ mov x8,#220 // SYS_clone + \\ mov x8,#220 \\ svc #0 \\ \\ cbz x0,1f @@ -134,14 +134,14 @@ pub fn clone() callconv(.naked) usize { \\ \\ ldp x1,x0,[sp],#16 \\ blr x1 - \\ mov x8,#93 // SYS_exit + \\ mov x8,#93 \\ svc #0 ); } pub fn clone3() callconv(.Naked) usize { asm volatile ( - \\ mov x8,#435 // SYS_clone3 + \\ mov x8,#435 \\ svc #0 \\ \\ cbz x0,1f @@ -153,7 +153,7 @@ pub fn clone3() callconv(.Naked) usize { \\ \\ mov x0,x3 \\ blr x2 - \\ mov x8,#93 // SYS_exit + \\ mov x8,#93 \\ svc #0 ); } diff --git a/lib/std/os/linux/arm.zig b/lib/std/os/linux/arm.zig index 74307a77e8e9..a4147e10eb59 100644 --- a/lib/std/os/linux/arm.zig +++ b/lib/std/os/linux/arm.zig @@ -106,7 +106,7 @@ pub fn clone() callconv(.naked) usize { // r7 r0, r1, r2, r3, r4 asm volatile ( \\ stmfd sp!,{r4,r5,r6,r7} - \\ mov r7,#120 // SYS_clone + \\ mov r7,#120 \\ mov r6,r3 \\ mov r5,r0 \\ mov r0,r2 @@ -127,7 +127,7 @@ pub fn clone() callconv(.naked) usize { \\ \\ mov r0,r6 \\ bl 3f - \\ mov r7,#1 // SYS_exit + \\ mov r7,#1 \\ svc 0 \\ \\3: bx r5 @@ -137,7 +137,7 @@ pub fn clone() callconv(.naked) usize { pub fn clone3() callconv(.Naked) usize { asm volatile ( \\ stmfd sp!,{r7} - \\ mov r7,#435 // SYS_clone3 + \\ mov r7,#435 \\ svc 0 \\ tst r0,r0 \\ beq 1f @@ -149,7 +149,7 @@ pub fn clone3() callconv(.Naked) usize { \\ mov lr, #0 \\ mov r0,r3 \\ bx r2 - \\ mov r7,#1 // SYS_exit + \\ mov r7,#1 \\ svc 0 ); } diff --git a/lib/std/os/linux/x86.zig b/lib/std/os/linux/x86.zig index 71bbb70bb6b3..517fa963cd4f 100644 --- a/lib/std/os/linux/x86.zig +++ b/lib/std/os/linux/x86.zig @@ -146,7 +146,7 @@ pub fn clone() callconv(.naked) usize { \\ movl 24(%%ebp),%%edx \\ movl 28(%%ebp),%%esi \\ movl 32(%%ebp),%%edi - \\ movl $120,%%eax // SYS_clone + \\ movl $120,%%eax \\ int $128 \\ testl %%eax,%%eax \\ jz 1f @@ -167,7 +167,7 @@ pub fn clone() callconv(.naked) usize { \\ popl %%eax \\ calll *%%eax \\ movl %%eax,%%ebx - \\ movl $1,%%eax // SYS_exit + \\ movl $1,%%eax \\ int $128 ); } @@ -180,7 +180,7 @@ pub fn clone3() callconv(.Naked) usize { \\ movl 16(%%esp),%%ecx \\ movl 20(%%esp),%%edx \\ movl 24(%%esp),%%esi - \\ movl $435,%%eax // SYS_clone3 + \\ movl $435,%%eax \\ int $128 \\ testl %%eax,%%eax \\ jz 1f @@ -197,7 +197,7 @@ pub fn clone3() callconv(.Naked) usize { \\ pushl %%esi \\ calll *%%edx \\ movl %%eax,%%ebx - \\ movl $1,%%eax // SYS_exit + \\ movl $1,%%eax \\ int $128 ); } diff --git a/lib/std/os/linux/x86_64.zig b/lib/std/os/linux/x86_64.zig index bf89a1c149aa..409c5620fe95 100644 --- a/lib/std/os/linux/x86_64.zig +++ b/lib/std/os/linux/x86_64.zig @@ -103,7 +103,7 @@ pub fn syscall6( pub fn clone() callconv(.naked) usize { asm volatile ( - \\ movl $56,%%eax // SYS_clone + \\ movl $56,%%eax \\ movq %%rdi,%%r11 \\ movq %%rdx,%%rdi \\ movq %%r8,%%rdx @@ -129,7 +129,7 @@ pub fn clone() callconv(.naked) usize { \\ popq %%rdi \\ callq *%%r9 \\ movl %%eax,%%edi - \\ movl $60,%%eax // SYS_exit + \\ movl $60,%%eax \\ syscall \\ ); @@ -137,7 +137,7 @@ pub fn clone() callconv(.naked) usize { pub fn clone3() callconv(.Naked) usize { asm volatile ( - \\ movl $435,%%eax // SYS_clone3 + \\ movl $435,%%eax \\ movq %%rcx,%%r8 \\ syscall \\ testq %%rax,%%rax @@ -150,7 +150,7 @@ pub fn clone3() callconv(.Naked) usize { \\ movq %%r8,%%rdi \\ callq *%%rdx \\ movl %%eax,%%edi - \\ movl $60,%%eax // SYS_exit + \\ movl $60,%%eax \\ syscall \\ ); From 2216a5dadeb21c690c964308429ec27f39729b83 Mon Sep 17 00:00:00 2001 From: Misaki Kasumi Date: Sat, 5 Apr 2025 05:23:23 +0800 Subject: [PATCH 13/13] std.process.Child: cleanup --- lib/std/process/Child.zig | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 4f2b12d99c98..92c63628165d 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -30,7 +30,7 @@ id: Id, thread_handle: if (native_os == .windows) windows.HANDLE else void, /// Linux only. May be unavailable on older kernel versions. -pid_fd: ?posix.fd_t, +pid_fd: ?if (use_clone) posix.fd_t else void, allocator: mem.Allocator, @@ -548,29 +548,6 @@ fn cleanupStreams(self: *ChildProcess) void { } } -fn cleanupAfterWait(self: *ChildProcess, status: u32) !Term { - if (!use_clone) { - if (self.err_pipe) |err_pipe| { - defer destroyPipe(err_pipe); - - // Write maxInt(ErrInt) to the write end of the err_pipe. This is after - // waitpid, so this write is guaranteed to be after the child - // pid potentially wrote an error. This way we can do a blocking - // read on the error pipe and either get maxInt(ErrInt) (no error) or - // an error code. - try writeIntFd(err_pipe[1], maxInt(ErrInt)); - const err_int = try readIntFd(err_pipe[0]); - // Here we potentially return the fork child's error from the parent - // pid. - if (err_int != maxInt(ErrInt)) { - return @as(SpawnError, @errorCast(@errorFromInt(err_int))); - } - } - } - - return statusToTerm(status); -} - fn statusToTerm(status: u32) Term { return if (posix.W.IFEXITED(status)) Term{ .Exited = posix.W.EXITSTATUS(status) }