// SPDX-License-Identifier: GPL-2.0-or-later // // sigtest -- the SIGCHLD/sigsuspend contract every shell is built on. // // This is not one shell's quirk. It is what POSIX says to do and what zsh, // mksh, bash and dash all do to wait for a child: // // sigprocmask(SIG_BLOCK, {SIGCHLD}, &old); // close the window // pid = fork(); // child may exit at once // while (!flag) // flag set by the handler // sigsuspend(&old_without_SIGCHLD); // unblock and wait, atomically // sigprocmask(SIG_SETMASK, &old, 0); // // Blocking first is the whole point: the child can exit *before* the parent // reaches sigsuspend. Blocked, the signal stays pending; sigsuspend then // unblocks and waits in one step, so a pending signal is delivered at once and // the wait returns immediately. Get that wrong in either direction and the // parent waits for a signal that has already happened -- which is a hang, in // every shell, on every `$(command substitution)`. // // Written against the raw syscalls rather than a libc, because the thing under // test is the kernel's half of the contract. **The user-facing sigset has // signal N at bit N-1** (Linux's layout); the kernel converts at the boundary. // Getting that backwards is a real bug this tree has had, so the masks below // are spelled out rather than built by a macro. #include "types.h" #include "user.h" #include "posix.h" #define U_SIGCHLD_BIT (1ULL << (SIGCHLD - 1)) // rt_sigaction takes a pointer to Linux's struct sigaction, not a function // pointer: the handler is at offset 0 and the restorer at 144. The kernel // reads exactly those two words (SA_HANDLER_OFF / SA_RESTORER_OFF in // kernel/sys/sys_sig.c), so a 152-byte zeroed block with the handler written // into the first word is the whole of what it needs. #define SA_SIZE 152 static int failures; static volatile int handler_runs; static void puts_(const char *s) { int n = 0; while (s[n]) n++; write(1, (void *)s, n); } static void check(const char *what, int ok) { puts_(ok ? "[ok] " : "[FAIL] "); puts_(what); puts_("\n"); if (!ok) failures++; } static void on_chld(int sig) { (void)sig; handler_runs++; } // A bounded wait. A wrong answer here is a hang, so nothing may wait for ever: // sigsuspend returns -EINTR each time a signal is delivered, and a kernel that // never delivers spins out instead of stopping the test dead. #define SPINS 300 int main(void) { uint64 blocked, unblocked, old = 0; int pid, status, i; puts_("sigtest: the SIGCHLD/sigsuspend contract\n\n"); { static char act[SA_SIZE]; *(uint64 *)act = (uint64)(void *)on_chld; rt_sigaction(SIGCHLD, act, 0, 8); } // --- 1. blocking actually blocks -------------------------------------- blocked = U_SIGCHLD_BIT; sigprocmask(SIG_BLOCK, (const sigset_t *)&blocked, (sigset_t *)&old); handler_runs = 0; pid = fork(); if (pid == 0) exit_group(0); // Every chance for the child to exit and for the kernel to deliver // wrongly. If the mask is honoured, the handler has not run. for (i = 0; i < 50; i++) getpid(); sleep(10); check("SIGCHLD stays pending while blocked", handler_runs == 0); // --- 2. sigsuspend delivers what is already pending -------------------- // The case that matters, and the one that hangs: the child is already // gone, so nothing new will ever arrive. sigsuspend has to deliver what is // *pending*, not wait for the next one. unblocked = ~U_SIGCHLD_BIT; for (i = 0; i < SPINS && handler_runs == 0; i++) rt_sigsuspend(&unblocked); check("sigsuspend delivers a signal that was already pending", handler_runs > 0); check("and it did not have to spin to get it", i <= 1); check("the handler ran exactly once", handler_runs == 1); // --- 3. the mask survives ---------------------------------------------- { uint64 now = 0; sigprocmask(SIG_BLOCK, 0, (sigset_t *)&now); check("sigsuspend restores the mask it was called with", (now & U_SIGCHLD_BIT) != 0); } sigprocmask(SIG_SETMASK, (const sigset_t *)&old, 0); // --- 4. the child is still reapable ------------------------------------ status = -1; check("the child can still be reaped after the handler ran", waitpid(pid, &status, 0) == pid); check("and its status says it exited cleanly", status == 0); // --- 5. the other order: the signal arrives while waiting --------------- handler_runs = 0; sigprocmask(SIG_BLOCK, (const sigset_t *)&blocked, (sigset_t *)&old); pid = fork(); if (pid == 0) { sleep(30); // let the parent reach sigsuspend first exit_group(0); } for (i = 0; i < SPINS && handler_runs == 0; i++) rt_sigsuspend(&unblocked); check("sigsuspend wakes for a signal that arrives while it waits", handler_runs > 0); sigprocmask(SIG_SETMASK, (const sigset_t *)&old, 0); waitpid(pid, &status, 0); puts_("\n"); puts_(failures ? "sigtest: FAILED\n" : "sigtest: all checks passed\n"); return failures ? 1 : 0; }