diff --git a/.github/ci-constants.env b/.github/ci-constants.env index 85885c2f14be..163657207d35 100644 --- a/.github/ci-constants.env +++ b/.github/ci-constants.env @@ -1,3 +1,3 @@ # Shared CI constants. Loaded into GITHUB_ENV by workflows that need them. # Pinned wasix-libc sysroot (wasix-org/wasix-libc release tag). -WASIX_LIBC_SYSROOT_TAG=v2026-07-03.1 +WASIX_LIBC_SYSROOT_TAG=v2026-07-30.1 diff --git a/lib/wasix/tests/wasm_tests/signal/pthread-sigmask-inherit/main.c b/lib/wasix/tests/wasm_tests/signal/pthread-sigmask-inherit/main.c new file mode 100644 index 000000000000..74e3cf9e6b4b --- /dev/null +++ b/lib/wasix/tests/wasm_tests/signal/pthread-sigmask-inherit/main.c @@ -0,0 +1,55 @@ +//#MinimalLibc: v2026-07-30.1 +//#ExpectedStdout: child inherited the blocked mask + +#include +#include +#include +#include +#include + +// A new thread inherits its creator's signal mask, and the mask is per-thread: +// unblocking in the child must not affect the parent. + +static volatile int child_saw_blocked = 0; +static volatile int child_query_ok = 0; + +static void* child(void* arg) { + (void)arg; + + sigset_t mask; + sigemptyset(&mask); + child_query_ok = (pthread_sigmask(SIG_BLOCK, NULL, &mask) == 0); + child_saw_blocked = sigismember(&mask, SIGUSR1); + + sigset_t unblock; + sigemptyset(&unblock); + sigaddset(&unblock, SIGUSR1); + pthread_sigmask(SIG_UNBLOCK, &unblock, NULL); + + return NULL; +} + +int main() { + sigset_t block; + sigemptyset(&block); + sigaddset(&block, SIGUSR1); + assert(pthread_sigmask(SIG_BLOCK, &block, NULL) == 0); + + pthread_t thread; + assert(pthread_create(&thread, NULL, child, NULL) == 0); + assert(pthread_join(thread, NULL) == 0); + + assert(child_query_ok && "pthread_sigmask should succeed in the child"); + assert(child_saw_blocked == 1 && + "child should inherit the creator's blocked mask"); + + // The child unblocked SIGUSR1 for itself only; ours must still be blocked. + sigset_t mine; + sigemptyset(&mine); + assert(pthread_sigmask(SIG_BLOCK, NULL, &mine) == 0); + assert(sigismember(&mine, SIGUSR1) == 1 && + "the mask should be per-thread, not shared"); + + printf("child inherited the blocked mask\n"); + return 0; +} diff --git a/lib/wasix/tests/wasm_tests/signal/sigaction-mask-blocks-other/main.c b/lib/wasix/tests/wasm_tests/signal/sigaction-mask-blocks-other/main.c new file mode 100644 index 000000000000..3e0146e98812 --- /dev/null +++ b/lib/wasix/tests/wasm_tests/signal/sigaction-mask-blocks-other/main.c @@ -0,0 +1,61 @@ +//#MinimalLibc: v2026-07-30.1 +//#ExpectedStdout: usr2 deferred until handler returned + +#include +#include +#include +#include + +// sa_mask names *additional* signals to block while a handler runs, not just +// the one being delivered. A SIGUSR1 handler whose sa_mask contains SIGUSR2 +// must not be interrupted by SIGUSR2; that delivery is deferred until the +// handler returns. + +static volatile sig_atomic_t usr2_calls = 0; +static volatile sig_atomic_t usr2_seen_inside = -1; + +static void usr1_handler(int sig, siginfo_t* info, void* ucontext) { + (void)sig; + (void)info; + (void)ucontext; + + raise(SIGUSR2); + // Sampled before returning: SIGUSR2 is in our sa_mask, so it must still be + // pending rather than already handled. + usr2_seen_inside = usr2_calls; +} + +static void usr2_handler(int sig, siginfo_t* info, void* ucontext) { + (void)sig; + (void)info; + (void)ucontext; + + usr2_calls++; +} + +int main() { + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = usr2_handler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + assert(sigaction(SIGUSR2, &sa, NULL) == 0); + + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = usr1_handler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + sigaddset(&sa.sa_mask, SIGUSR2); + assert(sigaction(SIGUSR1, &sa, NULL) == 0); + + assert(raise(SIGUSR1) == 0); + + assert(usr2_seen_inside == 0 && + "sa_mask should keep SIGUSR2 blocked inside the SIGUSR1 handler"); + assert(usr2_calls == 1 && + "SIGUSR2 should be delivered once the SIGUSR1 handler returns"); + + printf("usr2 deferred until handler returned\n"); + return 0; +} diff --git a/lib/wasix/tests/wasm_tests/signal/sigaction-mask-defers/main.c b/lib/wasix/tests/wasm_tests/signal/sigaction-mask-defers/main.c new file mode 100644 index 000000000000..d536565a6f94 --- /dev/null +++ b/lib/wasix/tests/wasm_tests/signal/sigaction-mask-defers/main.c @@ -0,0 +1,60 @@ +//#MinimalLibc: v2026-07-30.1 +//#ExpectedStdout: calls=5 max_depth=1 + +#include +#include +#include +#include + +// POSIX blocks the delivered signal for the duration of its handler (sa_mask +// plus the signal itself, the latter unless SA_NODEFER). So a signal re-raised +// from inside the handler is deferred and delivered once the handler returns, +// running the handler again sequentially rather than nesting it. +// +// The re-raise is gated on `calls < LIMIT` in guest code, so the recursion is +// bounded no matter how libc behaves: even with masking entirely broken this +// nests 5 deep rather than exhausting the stack. +// +// Paired with sigaction-nodefer-reenters, which is the same program with +// SA_NODEFER and therefore expects max_depth == LIMIT. + +#define LIMIT 5 + +static volatile sig_atomic_t calls = 0; +static volatile sig_atomic_t depth = 0; +static volatile sig_atomic_t max_depth = 0; + +static void handler(int sig, siginfo_t* info, void* ucontext) { + (void)info; + (void)ucontext; + + calls++; + depth++; + if (depth > max_depth) { + max_depth = depth; + } + if (calls < LIMIT) { + raise(sig); + } + depth--; +} + +int main() { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = handler; + sa.sa_flags = SA_SIGINFO; + sigfillset(&sa.sa_mask); + assert(sigaction(SIGUSR1, &sa, NULL) == 0); + + // Every deferred delivery is drained before the raise that triggered the + // first one returns, so no further syscall is needed to observe them. + assert(raise(SIGUSR1) == 0); + + assert(calls == LIMIT && "handler should run once per raise"); + assert(max_depth == 1 && "a blocked signal must not re-enter its handler"); + assert(depth == 0 && "every invocation should have returned"); + + printf("calls=%d max_depth=%d\n", (int)calls, (int)max_depth); + return 0; +} diff --git a/lib/wasix/tests/wasm_tests/signal/sigaction-nodefer-reenters/main.c b/lib/wasix/tests/wasm_tests/signal/sigaction-nodefer-reenters/main.c new file mode 100644 index 000000000000..454be080b09c --- /dev/null +++ b/lib/wasix/tests/wasm_tests/signal/sigaction-nodefer-reenters/main.c @@ -0,0 +1,57 @@ +//#MinimalLibc: v2026-07-30.1 +//#ExpectedStdout: calls=5 max_depth=5 + +#include +#include +#include +#include + +// SA_NODEFER opts out of adding the delivered signal to the mask for the +// duration of its handler, so a signal re-raised from inside the handler +// re-enters it immediately instead of being deferred. +// +// sa_mask must be empty here: it is applied on top of SA_NODEFER, so a +// sigfillset() mask would block SIGUSR1 anyway and defeat the opt-out. +// +// The re-raise is gated on `calls < LIMIT` in guest code, bounding the nesting +// to 5 frames. Paired with sigaction-mask-defers, the same program without +// SA_NODEFER, which expects max_depth == 1. + +#define LIMIT 5 + +static volatile sig_atomic_t calls = 0; +static volatile sig_atomic_t depth = 0; +static volatile sig_atomic_t max_depth = 0; + +static void handler(int sig, siginfo_t* info, void* ucontext) { + (void)info; + (void)ucontext; + + calls++; + depth++; + if (depth > max_depth) { + max_depth = depth; + } + if (calls < LIMIT) { + raise(sig); + } + depth--; +} + +int main() { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = handler; + sa.sa_flags = SA_SIGINFO | SA_NODEFER; + sigemptyset(&sa.sa_mask); + assert(sigaction(SIGUSR1, &sa, NULL) == 0); + + assert(raise(SIGUSR1) == 0); + + assert(calls == LIMIT && "handler should run once per raise"); + assert(max_depth == LIMIT && "SA_NODEFER should allow the handler to nest"); + assert(depth == 0 && "every invocation should have returned"); + + printf("calls=%d max_depth=%d\n", (int)calls, (int)max_depth); + return 0; +} diff --git a/lib/wasix/tests/wasm_tests/signal/sigaction-resethand/main.c b/lib/wasix/tests/wasm_tests/signal/sigaction-resethand/main.c new file mode 100644 index 000000000000..61e759a7a7b2 --- /dev/null +++ b/lib/wasix/tests/wasm_tests/signal/sigaction-resethand/main.c @@ -0,0 +1,47 @@ +//#MinimalLibc: v2026-07-30.1 +//#ExpectedStdout: resethand ok + +#include +#include +#include +#include + +// POSIX resets a handler installed with SA_RESETHAND to SIG_DFL on entry, so +// re-raising the same signal from inside the handler takes the default action +// instead of re-entering the handler. Node's `SignalExit` relies on exactly +// that: it re-raises to let the default disposition terminate the process. +// Without the reset it recurses until the stack is exhausted. +// +// The reset is asserted directly rather than by re-raising, because unbounded +// re-entry exhausts the host stack and would take the test process down with +// it instead of failing this one test. + +static volatile sig_atomic_t calls = 0; + +static void handler(int sig, siginfo_t* info, void* ucontext) { + (void)sig; + (void)info; + (void)ucontext; + calls++; +} + +int main() { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = handler; + sa.sa_flags = SA_SIGINFO | SA_RESETHAND; + sigfillset(&sa.sa_mask); + assert(sigaction(SIGUSR1, &sa, NULL) == 0); + + assert(raise(SIGUSR1) == 0); + assert(calls == 1 && "handler should have run once"); + + struct sigaction old; + memset(&old, 0, sizeof(old)); + assert(sigaction(SIGUSR1, NULL, &old) == 0); + assert(old.sa_handler == SIG_DFL && + "SA_RESETHAND should reset the disposition to SIG_DFL"); + + printf("resethand ok\n"); + return 0; +} diff --git a/lib/wasix/tests/wasm_tests/signal/sigaction-siginfo/main.c b/lib/wasix/tests/wasm_tests/signal/sigaction-siginfo/main.c new file mode 100644 index 000000000000..de13f5b0c071 --- /dev/null +++ b/lib/wasix/tests/wasm_tests/signal/sigaction-siginfo/main.c @@ -0,0 +1,58 @@ +//#MinimalLibc: v2026-07-30.1 +//#ExpectedStdout: plain handler ok +//#ExpectedStdout: siginfo handler ok + +#include +#include +#include +#include + +// A handler installed through `sa_sigaction` with `SA_SIGINFO` takes three +// arguments, while one installed through `sa_handler` takes a single one. On +// wasm these are distinct function types, so dispatching a three-argument +// handler through a one-argument `call_indirect` traps with "indirect call +// type mismatch" and kills the instance. Cover both dispatch forms. + +static volatile sig_atomic_t plain_calls = 0; +static volatile sig_atomic_t info_calls = 0; +static volatile sig_atomic_t info_signo = 0; +static volatile sig_atomic_t info_arg_ok = 0; + +static void plain_handler(int sig) { + (void)sig; + plain_calls++; +} + +static void info_handler(int sig, siginfo_t* info, void* ucontext) { + (void)ucontext; + info_calls++; + info_signo = sig; + info_arg_ok = (info != NULL && info->si_signo == sig); +} + +int main() { + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = plain_handler; + sigfillset(&sa.sa_mask); + assert(sigaction(SIGUSR1, &sa, NULL) == 0); + + assert(raise(SIGUSR1) == 0); + assert(plain_calls == 1 && "one-argument handler should have run"); + printf("plain handler ok\n"); + + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = info_handler; + sa.sa_flags = SA_SIGINFO; + sigfillset(&sa.sa_mask); + assert(sigaction(SIGUSR2, &sa, NULL) == 0); + + assert(raise(SIGUSR2) == 0); + assert(info_calls == 1 && "three-argument handler should have run"); + assert(info_signo == SIGUSR2 && "handler should receive the signal number"); + assert(info_arg_ok && "handler should receive a populated siginfo_t"); + printf("siginfo handler ok\n"); + + return 0; +} diff --git a/lib/wasix/tests/wasm_tests/signal/sigprocmask-blocks-and-pends/main.c b/lib/wasix/tests/wasm_tests/signal/sigprocmask-blocks-and-pends/main.c new file mode 100644 index 000000000000..6e957d1cb851 --- /dev/null +++ b/lib/wasix/tests/wasm_tests/signal/sigprocmask-blocks-and-pends/main.c @@ -0,0 +1,56 @@ +//#MinimalLibc: v2026-07-30.1 +//#ExpectedStdout: blocked, pending, then delivered on unblock + +#include +#include +#include +#include + +// sigprocmask must actually block: a signal raised while blocked is held +// pending, reported by sigpending(), and delivered when it is unblocked. +// Delivery has to happen before sigprocmask returns, since WASIX only delivers +// signals at syscall boundaries and nothing else would come along to do it. + +static volatile sig_atomic_t calls = 0; + +static void handler(int sig, siginfo_t* info, void* ucontext) { + (void)sig; + (void)info; + (void)ucontext; + + calls++; +} + +int main() { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = handler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + assert(sigaction(SIGUSR1, &sa, NULL) == 0); + + sigset_t block; + sigemptyset(&block); + sigaddset(&block, SIGUSR1); + assert(sigprocmask(SIG_BLOCK, &block, NULL) == 0); + + assert(raise(SIGUSR1) == 0); + assert(calls == 0 && "a blocked signal must not be delivered"); + + sigset_t pending; + sigemptyset(&pending); + assert(sigpending(&pending) == 0); + assert(sigismember(&pending, SIGUSR1) == 1 && + "sigpending should report the blocked signal"); + + assert(sigprocmask(SIG_UNBLOCK, &block, NULL) == 0); + assert(calls == 1 && "unblocking should deliver the pending signal"); + + sigemptyset(&pending); + assert(sigpending(&pending) == 0); + assert(sigismember(&pending, SIGUSR1) == 0 && + "the signal should no longer be pending once delivered"); + + printf("blocked, pending, then delivered on unblock\n"); + return 0; +}