From 6eaacd44486e0e18cd4617f5baa4c1f9ac5991f7 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 5 Sep 2026 16:39:58 -0400 Subject: [PATCH] feat(wait): wait/waitpid/waitid and status macros --- include/sys/wait.h | 207 ++++++++++++++++++++++++ src/process/wait.c | 59 +++++++ tests/test_wait.c | 380 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 646 insertions(+) create mode 100644 include/sys/wait.h create mode 100644 src/process/wait.c create mode 100644 tests/test_wait.c diff --git a/include/sys/wait.h b/include/sys/wait.h new file mode 100644 index 0000000..e786038 --- /dev/null +++ b/include/sys/wait.h @@ -0,0 +1,207 @@ +#ifndef VLIBC_SYS_WAIT_H +#define VLIBC_SYS_WAIT_H + +/* + * vlibc — . + * + * Child-process status collection (POSIX.1-2008) and the wait status macros. + * Everything here is a pass-through to the kernel: wait/waitpid/wait3/wait4 + * ride SYS_wait4, waitid rides SYS_waitid, and failures are reported as -1 + * (or the child pid on success) with errno set by the syscall layer. + * + * Level 1 (onlyposix): wait, waitpid, waitid + the W* status macros. + * Level 2 (muslmimic): wait3, wait4 (XSI). + * + * The W* decode macros follow the kernel's wait status encoding: bits 0-6 + * hold the terminating signal (or 0x7f for a stop), bit 7 the core-dump + * flag, bits 8-15 the exit status, and the word 0xffff marks a continued + * child. WEXITED/WSTOPPED/WNOWAIT are the waitid-only option bits (XSI; + * kernel-identical values) and WCOREDUMP is an XSI/Linux extension (bit + * 0x80) kept alongside the POSIX set for source compatibility. + * + * None of the declarations carry an intent attribute: every function has + * kernel-visible side effects and reports failures through errno. + * + * does not exist yet (a later todo owns it). The minimal + * siginfo_t below and the CLD_* constants live here under guards so that + * header can take them over without conflict; see the notes by each. + */ + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Selector type for waitid(). POSIX defines idtype_t as an integer type and + * places it in , which does not define it yet; it is provided + * here under a VLIBC_ guard so sys/types.h can take it over later without a + * redefinition. int matches the kernel's `which` argument on x86_64. + */ +#ifndef VLIBC_DEFINED_IDTYPE_T +#define VLIBC_DEFINED_IDTYPE_T +typedef int idtype_t; +#endif + +/* waitid() idtype selectors (kernel-identical). */ +#define P_ALL 0 /* wait for any child */ +#define P_PID 1 /* wait for the specific child */ +#define P_PGID 2 /* wait for any child in the process group */ + +/* wait/waitpid option bits (kernel-identical). */ +#define WNOHANG 1 /* do not block; return 0 if no child has exited */ +#define WUNTRACED 2 /* also report stopped children */ +#define WCONTINUED 8 /* also report continued children */ + +/* + * waitid-only option bits (kernel-identical; XSI). WSTOPPED shares its + * value with WUNTRACED, so the two names are interchangeable where both + * apply. + */ +#define WEXITED 4 /* wait for exited children */ +#define WSTOPPED 2 /* wait for stopped children */ +#define WNOWAIT 0x01000000 /* report but do not reap */ + +/* Status decode macros. The argument is the raw wait status word. */ + +/* True when the child terminated normally via exit() or _exit(). */ +#define WIFEXITED(s) (((s) & 0x7f) == 0) + +/* Exit status of a normally terminated child (WIFEXITED true). */ +#define WEXITSTATUS(s) (((s) & 0xff00) >> 8) + +/* + * True when the child was killed by a signal. The 0x7f exclusion keeps the + * stop code (0x7f) from being misread as a terminating signal. + */ +#define WIFSIGNALED(s) (((s) & 0x7f) != 0 && ((s) & 0x7f) != 0x7f) + +/* Number of the signal that killed the child (WIFSIGNALED true). */ +#define WTERMSIG(s) ((s) & 0x7f) + +/* True when the child is stopped by a signal (WUNTRACED). */ +#define WIFSTOPPED(s) (((s) & 0xff) == 0x7f) + +/* Number of the signal that stopped the child (WIFSTOPPED true). */ +#define WSTOPSIG(s) WEXITSTATUS(s) + +/* True when the child was resumed by SIGCONT (WCONTINUED). */ +#define WIFCONTINUED(s) ((s) == 0xffff) + +/* True when the killed child dumped core (XSI/Linux extension, bit 0x80). */ +#define WCOREDUMP(s) (((s) & 0x80) != 0) + +/* + * si_code values reported by waitid() (kernel-identical). POSIX defines + * these in , which does not exist yet; guarded per name so the + * future signal.h can define them without a redefinition warning. + */ +#ifndef CLD_EXITED +#define CLD_EXITED 1 /* child exited normally */ +#define CLD_KILLED 2 /* child killed by a signal */ +#define CLD_DUMPED 3 /* child killed by a signal and dumped core */ +#define CLD_TRAPPED 4 /* child stopped by a trace event */ +#define CLD_STOPPED 5 /* child stopped by a signal */ +#define CLD_CONTINUED 6 /* child resumed by SIGCONT */ +#endif + +/* + * Minimal siginfo_t, sized and laid out to match the kernel's x86_64 + * siginfo_t (128 bytes) for the fields waitid() fills: si_signo, si_errno, + * si_code at offsets 0/4/8, then the wait-fields si_pid/si_uid/si_status at + * offsets 16/20/24. The union is 8-aligned (si_utime/si_stime are 8-byte + * clock_t in both the kernel and glibc layouts), so it starts at offset 16 + * with implicit padding after si_code. The kernel copies the full 128 bytes, + * so the size must stay 128; the pad member guarantees it and the static + * asserts pin the layout. The signal-handling fields (si_addr, si_value, + * timers, ...) are deliberately absent — the future owns the + * complete siginfo_t and must reconcile this guard. + */ +#ifndef VLIBC_INTERNAL_SIGINFO_DEFINED +#define VLIBC_INTERNAL_SIGINFO_DEFINED +typedef struct +{ + int si_signo; + int si_errno; + int si_code; + union + { + struct + { + pid_t si_pid; /* 16 */ + uid_t si_uid; /* 20 */ + int si_status; /* 24 */ + long si_utime; /* 32 — 8-byte clock_t, matches the kernel/glibc ABI */ + long si_stime; /* 40 */ + }; + int vlibc_siginfo_pad[28]; /* union sized 112 so the struct stays 128 */ + }; +} siginfo_t; + +_Static_assert(sizeof(siginfo_t) == 128, "siginfo_t must match the kernel size"); +_Static_assert(offsetof(siginfo_t, si_pid) == 16, "si_pid must sit at offset 16"); +#endif + +/* Level 1 (POSIX base). */ + +/* + * Wait for any child to terminate or stop and store its status in + * *stat_loc (NULL skips the store); return the child pid, or -1 with + * errno set. Equivalent to waitpid(-1, stat_loc, 0). + */ +pid_t +wait(int *stat_loc); + +/* + * Wait for the child identified by pid (-1: any child, 0: any child in the + * calling process group, < -1: any child in the process group -pid) and + * store its status in *stat_loc (NULL skips the store). options are the + * WNOHANG/WUNTRACED/WCONTINUED bits. Return the child pid, 0 when WNOHANG + * found nothing, or -1 with errno set. + */ +pid_t +waitpid(pid_t pid, int *stat_loc, int options); + +/* + * Wait for a child selected by idtype/id (P_ALL, P_PID, P_PGID) and fill + * *infop with the siginfo details (si_pid, si_uid, si_status and a CLD_* + * si_code); options are the WEXITED/WSTOPPED/WCONTINUED/WNOHANG/WNOWAIT + * bits. Return 0, or -1 with errno set. infop must point to at least + * 128 bytes (the kernel writes a full siginfo). + */ +int +waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options); + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): XSI. */ + +/* + * struct rusage is defined by , which does not exist yet; + * a forward declaration is enough to pass a pointer through to the kernel. + */ +struct rusage; + +/* + * Like waitpid(-1, stat_loc, options), and additionally store resource + * usage in *rusage (NULL skips the store); return the child pid, or -1 + * with errno set. XSI. + */ +pid_t +wait3(int *stat_loc, int options, struct rusage *rusage); + +/* + * Like waitpid(), and additionally store resource usage in *rusage (NULL + * skips the store); return the child pid, or -1 with errno set. XSI. + */ +pid_t +wait4(pid_t pid, int *stat_loc, int options, struct rusage *rusage); +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_SYS_WAIT_H */ diff --git a/src/process/wait.c b/src/process/wait.c new file mode 100644 index 0000000..5dda067 --- /dev/null +++ b/src/process/wait.c @@ -0,0 +1,59 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * wait/waitpid/waitid/wait3/wait4 — child-process status collection. + * + * All four are pass-throughs to the kernel: wait/waitpid/wait3/wait4 ride + * SYS_wait4 (the kernel does the pid selection, the option masking, and the + * status-word encoding), waitid rides SYS_waitid (which fills a full + * 128-byte siginfo with the si_pid/si_uid/si_status details and a CLD_* + * si_code). The raw result goes through syscall_ret(), so a child pid (or + * waitid's 0) is returned on success and -1 with errno set on error; + * WNOHANG-with-no-child returns 0 from the kernel untouched. + * + * No argument inspection is needed: the kernel interprets pid == -1 (any + * child), 0 (own process group) and pid < -1 (process group) for wait4, and + * the which/id pair for waitid, exactly as POSIX specifies. The options are + * the kernel-identical W* bits from . rusage passes through + * verbatim for wait3/wait4 (NULL is legal and skips the fill). + */ + +pid_t +wait(int *stat_loc) +{ + return (pid_t)syscall_ret(__syscall4(SYS_wait4, -1, (long)stat_loc, 0, 0)); +} + +pid_t +waitpid(pid_t pid, int *stat_loc, int options) +{ + return (pid_t)syscall_ret(__syscall4(SYS_wait4, pid, (long)stat_loc, options, 0)); +} + +int +waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options) +{ + return syscall_ret(__syscall5(SYS_waitid, idtype, id, (long)infop, options, 0)); +} + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): XSI wait3/wait4. */ + +pid_t +wait3(int *stat_loc, int options, struct rusage *rusage) +{ + return (pid_t)syscall_ret(__syscall4(SYS_wait4, -1, (long)stat_loc, options, (long)rusage)); +} + +pid_t +wait4(pid_t pid, int *stat_loc, int options, struct rusage *rusage) +{ + return (pid_t)syscall_ret(__syscall4(SYS_wait4, pid, (long)stat_loc, options, (long)rusage)); +} +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/tests/test_wait.c b/tests/test_wait.c new file mode 100644 index 0000000..4d4e30d --- /dev/null +++ b/tests/test_wait.c @@ -0,0 +1,380 @@ +/* + * vlibc — wait family test (todo 23). + * + * Exercises wait/waitpid/waitid (+ L2 wait3/wait4) end to end against real + * children. Children are created with the RAW SYS_clone(SIGCHLD) syscall and + * exit with raw SYS_exit — deliberately NOT the fork() wrapper of the + * parallel todo 20, so this test stays independent of it. + * + * Default-mode scenarios (in run order): + * + * 1. waitpid(-1, &st, WNOHANG) with no children yet -> -1 (ECHILD). + * Runs FIRST: the test process must have no live children. + * 2. child exits 7 -> waitpid returns the child, WIFEXITED, WEXITSTATUS 7. + * 3. child exits 42 -> the QA happy path: WEXITSTATUS 42. + * 4. child killed via raw SYS_kill(SIGKILL) -> WIFSIGNALED, WTERMSIG 9, + * and WCOREDUMP false (SIGKILL never dumps core). + * 5. WNOHANG: child sleeps 100 ms via raw SYS_nanosleep -> waitpid(WNOHANG) + * returns 0 while it runs, then the blocking waitpid reaps it. + * 6. wait() == waitpid(-1): child exits 3 -> wait() reaps it, WEXITSTATUS 3. + * 7. waitid(P_PID, child, &info, WEXITED): returns 0, info.si_pid == child, + * si_signo == SIGCHLD, si_code == CLD_EXITED, si_status == 5. + * + * Level-2 gated section: wait4(child,...) and wait3(...) reaping exit 6/9. + * + * The negative path in scenario 1 makes the LIBRARY write errno + * (syscall_ret), which under a host-linked binary targets glibc's private + * dtv slot at %fs:0+8. In the default mode that call is bracketed with a + * save/restore of the slot (task 13 technique) — only vlibc/raw-syscall code + * runs between the write and the restore. The test itself NEVER reads + * errno; the negative is asserted on the return value. The -f mode runs + * the failure scenario alone and exits via raw SYS_exit_group (house + * pattern, tests/test_malloc.c). + * + * All diagnostics go through raw SYS_write (no stdio): under -Iinclude the + * vlibc public headers shadow GCC's internal ones, so a host header would + * not compile. Not part of the library proper; compiled manually for this + * todo (the tests/ + make check wiring is owned by a later todo). + */ + +#include "../include/sys/wait.h" + +#include "../src/internal/syscall.h" + +/* Kernel-UAPI signal numbers, local to this test (signal.h is todo 28). */ +#define T23_SIGCHLD 17 +#define T23_SIGKILL 9 + +/* Kernel timespec (time.h is a later todo). */ +struct t23_timespec +{ + long tv_sec; + long tv_nsec; +}; + +static int failures; + +/* Write a NUL-terminated string to fd via the raw syscall layer. The + * optimize attribute keeps GCC from lowering the length loop into a + * strlen call, which would leave a vlibc-owned symbol undefined in this + * host-linked standalone binary (house idiom, see src/string). */ +static __attribute__((optimize("no-tree-loop-distribute-patterns"))) void +say(int fd, const char *s) +{ + long n = 0; + + while (s[n] != '\0') + { + n++; + } + __syscall3(SYS_write, fd, (long)s, n); +} + +static void +check(int cond, const char *what) +{ + if (cond) + { + say(1, "PASS: "); + say(1, what); + say(1, "\n"); + } + else + { + say(2, "FAIL: "); + say(2, what); + say(2, "\n"); + failures++; + } +} + +/* + * Host-TCB slot-1 bracket: the library's errno write on a negative path + * lands at %fs:0+8, glibc's dtv pointer. Save and restore it around each + * such call; only vlibc/raw-syscall code runs in between (task 13 + * technique). + */ +static unsigned long +tcb_slot1(void) +{ + return *(unsigned long *)((char *)__builtin_thread_pointer() + 8); +} + +static void +tcb_slot1_set(unsigned long v) +{ + *(unsigned long *)((char *)__builtin_thread_pointer() + 8) = v; +} + +/* Terminate the current thread via the raw syscall (never returns). */ +static void +child_exit(int code) +{ + __syscall1(SYS_exit, code); + __builtin_unreachable(); +} + +/* fork-equivalent via raw clone: parent gets the child pid, child gets 0. */ +static pid_t +spawn_child(void) +{ + return (pid_t)__syscall5(SYS_clone, T23_SIGCHLD, 0, 0, 0, 0); +} + +/* Reap a child that has already been spawned (pid > 0 in the parent). */ +static void +reap_exited(const char *what, pid_t pid, int want_status) +{ + int st = -1; + pid_t r = waitpid(pid, &st, 0); + + check(r == pid && WIFEXITED(st) && WEXITSTATUS(st) == want_status, what); +} + +/* + * waitpid(-1, &st, WNOHANG) with no children -> -1 (ECHILD). Must run before + * any child is created for the result to be deterministic. + */ +static void +scenario_no_children(void) +{ + int st = 0; + unsigned long saved = tcb_slot1(); + + check(waitpid(-1, &st, WNOHANG) == -1, "waitpid WNOHANG with no children -> -1"); + tcb_slot1_set(saved); +} + +/* Child exits 7 -> WIFEXITED && WEXITSTATUS == 7. */ +static void +scenario_exit_7(void) +{ + pid_t pid = spawn_child(); + + if (pid == 0) + { + child_exit(7); + } + if (pid > 0) + { + reap_exited("waitpid reaps exit(7): WIFEXITED && WEXITSTATUS 7", pid, 7); + } + else + { + check(0, "clone for exit(7) child"); + } +} + +/* QA happy path: child exits 42 -> WEXITSTATUS == 42. */ +static void +scenario_exit_42(void) +{ + pid_t pid = spawn_child(); + + if (pid == 0) + { + child_exit(42); + } + if (pid > 0) + { + reap_exited("waitpid reaps exit(42): WEXITSTATUS 42", pid, 42); + } + else + { + check(0, "clone for exit(42) child"); + } +} + +/* Child killed by SIGKILL -> WIFSIGNALED && WTERMSIG == 9, no core dump. */ +static void +scenario_killed(void) +{ + int st = -1; + pid_t pid = spawn_child(); + + if (pid == 0) + { + /* Stay alive until the parent's SIGKILL arrives (10 s is plenty). */ + struct t23_timespec ts = {.tv_sec = 10, .tv_nsec = 0}; + + __syscall2(SYS_nanosleep, (long)&ts, 0); + child_exit(0); + } + if (pid > 0) + { + pid_t r; + + __syscall2(SYS_kill, pid, T23_SIGKILL); + r = waitpid(pid, &st, 0); + check(r == pid && WIFSIGNALED(st) && WTERMSIG(st) == T23_SIGKILL && !WCOREDUMP(st), + "waitpid reaps SIGKILLed child: WIFSIGNALED && WTERMSIG 9"); + } + else + { + check(0, "clone for SIGKILL child"); + } +} + +/* WNOHANG returns 0 while the child runs; the blocking waitpid then reaps. */ +static void +scenario_wnohang(void) +{ + int st = -1; + pid_t pid = spawn_child(); + + if (pid == 0) + { + struct t23_timespec ts = {.tv_sec = 0, .tv_nsec = 100000000L}; /* 100 ms */ + + __syscall2(SYS_nanosleep, (long)&ts, 0); + child_exit(0); + } + if (pid > 0) + { + pid_t r; + + /* The child cannot have exited yet: 100 ms of nanosleep remain. */ + r = waitpid(pid, &st, WNOHANG); + check(r == 0, "waitpid WNOHANG returns 0 while child still runs"); + + r = waitpid(pid, &st, 0); + check(r == pid && WIFEXITED(st) && WEXITSTATUS(st) == 0, + "blocking waitpid reaps the child after WNOHANG"); + } + else + { + check(0, "clone for WNOHANG child"); + } +} + +/* wait() == waitpid(-1): reaps any child. */ +static void +scenario_wait_any(void) +{ + int st = -1; + pid_t pid = spawn_child(); + + if (pid == 0) + { + child_exit(3); + } + if (pid > 0) + { + pid_t r = wait(&st); + + check(r == pid && WIFEXITED(st) && WEXITSTATUS(st) == 3, + "wait() reaps any child: WEXITSTATUS 3"); + } + else + { + check(0, "clone for wait() child"); + } +} + +/* waitid(P_PID, ...) fills siginfo with the reaped child's details. */ +static void +scenario_waitid(void) +{ + pid_t pid = spawn_child(); + + if (pid == 0) + { + child_exit(5); + } + if (pid > 0) + { + siginfo_t info = {0}; + + check(waitid(P_PID, (id_t)pid, &info, WEXITED) == 0, "waitid WEXITED returns 0"); + check(info.si_pid == pid, "waitid info.si_pid == child"); + check(info.si_signo == T23_SIGCHLD, "waitid info.si_signo == SIGCHLD"); + check(info.si_code == CLD_EXITED, "waitid info.si_code == CLD_EXITED"); + check(info.si_status == 5, "waitid info.si_status == exit status"); + } + else + { + check(0, "clone for waitid child"); + } +} + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): wait3/wait4 pass-through (NULL rusage is legal). */ +static void +scenario_wait3_wait4(void) +{ + int st = -1; + pid_t pid = spawn_child(); + + if (pid == 0) + { + child_exit(6); + } + if (pid > 0) + { + pid_t r = wait4(pid, &st, 0, 0); + + check(r == pid && WIFEXITED(st) && WEXITSTATUS(st) == 6, + "wait4 reaps the child: WEXITSTATUS 6"); + } + else + { + check(0, "clone for wait4 child"); + return; + } + + pid = spawn_child(); + if (pid == 0) + { + child_exit(9); + } + if (pid > 0) + { + pid_t r = wait3(&st, 0, 0); + + check(r == pid && WIFEXITED(st) && WEXITSTATUS(st) == 9, + "wait3 reaps any child: WEXITSTATUS 9"); + } + else + { + check(0, "clone for wait3 child"); + } +} +#endif /* VLIBC_LEVEL_GE(2) */ + +/* The failure scenario, run alone under -f (writes errno; see top). */ +static int +failure_scenarios(void) +{ + scenario_no_children(); + return failures; +} + +int +main(int argc, char **argv) +{ + if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f') + { + /* + * The failure scenario writes errno inside the library; under the + * host libc that slot is glibc's private TLS state, so leave via + * the raw syscall without running host cleanup. + */ + int rc = failure_scenarios(); + + __syscall1(SYS_exit_group, rc); + return rc; /* not reached */ + } + + scenario_no_children(); /* first: the process must have no children */ + scenario_exit_7(); + scenario_exit_42(); + scenario_killed(); + scenario_wnohang(); + scenario_wait_any(); + scenario_waitid(); +#if VLIBC_LEVEL_GE(2) + scenario_wait3_wait4(); +#endif + + return failures != 0; +}