Files
nt-lite/code/user/tests/posix_signal_smoke/posix_signal_smoke.c
T

461 lines
17 KiB
C

// Runtime coverage for P6 POSIX signal delivery, waits, spawn state, and exceptions.
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>
#include <spawn.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <ntlite/syscall.h>
#include <ntlite/posix_extensions.h>
#define GROUP_READ_FD 50
#define GROUP_WRITE_FD 51
static volatile sig_atomic_t gHandlerCount;
static volatile uint32_t gHandlerTid;
static volatile int gWorkerReady;
static volatile int gWorkerInheritedBlock;
static volatile int gWorkerStop;
static volatile sig_atomic_t gNestedCount;
static volatile sig_atomic_t gAlarmCount;
static volatile sig_atomic_t gPipeCount;
// Writes one test diagnostic line to the native console.
static void print(const char* text) {
SYS_WRITE_CONSOLE_REQUEST request = { sizeof(request), SysSvFromCstr(text) };
(void)SysWriteConsole(&request);
}
// Exits immediately after one failed assertion.
static void fail(const char* message) {
print("posix_signal_smoke: FAIL: ");
print(message);
print("\n");
_exit(1);
}
// Returns whether the compatibility command line contains one child marker.
static bool child_mode(const char* marker) {
const char* command = posix_get_command_line();
return command && marker && strstr(command, marker) != (char*)0;
}
// Resolves the current executable path from the compatibility command line.
static void get_self_path(char* path, size_t capacity) {
const char* command = posix_get_command_line();
size_t length = 0;
while (command && command[length] && command[length] != ' ' && command[length] != '\t' &&
length + 1u < capacity) {
path[length] = command[length];
++length;
}
path[length] = '\0';
if (length == 0 || (command[length] && command[length] != ' ' && command[length] != '\t'))
_exit(90);
}
// Moves one descriptor to an exact number and closes the source alias.
static void move_fd(int source, int target) {
if (source < 0 || dup2(source, target) != target)
fail("control dup2 failed");
if (source != target && close(source) != 0)
fail("control source close failed");
}
// Records the native thread selected for one caught user signal.
static void signal_handler(int signalNumber) {
if (signalNumber != SIGUSR1) return;
gHandlerTid = SysGetCurrentTid();
++gHandlerCount;
}
// Queues the same signal once while its first handler activation masks it.
static void nested_handler(int signalNumber) {
if (signalNumber != SIGUSR1) return;
++gNestedCount;
if (gNestedCount == 1 && raise(SIGUSR1) != 0)
_exit(91);
}
// Records one SIGALRM used to interrupt alertable POSIX waits.
static void alarm_handler(int signalNumber) {
if (signalNumber == SIGALRM) ++gAlarmCount;
}
// Records SIGPIPE generation while allowing write() to return EPIPE.
static void pipe_handler(int signalNumber) {
if (signalNumber == SIGPIPE) ++gPipeCount;
}
// Terminates the process-group child from asynchronous SIGUSR1 delivery.
static void group_handler(int signalNumber) {
if (signalNumber == SIGUSR1) _exit(61);
}
// Terminates the exception child after native access-fault translation.
static void fault_handler(int signalNumber) {
if (signalNumber == SIGSEGV) _exit(62);
}
// Keeps one registered unblocked POSIX thread available for process-directed delivery.
static void* signal_worker(void* argument) {
(void)argument;
sigset_t current = 0;
if (sigprocmask(SIG_SETMASK, (const sigset_t*)0, &current) != 0)
return (void*)(uintptr_t)1u;
gWorkerInheritedBlock = sigismember(&current, SIGUSR1) == 1;
sigset_t unblock = 0;
if (sigemptyset(&unblock) != 0 || sigaddset(&unblock, SIGUSR1) != 0 ||
sigprocmask(SIG_UNBLOCK, &unblock, (sigset_t*)0) != 0)
return (void*)(uintptr_t)1u;
gWorkerReady = 1;
while (!gWorkerStop && gHandlerCount < 3) (void)SysYield();
return (void*)0;
}
// Validates signal mask/default attributes before ordinary child entry proceeds.
static void run_spawn_attribute_child(void) {
sigset_t current = 0;
if (sigprocmask(SIG_SETMASK, (const sigset_t*)0, &current) != 0 ||
sigismember(&current, SIGUSR1) != 1)
_exit(70);
struct sigaction action;
if (sigaction(SIGUSR2, (const struct sigaction*)0, &action) != 0 ||
action.sa_handler != SIG_DFL)
_exit(71);
_exit(0);
}
// Announces readiness, then waits for one process-group-directed SIGUSR1.
static void run_group_child(void) {
struct sigaction action;
action.sa_handler = group_handler;
action.sa_mask = 0;
action.sa_flags = 0;
if (sigaction(SIGUSR1, &action, (struct sigaction*)0) != 0)
_exit(72);
if (write(GROUP_WRITE_FD, "R", 1) != 1)
_exit(73);
(void)close(GROUP_WRITE_FD);
for (;;) (void)pause();
}
// Waits for the parent to exercise one ordinary default-terminate disposition.
static void run_default_child(void) {
for (;;) (void)pause();
}
// Triggers one native access fault after installing a POSIX SIGSEGV handler.
static void run_exception_child(void) {
struct sigaction action;
action.sa_handler = fault_handler;
action.sa_mask = 0;
action.sa_flags = 0;
if (sigaction(SIGSEGV, &action, (struct sigaction*)0) != 0)
_exit(74);
*(volatile uint64_t*)(uintptr_t)1u = 0x1234u;
_exit(75);
}
// Spawns one child and requires one exact ordinary exit result.
static void wait_for_child(pid_t child, int expectedExit) {
int status = -1;
if (waitpid(child, &status, 0) != child || !WIFEXITED(status) ||
WEXITSTATUS(status) != expectedExit)
fail("child exit result");
}
// Verifies spawn-time signal masks and default-disposition reset policy.
static void test_spawn_signal_attributes(const char* self) {
struct sigaction ignore;
ignore.sa_handler = SIG_IGN;
ignore.sa_mask = 0;
ignore.sa_flags = 0;
if (sigaction(SIGUSR2, &ignore, (struct sigaction*)0) != 0)
fail("install inherited ignore");
sigset_t mask = 0;
sigset_t defaults = 0;
if (sigemptyset(&mask) != 0 || sigaddset(&mask, SIGUSR1) != 0 ||
sigemptyset(&defaults) != 0 || sigaddset(&defaults, SIGUSR2) != 0)
fail("spawn signal sets");
posix_spawnattr_t attributes;
if (posix_spawnattr_init(&attributes) != 0 ||
posix_spawnattr_setsigmask(&attributes, &mask) != 0 ||
posix_spawnattr_setsigdefault(&attributes, &defaults) != 0 ||
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF) != 0)
fail("spawn signal attributes");
sigset_t check = 0;
if (posix_spawnattr_getsigmask(&attributes, &check) != 0 || check != mask ||
posix_spawnattr_getsigdefault(&attributes, &check) != 0 || check != defaults)
fail("spawn signal attribute getters");
char* argv[] = { (char*)self, "--p6-spawn-attrs", (char*)0 };
pid_t child = (pid_t)-1;
if (posix_spawn(&child, self, (const posix_spawn_file_actions_t*)0,
&attributes, argv, (char* const*)0) != 0 || child <= 0)
fail("spawn signal child");
if (posix_spawnattr_destroy(&attributes) != 0)
fail("spawn attribute destroy");
wait_for_child(child, 0);
struct sigaction reset;
reset.sa_handler = SIG_DFL;
reset.sa_mask = 0;
reset.sa_flags = 0;
if (sigaction(SIGUSR2, &reset, (struct sigaction*)0) != 0)
fail("restore SIGUSR2 default");
}
// Verifies actual process-group routing to a child in a newly-created group.
static void test_process_group_delivery(const char* self) {
int descriptors[2];
if (pipe(descriptors) != 0)
fail("group pipe");
move_fd(descriptors[0], GROUP_READ_FD);
move_fd(descriptors[1], GROUP_WRITE_FD);
if (fcntl(GROUP_READ_FD, F_SETFD, FD_CLOEXEC) != 0)
fail("group read CLOEXEC");
posix_spawnattr_t attributes;
if (posix_spawnattr_init(&attributes) != 0 ||
posix_spawnattr_setpgroup(&attributes, 0) != 0 ||
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETPGROUP) != 0)
fail("group spawn attributes");
char* argv[] = { (char*)self, "--p6-group-child", (char*)0 };
pid_t child = (pid_t)-1;
if (posix_spawn(&child, self, (const posix_spawn_file_actions_t*)0,
&attributes, argv, (char* const*)0) != 0 || child <= 0)
fail("group child spawn");
if (posix_spawnattr_destroy(&attributes) != 0)
fail("group spawn attribute destroy");
if (close(GROUP_WRITE_FD) != 0)
fail("group parent write close");
char ready = 0;
if (read(GROUP_READ_FD, &ready, 1) != 1 || ready != 'R')
fail("group child readiness");
if (close(GROUP_READ_FD) != 0)
fail("group parent read close");
if (kill(-child, SIGUSR1) != 0)
fail("process-group signal delivery");
wait_for_child(child, 61);
}
// Verifies an ordinary default disposition terminates the target process.
static void test_default_disposition(const char* self) {
char* argv[] = { (char*)self, "--p6-default", (char*)0 };
pid_t child = (pid_t)-1;
if (posix_spawn(&child, self, (const posix_spawn_file_actions_t*)0,
(const posix_spawnattr_t*)0, argv, (char* const*)0) != 0 || child <= 0)
fail("default-disposition child spawn");
if (kill(child, SIGTERM) != 0)
fail("default-disposition signal");
wait_for_child(child, 128 + SIGTERM);
}
// Verifies selected native exceptions enter the POSIX signal adapter.
static void test_exception_translation(const char* self) {
char* argv[] = { (char*)self, "--p6-exception", (char*)0 };
pid_t child = (pid_t)-1;
if (posix_spawn(&child, self, (const posix_spawn_file_actions_t*)0,
(const posix_spawnattr_t*)0, argv, (char* const*)0) != 0 || child <= 0)
fail("exception child spawn");
wait_for_child(child, 62);
}
// Verifies alarm-backed waits and EINTR behavior for a blocked pipe read.
static void test_signal_waits(void) {
struct sigaction action;
action.sa_handler = alarm_handler;
action.sa_mask = 0;
action.sa_flags = 0;
if (sigaction(SIGALRM, &action, (struct sigaction*)0) != 0)
fail("install SIGALRM handler");
if (alarm(3) != 0)
fail("initial alarm remaining");
unsigned int remaining = alarm(0);
if (remaining < 2u || remaining > 3u)
fail("alarm replacement remaining");
sigset_t alarmSet = 0;
sigset_t previous = 0;
if (sigemptyset(&alarmSet) != 0 || sigaddset(&alarmSet, SIGALRM) != 0 ||
sigprocmask(SIG_BLOCK, &alarmSet, &previous) != 0)
fail("block SIGALRM");
sigset_t temporary = previous;
if (sigdelset(&temporary, SIGALRM) != 0)
fail("sigsuspend mask");
sig_atomic_t before = gAlarmCount;
(void)alarm(1);
errno = 0;
if (sigsuspend(&temporary) != -1 || errno != EINTR || gAlarmCount != before + 1)
fail("sigsuspend signal wake");
sigset_t current = 0;
if (sigprocmask(SIG_SETMASK, (const sigset_t*)0, &current) != 0 ||
sigismember(&current, SIGALRM) != 1)
fail("sigsuspend mask restoration");
if (sigprocmask(SIG_SETMASK, &previous, (sigset_t*)0) != 0)
fail("restore SIGALRM mask");
before = gAlarmCount;
(void)alarm(1);
errno = 0;
if (pause() != -1 || errno != EINTR || gAlarmCount != before + 1)
fail("pause signal wake");
before = gAlarmCount;
(void)alarm(1);
remaining = sleep(3);
if (gAlarmCount != before + 1 || remaining < 1u || remaining > 2u)
fail("sleep interruption");
int descriptors[2];
if (pipe(descriptors) != 0)
fail("EINTR pipe create");
before = gAlarmCount;
(void)alarm(1);
char byte = 0;
errno = 0;
if (read(descriptors[0], &byte, 1) != -1 || errno != EINTR || gAlarmCount != before + 1)
fail("blocked pipe read EINTR");
(void)alarm(0);
if (close(descriptors[0]) != 0 || close(descriptors[1]) != 0)
fail("EINTR pipe cleanup");
action.sa_handler = pipe_handler;
action.sa_mask = 0;
action.sa_flags = 0;
if (sigaction(SIGPIPE, &action, (struct sigaction*)0) != 0)
fail("install SIGPIPE handler");
if (pipe(descriptors) != 0 || close(descriptors[0]) != 0)
fail("SIGPIPE pipe setup");
sig_atomic_t pipeBefore = gPipeCount;
errno = 0;
if (write(descriptors[1], "P", 1) != -1 || errno != EPIPE ||
gPipeCount != pipeBefore + 1)
fail("SIGPIPE broken-pipe delivery");
if (close(descriptors[1]) != 0)
fail("SIGPIPE pipe cleanup");
}
// Exercises the complete P6 signal contract used by later exec/vfork stages.
int main(void) {
if (child_mode("--p6-spawn-attrs")) run_spawn_attribute_child();
if (child_mode("--p6-group-child")) run_group_child();
if (child_mode("--p6-default")) run_default_child();
if (child_mode("--p6-exception")) run_exception_child();
print("posix_signal_smoke: ");
sigset_t set;
if (sigemptyset(&set) != 0 || set != 0) fail("sigemptyset");
if (sigaddset(&set, SIGUSR1) != 0 || sigismember(&set, SIGUSR1) != 1)
fail("sigaddset/sigismember");
if (sigdelset(&set, SIGUSR1) != 0 || sigismember(&set, SIGUSR1) != 0)
fail("sigdelset");
if (sigfillset(&set) != 0 || sigismember(&set, SIGKILL) != 1)
fail("sigfillset");
errno = 0;
if (sigaddset(&set, 0) != -1 || errno != EINVAL) fail("invalid signal set member");
struct sigaction action;
action.sa_handler = signal_handler;
action.sa_mask = 0;
action.sa_flags = 0;
if (sigaction(SIGUSR1, &action, (struct sigaction*)0) != 0)
fail("install handler");
if (raise(SIGUSR1) != 0 || gHandlerCount != 1 || gHandlerTid != SysGetCurrentTid())
fail("raise delivery");
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
if (sigprocmask(SIG_BLOCK, &set, (sigset_t*)0) != 0) fail("block signal");
if (raise(SIGUSR1) != 0 || gHandlerCount != 1) fail("blocked raise delivered");
sigset_t pending = 0;
if (sigpending(&pending) != 0 || sigismember(&pending, SIGUSR1) != 1)
fail("thread pending signal");
if (sigprocmask(SIG_UNBLOCK, &set, (sigset_t*)0) != 0 || gHandlerCount != 2)
fail("pending delivery after unblock");
if (sigpending(&pending) != 0 || sigismember(&pending, SIGUSR1) != 0)
fail("pending signal not cleared");
struct sigaction nested;
nested.sa_handler = nested_handler;
nested.sa_mask = 0;
nested.sa_flags = 0;
if (sigaction(SIGUSR1, &nested, (struct sigaction*)0) != 0 ||
raise(SIGUSR1) != 0 || gNestedCount != 2)
fail("nested pending delivery");
if (sigaction(SIGUSR1, &action, (struct sigaction*)0) != 0)
fail("restore handler after nested delivery");
struct sigaction ignore;
ignore.sa_handler = SIG_IGN;
ignore.sa_mask = 0;
ignore.sa_flags = 0;
if (sigaction(SIGUSR1, &ignore, (struct sigaction*)0) != 0) fail("ignore disposition");
if (kill(getpid(), SIGUSR1) != 0) fail("ignored kill");
for (uint32_t i = 0; i < 32u; ++i) (void)SysYield();
if (gHandlerCount != 2) fail("ignored signal reached handler");
if (sigaction(SIGUSR1, &action, (struct sigaction*)0) != 0) fail("restore handler");
if (sigprocmask(SIG_BLOCK, &set, (sigset_t*)0) != 0) fail("block main for routing");
pthread_t worker;
if (pthread_create(&worker, (const pthread_attr_t*)0, signal_worker, (void*)0) != 0)
fail("pthread_create");
while (!gWorkerReady) (void)SysYield();
if (!gWorkerInheritedBlock) fail("pthread signal-mask inheritance");
if (kill(getpid(), SIGUSR1) != 0) fail("process-directed kill");
for (uint32_t i = 0; i < 10000u && gHandlerCount < 3; ++i) (void)SysYield();
if (gHandlerCount != 3 || gHandlerTid != worker.Tid)
fail("process-directed target selection");
gWorkerStop = 1;
void* workerResult = (void*)0;
if (pthread_join(worker, &workerResult) != 0 || workerResult != (void*)0)
fail("pthread_join");
if (sigprocmask(SIG_UNBLOCK, &set, (sigset_t*)0) != 0) fail("unblock main");
if (kill(0, 0) != 0) fail("current process-group existence");
pid_t group = getpgrp();
if (group <= 0 || kill(-group, 0) != 0) fail("explicit process-group existence");
errno = 0;
if (kill((pid_t)0x7fffffff, 0) != -1 || errno != ESRCH)
fail("missing process ESRCH");
struct sigaction killAction;
killAction.sa_handler = signal_handler;
killAction.sa_mask = 0;
killAction.sa_flags = 0;
errno = 0;
if (sigaction(SIGKILL, &killAction, (struct sigaction*)0) != -1 || errno != EINVAL)
fail("SIGKILL catch accepted");
sigset_t killSet;
sigemptyset(&killSet);
sigaddset(&killSet, SIGKILL);
if (sigprocmask(SIG_BLOCK, &killSet, &pending) != 0) fail("SIGKILL block request");
if (sigprocmask(SIG_SETMASK, &pending, (sigset_t*)0) != 0) fail("restore signal mask");
char self[512];
get_self_path(self, sizeof(self));
test_spawn_signal_attributes(self);
test_process_group_delivery(self);
test_default_disposition(self);
test_exception_translation(self);
test_signal_waits();
print("PASS\n");
return 0;
}