From 1acd57a4087d86f38439aafd7d9e7e44621a42be Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 7 Aug 2026 23:00:47 +0900 Subject: [PATCH] os: add process-control syscalls pipe/pipe2, setpgid, kill, sigprocmask, signalfd, chdir, the wait-status decoders, and the signal/flag constants the subprocess stack needs. --- lib/os/os.ww | 96 +++++++++++++- lib/os/ostest.ww | 320 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 415 insertions(+), 1 deletion(-) diff --git a/lib/os/os.ww b/lib/os/os.ww index dc560b7a..95dd9fe4 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -30,20 +30,27 @@ type nr = enum i64 { OPEN = 2, CLOSE = 3, LSEEK = 8, + RTSIGPROCMASK = 14, ACCESS = 21, + PIPE = 22, DUP2 = 33, GETPID = 39, FORK = 57, EXECVE = 59, EXIT = 60, WAIT4 = 61, + KILL = 62, + GETCWD = 79, + CHDIR = 80, RENAME = 82, MKDIR = 83, RMDIR = 84, UNLINK = 87, - GETCWD = 79, + SETPGID = 109, GETDENTS64 = 217, NEWFSTATAT = 262, + SIGNALFD4 = 289, + PIPE2 = 293, }; // open(2) flags. Linux values, matching . Hare names them @@ -106,6 +113,24 @@ export def STDIN_FILENO: i32 = 0; export def STDOUT_FILENO: i32 = 1; export def STDERR_FILENO: i32 = 2; +// ref/hare/sys/+linux/types.ha:82,87. Narrow pipe2 flags used by the native +// process coordinators; the decimal values are Linux O_CLOEXEC and O_NONBLOCK. +export def O_CLOEXEC: i32 = 524288; +export def O_NONBLOCK: i32 = 2048; + +// ref/hare/sys/+linux/types.ha:156,162,305,452-454. ww folds Hare's sys role +// into os; these are the narrow process-control subset required by the +// native test coordinators. +export def SIGKILL: i32 = 9; +export def SIGINT: i32 = 2; +export def SIGTERM: i32 = 15; +export def WNOHANG: i32 = 1; +export def SIG_BLOCK: i32 = 0; +export def SIG_UNBLOCK: i32 = 1; +export def SIG_SETMASK: i32 = 2; +export def SFD_NONBLOCK: i32 = O_NONBLOCK; +export def SFD_CLOEXEC: i32 = O_CLOEXEC; + fn kpath(p: str) *u8 = { if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG let i: i32 = 0; @@ -137,6 +162,20 @@ export fn dup2(oldfd: i32, newfd: i32) i32 = { return syscall2(nr.DUP2, oldfd: i64, newfd: i64): i32; }; +// Linux amd64 pipe(2), ref/hare/sys/+linux/syscallno+x86_64.ha:26. +// Hare's public syscall layer prefers pipe2 (syscalls.ha:367), but the +// completion-token path needs no flags and os is ww's raw syscall floor. +export fn pipe(fds: *[2]i32) i32 = { + return syscall1(nr.PIPE, fds: i64): i32; +}; + +// Linux amd64 pipe2(2), ref/hare/sys/+linux/syscalls.ha:367-369 and +// syscallno+x86_64.ha:297. O_CLOEXEC makes launch-status writers disappear +// atomically at exec rather than leaking into the executed process tree. +export fn pipe2(fds: *[2]i32, flags: i32) i32 = { + return syscall2(nr.PIPE2, fds: i64, flags: i64): i32; +}; + // Fallible wrappers. The error variant is `oserror` (an i64 carrying // -errno). The sum type makes success/failure explicit and lets // callers `?` the result up the stack. @@ -376,6 +415,33 @@ export fn fork() i32 = { return syscall0(nr.FORK): i32; }; +// Raw Linux process-group and signal primitives. Signatures follow the +// pinned Hare syscall order (ref/hare/sys/+linux/syscalls.ha:323,363), +// while returning the raw negative errno used throughout this module. +export fn setpgid(pid: i32, pgid: i32) i32 = { + return syscall2(nr.SETPGID, pid: i64, pgid: i64): i32; +}; + +export fn kill(pid: i32, signal: i32) i32 = { + return syscall2(nr.KILL, pid: i64, signal: i64): i32; +}; + +// ref/hare/sys/+linux/syscalls.ha:642-650 and types.ha:452-454. +// Linux amd64's kernel sigset is one u64; size(u64) keeps the syscall +// argument tied to that declared representation. +export fn sigprocmask(how: i32, set: *u64, oldset: *u64) i32 = { + return syscall4(nr.RTSIGPROCMASK, how: i64, set: i64, oldset: i64, + size(u64): i64): i32; +}; + +// Native coordinators cannot install an rt_sigreturn restorer, so interruption +// delivery follows Hare's signalfd shape instead; ref/hare/sys/+linux/ +// syscalls.ha:636-639 and types.ha:577-578. +export fn signalfd(fd: i32, mask: *u64, flags: i32) i32 = { + return syscall4(nr.SIGNALFD4, fd: i64, mask: i64, + size(u64): i64, flags: i64): i32; +}; + // execve(2): on success, does not return. Mirrors Hare's // os::exec::exec path arg (str). argv/envp stay `**u8` — the // kernel takes a NUL-pointer-terminated table of NUL-terminated @@ -393,6 +459,26 @@ export fn wait4(pid: i32, status: *i32, options: i32, rusage: *void) i32 = { options: i64, rusage: i64): i32; }; +// Linux wait-status decoding, ref/hare/sys/+linux/types.ha:312-319. +// Keeping exit and signal as separate predicates prevents an expected +// numeric exit from accepting a signal with the same collapsed status. +export fn wexitstatus(status: i32) i32 = { + return (status >> 8i32) & 255i32; +}; + +export fn wtermsig(status: i32) i32 = { + return status & 127i32; +}; + +export fn wifexited(status: i32) bool = { + return wtermsig(status) == 0; +}; + +export fn wifsignaled(status: i32) bool = { + let sig: i32 = wtermsig(status); + return sig != 0 && sig != 127i32; +}; + // getcwd(2) — Linux flavour. Writes the NUL-terminated cwd into `buf` // and returns the number of bytes written (including the NUL), or a // negative errno. The driver uses it to expand `.` to the cwd's @@ -401,6 +487,14 @@ export fn getcwd(buf: *u8, n: u64) i64 = { return syscall2(nr.GETCWD, buf: i64, n: i64); }; +// Child setup must change cwd without importing the higher fs layer; +// ref/hare/sys/+linux/syscalls.ha:251-254. +export fn chdir(path: str) i32 = { + let p: *u8 = kpath(path); + if (p == nil: *u8) { return -36i32; }; + return syscall1(nr.CHDIR, p: i64): i32; +}; + // getdents64(2) — Linux directory enumeration. The fd must be opened // with O_RDONLY on a directory. `buf` receives a packed sequence of // linux_dirent64 records: diff --git a/lib/os/ostest.ww b/lib/os/ostest.ww index 134e3689..29a1c0bb 100644 --- a/lib/os/ostest.ww +++ b/lib/os/ostest.ww @@ -38,6 +38,14 @@ fn streq(a: str, b: str) bool = { return true; }; +fn ostestwait(pid: i32, status: *i32) i32 = { + for (true) { + let r: i32 = os.wait4(pid, status, 0i32, nil: *void); + if (r != -4i32) { return r; }; + }; + return -1; +}; + // ---- getenv: set var → matching value ------------------------------- @test fn test_getenv_set() void = { @@ -134,3 +142,315 @@ fn streq(a: str, b: str) bool = { assert(!(p[4095] != 165u8)); os.free(p.ptr: *void, 4096u64); }; + +@test fn test_pipe_roundtrip() void = { + let fds: [2]i32; + assert(!(os.pipe(&fds) != 0)); + let want: [1]u8 = [90u8]; + let got: [1]u8; + assert(!(os.write(fds[1], &want[0], 1u64) != 1i64)); + assert(!(os.read(fds[0], &got[0], 1u64) != 1i64)); + assert(!(got[0] != want[0])); + assert(!(os.close(fds[0]) != 0)); + assert(!(os.close(fds[1]) != 0)); +}; + +@test fn test_pipe2_exec_hold() void = { + let aa: []str = os.args(); + if (len(aa) < 3 || !streq(aa[2], "pipe2-cloexec-helper")) { return; }; + let b: [1]u8 = [1u8]; + assert(!(os.write(101i32, &b[0], 1u64) != 1i64)); + os.close(101i32); + assert(!(os.read(100i32, &b[0], 1u64) != 1i64)); + os.close(100i32); +}; + +@test fn test_pipe2_cloexec() void = { + let fds: [2]i32; + assert(!(os.pipe2(&fds, os.O_CLOEXEC) != 0)); + let hold: [2]i32; + if (os.pipe(&hold) != 0) { + os.close(fds[0]); os.close(fds[1]); + abort(); + }; + let ready: [2]i32; + if (os.pipe(&ready) != 0) { + os.close(fds[0]); os.close(fds[1]); + os.close(hold[0]); os.close(hold[1]); + abort(); + }; + let pid: i32 = os.fork(); + if (pid < 0) { + os.close(fds[0]); os.close(fds[1]); + os.close(hold[0]); os.close(hold[1]); + os.close(ready[0]); os.close(ready[1]); + abort(); + }; + if (pid == 0) { + os.close(fds[0]); + os.close(hold[1]); + os.close(ready[0]); + if (os.dup2(hold[0], 100i32) != 100i32) { os.exit(120); }; + if (os.dup2(ready[1], 101i32) != 101i32) { os.exit(121); }; + os.close(hold[0]); + os.close(ready[1]); + let aa: []str = os.args(); + if (len(aa) == 0) { os.exit(122); }; + let av: [4]*u8 = [aa[0].ptr, "test_pipe2_exec_hold\0".ptr, + "pipe2-cloexec-helper\0".ptr, nil: *u8]; + os.execve(aa[0], &av[0], nil: **u8); + let mark: [1]u8 = ['E']; + for (true) { + let wr: i64 = os.write(fds[1], &mark[0], 1u64); + if (wr == 1i64) { break; }; + if (wr != -4i64) { os.exit(123); }; + }; + os.close(fds[1]); + os.exit(124); + }; + os.close(fds[1]); + os.close(hold[0]); + os.close(ready[1]); + let b: [1]u8; + let rr: i64 = 0; + for (true) { + rr = os.read(ready[0], &b[0], 1u64); + if (rr != -4i64) { break; }; + }; + if (rr != 1i64) { + os.close(fds[0]); os.close(hold[1]); os.close(ready[0]); + os.kill(pid, os.SIGKILL); + let failedstatus: i32 = 0; + ostestwait(pid, &failedstatus); + abort(); + }; + os.close(ready[0]); + for (true) { + rr = os.read(fds[0], &b[0], 1u64); + if (rr != -4i64) { break; }; + }; + os.close(fds[0]); + let release: [1]u8 = [1u8]; + let wr: i64 = os.write(hold[1], &release[0], 1u64); + os.close(hold[1]); + let status: i32 = 0; + let r: i32 = ostestwait(pid, &status); + assert(!(wr != 1i64)); + assert(!(r != pid)); + assert(os.wifexited(status)); + assert(!(os.wexitstatus(status) != 0)); + assert(!(rr != 0i64)); +}; + +@test fn test_wait_status_decode() void = { + let exited: i32 = 7i32 << 8i32; + assert(os.wifexited(exited)); + assert(!os.wifsignaled(exited)); + assert(!(os.wexitstatus(exited) != 7)); + assert(!(os.wtermsig(exited) != 0)); + + let signaled: i32 = os.SIGKILL | 128i32; + assert(!os.wifexited(signaled)); + assert(os.wifsignaled(signaled)); + assert(!(os.wtermsig(signaled) != os.SIGKILL)); + assert(!os.wifsignaled(127i32)); + assert(!(os.WNOHANG != 1)); +}; + +@test fn test_wait4_nohang_normal_exit() void = { + let gate: [2]i32; + assert(!(os.pipe(&gate) != 0)); + let pid: i32 = os.fork(); + assert(!(pid < 0)); + if (pid == 0) { + os.close(gate[1]); + let b: [1]u8; + if (os.read(gate[0], &b[0], 1u64) != 1i64) { os.exit(120); }; + os.close(gate[0]); + os.exit(7); + }; + os.close(gate[0]); + let status: i32 = 0; + let r: i32 = os.wait4(pid, &status, os.WNOHANG, nil: *void); + if (r != 0) { + os.close(gate[1]); + if (r < 0) { + os.kill(pid, os.SIGKILL); + ostestwait(pid, &status); + }; + abort(); + }; + let b: [1]u8 = [1u8]; + assert(!(os.write(gate[1], &b[0], 1u64) != 1i64)); + os.close(gate[1]); + r = ostestwait(pid, &status); + assert(!(r != pid)); + assert(os.wifexited(status)); + assert(!(os.wexitstatus(status) != 7)); +}; + +@test fn test_process_group_signal() void = { + let hold: [2]i32; + assert(!(os.pipe(&hold) != 0)); + let pid: i32 = os.fork(); + assert(!(pid < 0)); + if (pid == 0) { + os.close(hold[1]); + if (os.setpgid(0i32, 0i32) != 0) { os.exit(120); }; + let b: [1]u8; + os.read(hold[0], &b[0], 1u64); + os.exit(120); + }; + os.close(hold[0]); + let pg: i32 = os.setpgid(pid, pid); + if (pg != 0) { + os.close(hold[1]); + os.kill(pid, os.SIGKILL); + let failedstatus: i32 = 0; + ostestwait(pid, &failedstatus); + abort(); + }; + let kr: i32 = os.kill(-pid, os.SIGTERM); + os.close(hold[1]); + if (kr != 0) { + os.kill(pid, os.SIGKILL); + let failedstatus: i32 = 0; + ostestwait(pid, &failedstatus); + abort(); + }; + let status: i32 = 0; + let r: i32 = ostestwait(pid, &status); + assert(!(r != pid)); + assert(os.wifsignaled(status)); + assert(!(os.wtermsig(status) != os.SIGTERM)); +}; + +@test fn test_process_group_term_escalation() void = { + let ready: [2]i32; + assert(!(os.pipe(&ready) != 0)); + let hold: [2]i32; + if (os.pipe(&hold) != 0) { + os.close(ready[0]); + os.close(ready[1]); + abort(); + }; + let pid: i32 = os.fork(); + if (pid < 0) { + os.close(ready[0]); os.close(ready[1]); + os.close(hold[0]); os.close(hold[1]); + abort(); + }; + if (pid == 0) { + os.close(ready[0]); + os.close(hold[1]); + if (os.setpgid(0i32, 0i32) != 0) { os.exit(120); }; + let mask: u64 = 1u64 << ((os.SIGTERM - 1): u64); + if (os.sigprocmask(os.SIG_BLOCK, &mask, nil: *u64) != 0) { + os.exit(121); + }; + let b: [1]u8 = [1u8]; + if (os.write(ready[1], &b[0], 1u64) != 1i64) { os.exit(122); }; + os.close(ready[1]); + os.read(hold[0], &b[0], 1u64); + os.exit(123); + }; + os.close(ready[1]); + os.close(hold[0]); + let pg: i32 = os.setpgid(pid, pid); + if (pg != 0) { + os.close(ready[0]); os.close(hold[1]); + os.kill(pid, os.SIGKILL); + let failedstatus: i32 = 0; + ostestwait(pid, &failedstatus); + abort(); + }; + let b: [1]u8; + if (os.read(ready[0], &b[0], 1u64) != 1i64) { + os.close(ready[0]); os.close(hold[1]); + os.kill(-pid, os.SIGKILL); + let failedstatus: i32 = 0; + ostestwait(pid, &failedstatus); + abort(); + }; + os.close(ready[0]); + if (os.kill(-pid, os.SIGTERM) != 0) { + os.close(hold[1]); + os.kill(-pid, os.SIGKILL); + let failedstatus: i32 = 0; + ostestwait(pid, &failedstatus); + abort(); + }; + let status: i32 = 0; + let r: i32 = os.wait4(pid, &status, os.WNOHANG, nil: *void); + if (r != 0) { + os.close(hold[1]); + if (r < 0) { + os.kill(-pid, os.SIGKILL); + ostestwait(pid, &status); + }; + abort(); + }; + let kr: i32 = os.kill(-pid, os.SIGKILL); + os.close(hold[1]); + if (kr != 0) { + os.kill(pid, os.SIGKILL); + ostestwait(pid, &status); + abort(); + }; + r = ostestwait(pid, &status); + assert(!(r != pid)); + assert(os.wifsignaled(status)); + assert(!(os.wtermsig(status) != os.SIGKILL)); + assert(!(os.SIG_UNBLOCK != 1)); + assert(!(os.SIG_SETMASK != 2)); +}; + +@test fn test_chdir_child() void = { + let pid: i32 = os.fork(); + assert(!(pid < 0)); + if (pid == 0) { + if (os.chdir("/") != 0) { os.exit(120); }; + let buf: [8]u8; + let n: i64 = os.getcwd(&buf[0], size([8]u8)); + if (n != 2i64 || buf[0] != '/' || buf[1] != 0u8) { + os.exit(121); + }; + os.exit(0); + }; + let status: i32 = 0; + assert(!(ostestwait(pid, &status) != pid)); + assert(os.wifexited(status)); + assert(!(os.wexitstatus(status) != 0)); +}; + +@test fn test_signalfd_nonblocking() void = { + let mask: u64 = 1u64 << ((os.SIGINT - 1): u64); + let oldmask: u64 = 0u64; + assert(!(os.sigprocmask(os.SIG_BLOCK, &mask, &oldmask) != 0)); + let fd: i32 = os.signalfd(-1i32, &mask, + os.SFD_CLOEXEC | os.SFD_NONBLOCK); + if (fd < 0) { + os.sigprocmask(os.SIG_SETMASK, &oldmask, nil: *u64); + abort(); + }; + if (os.kill(os.getpid(), os.SIGINT) != 0) { + os.close(fd); + os.sigprocmask(os.SIG_SETMASK, &oldmask, nil: *u64); + abort(); + }; + let info: [128]u8; + let n: i64 = 0i64; + for (true) { + n = os.read(fd, &info[0], size([128]u8)); + if (n != -4i64 && n != -11i64) { break; }; + }; + let cr: i32 = os.close(fd); + let mr: i32 = os.sigprocmask(os.SIG_SETMASK, &oldmask, nil: *u64); + assert(!(n != size([128]u8): i64)); + assert(!(info[0] != os.SIGINT: u8)); + assert(!(info[1] != 0u8 || info[2] != 0u8 || info[3] != 0u8)); + assert(!(cr != 0)); + assert(!(mr != 0)); + assert(!(os.SFD_CLOEXEC != os.O_CLOEXEC)); + assert(!(os.SFD_NONBLOCK != os.O_NONBLOCK)); +};