SchOS/cmd/sigtest/main.c
Nexoniarz 26bca020b6 licence: SchOS is GPL-2.0-or-later, no longer Apache-2.0
Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.

The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.

LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:48:45 +02:00

149 lines
5.1 KiB
C

// 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;
}