diff --git a/expected/wasm32-wasi-eh/defined-symbols.txt b/expected/wasm32-wasi-eh/defined-symbols.txt index ab98f67fb..1c7b3a747 100644 --- a/expected/wasm32-wasi-eh/defined-symbols.txt +++ b/expected/wasm32-wasi-eh/defined-symbols.txt @@ -277,6 +277,8 @@ __set_thread_area __shgetc __shlim __shm_mapname +__sig_deliver_pending +__sig_register_callback __sigaction __sigaction_external_default __signgam diff --git a/expected/wasm32-wasi-ehpic/defined-symbols.txt b/expected/wasm32-wasi-ehpic/defined-symbols.txt index 03182322a..0cbec78aa 100644 --- a/expected/wasm32-wasi-ehpic/defined-symbols.txt +++ b/expected/wasm32-wasi-ehpic/defined-symbols.txt @@ -280,6 +280,8 @@ __set_thread_area __shgetc __shlim __shm_mapname +__sig_deliver_pending +__sig_register_callback __sigaction __sigaction_external_default __signgam diff --git a/expected/wasm32-wasi/defined-symbols.txt b/expected/wasm32-wasi/defined-symbols.txt index 1e9b0707a..73a1da16a 100644 --- a/expected/wasm32-wasi/defined-symbols.txt +++ b/expected/wasm32-wasi/defined-symbols.txt @@ -282,6 +282,8 @@ __set_thread_area __shgetc __shlim __shm_mapname +__sig_deliver_pending +__sig_register_callback __sigaction __sigaction_external_default __signgam diff --git a/libc-top-half/musl/src/env/__init_tls.c b/libc-top-half/musl/src/env/__init_tls.c index 9aa556048..c6f855617 100644 --- a/libc-top-half/musl/src/env/__init_tls.c +++ b/libc-top-half/musl/src/env/__init_tls.c @@ -123,6 +123,14 @@ int __init_tp(void *p) td->robust_list.head = &td->robust_list.head; td->sysinfo = __sysinfo; td->next = td->prev = td; +#ifndef __wasilibc_unmodified_upstream + /* __wasi_init_tp() allocates this structure with aligned_alloc(), which + * does not zero, so every field the signal code reads has to be set here + * explicitly. Leaving the mask as heap garbage makes arbitrary signals + * look blocked. */ + memset(&td->sigmask, 0, sizeof td->sigmask); + memset(&td->sigpending, 0, sizeof td->sigpending); +#endif return 0; } diff --git a/libc-top-half/musl/src/include/signal.h b/libc-top-half/musl/src/include/signal.h index bb5667841..cd1049194 100644 --- a/libc-top-half/musl/src/include/signal.h +++ b/libc-top-half/musl/src/include/signal.h @@ -11,4 +11,13 @@ hidden void __restore_sigs(void *); hidden void __get_handler_set(sigset_t *); +/* Dispatches signals that are pending on the calling thread and no longer + * blocked. WASIX delivers signals only at syscall boundaries, so whoever + * unblocks a signal has to flush it: nothing will interrupt us to do it. */ +hidden void __sig_deliver_pending(void); + +/* (Re-)registers the guest signal callback with the host. Needed after fork: + * the host's registration is per instance and does not survive it. */ +hidden void __sig_register_callback(void); + #endif diff --git a/libc-top-half/musl/src/internal/pthread_impl.h b/libc-top-half/musl/src/internal/pthread_impl.h index 5bd7a52e3..769e9c422 100644 --- a/libc-top-half/musl/src/internal/pthread_impl.h +++ b/libc-top-half/musl/src/internal/pthread_impl.h @@ -4,6 +4,11 @@ #include #ifdef __wasilibc_unmodified_upstream #include +#else +/* The signal mask below needs sigset_t as a complete type. Pulling in all of + * here would be circular, so take just the typedef. */ +#define __NEED_sigset_t +#include #endif #include #include @@ -67,6 +72,14 @@ struct pthread { volatile int killlock[1]; char *dlerror_buf; void *stdio_locks; +#ifndef __wasilibc_unmodified_upstream + /* Signals are masked in the guest: WASIX has no kernel to hold a + * per-thread mask for us. `sigpending` holds deliveries that arrived + * while blocked, which __sig_deliver_pending() flushes on unblock. + * Only ever touched by the owning thread, so no lock is needed. */ + sigset_t sigmask; + sigset_t sigpending; +#endif /* Part 3 -- the positions of these fields relative to * the end of the structure is external and internal ABI. */ diff --git a/libc-top-half/musl/src/internal/sigmask.h b/libc-top-half/musl/src/internal/sigmask.h new file mode 100644 index 000000000..25a6b68cc --- /dev/null +++ b/libc-top-half/musl/src/internal/sigmask.h @@ -0,0 +1,54 @@ +#ifndef SIGMASK_H +#define SIGMASK_H + +#include + +/* Meaningful extent of a sigset_t. The structure is padded well past the + * number of signals that exist, and k_sigaction stores only this much, so all + * mask arithmetic is done over this many bytes. */ +#define __SIGSET_BYTES (_NSIG/8) + +static inline void __sigset_or(sigset_t *dst, const void *bits) +{ + unsigned char *d = (unsigned char *)dst; + const unsigned char *s = (const unsigned char *)bits; + for (unsigned i = 0; i < __SIGSET_BYTES; i++) d[i] |= s[i]; +} + +static inline void __sigset_andnot(sigset_t *dst, const void *bits) +{ + unsigned char *d = (unsigned char *)dst; + const unsigned char *s = (const unsigned char *)bits; + for (unsigned i = 0; i < __SIGSET_BYTES; i++) d[i] &= ~s[i]; +} + +static inline void __sigset_copy(sigset_t *dst, const void *bits) +{ + unsigned char *d = (unsigned char *)dst; + const unsigned char *s = (const unsigned char *)bits; + for (unsigned i = 0; i < __SIGSET_BYTES; i++) d[i] = s[i]; +} + +/* Unlike sigaddset()/sigismember(), these neither validate `sig` nor touch + * errno, so they are safe on the signal delivery path -- clobbering errno + * there would corrupt the error reporting of whatever syscall the delivery + * interrupted. Callers must keep `sig` within 1.._NSIG-1. */ +static inline int __sigset_test(const sigset_t *set, int sig) +{ + unsigned s = sig-1; + return !!(set->__bits[s/8/sizeof *set->__bits] & 1UL<<(s&8*sizeof *set->__bits-1)); +} + +static inline void __sigset_add(sigset_t *set, int sig) +{ + unsigned s = sig-1; + set->__bits[s/8/sizeof *set->__bits] |= 1UL<<(s&8*sizeof *set->__bits-1); +} + +static inline void __sigset_del(sigset_t *set, int sig) +{ + unsigned s = sig-1; + set->__bits[s/8/sizeof *set->__bits] &= ~(1UL<<(s&8*sizeof *set->__bits-1)); +} + +#endif diff --git a/libc-top-half/musl/src/process/_Fork.c b/libc-top-half/musl/src/process/_Fork.c index 76df42507..7c2fd0e6d 100644 --- a/libc-top-half/musl/src/process/_Fork.c +++ b/libc-top-half/musl/src/process/_Fork.c @@ -1,5 +1,6 @@ #include #include +#include #include "syscall.h" #ifdef __wasilibc_unmodified_upstream #else @@ -56,6 +57,15 @@ pid_t _Fork(int copy_mem) __thread_list_lock = 0; libc.threads_minus_1 = 0; if (libc.need_locks) libc.need_locks = -1; +#ifndef __wasilibc_unmodified_upstream + /* The child is a fresh instance as far as the host is concerned, so + * it has no signal callback registered even though the guest-side + * bookkeeping was inherited and claims otherwise. Without this the + * host discards every signal sent to the child. */ + __sig_register_callback(); + /* POSIX: the child's set of pending signals starts out empty. */ + memset(&self->sigpending, 0, sizeof self->sigpending); +#endif } UNLOCK(__abort_lock); __aio_atfork(!ret); diff --git a/libc-top-half/musl/src/signal/block.c b/libc-top-half/musl/src/signal/block.c index 455485fda..a86b589e4 100644 --- a/libc-top-half/musl/src/signal/block.c +++ b/libc-top-half/musl/src/signal/block.c @@ -5,10 +5,9 @@ #include #endif #include - #ifdef __wasilibc_unmodified_upstream #else -extern volatile int __eintr_handler_lock[1]; +#include "sigmask.h" #endif static const unsigned long all_mask[] = { @@ -42,8 +41,9 @@ void __block_all_sigs(void *set) #ifdef __wasilibc_unmodified_upstream __syscall(SYS_rt_sigprocmask, SIG_BLOCK, &all_mask, set, _NSIG/8); #else - a_store(__eintr_handler_lock, 1); - __wasi_callback_signal("__wasm_signal_blocked"); + pthread_t self = __pthread_self(); + if (set) __sigset_copy((sigset_t *)set, &self->sigmask); + __sigset_or(&self->sigmask, all_mask); #endif } @@ -52,8 +52,9 @@ void __block_app_sigs(void *set) #ifdef __wasilibc_unmodified_upstream __syscall(SYS_rt_sigprocmask, SIG_BLOCK, &app_mask, set, _NSIG/8); #else - a_store(__eintr_handler_lock, 1); - __wasi_callback_signal("__wasm_signal_blocked"); + pthread_t self = __pthread_self(); + if (set) __sigset_copy((sigset_t *)set, &self->sigmask); + __sigset_or(&self->sigmask, app_mask); #endif } @@ -62,6 +63,10 @@ void __restore_sigs(void *set) #ifdef __wasilibc_unmodified_upstream __syscall(SYS_rt_sigprocmask, SIG_SETMASK, set, 0, _NSIG/8); #else - __wasi_callback_signal("__wasm_signal"); + pthread_t self = __pthread_self(); + if (set) __sigset_copy(&self->sigmask, (const sigset_t *)set); + /* Restoring can unblock, and nothing else will come along to deliver: + * WASIX only delivers signals at syscall boundaries. */ + __sig_deliver_pending(); #endif } \ No newline at end of file diff --git a/libc-top-half/musl/src/signal/sigaction.c b/libc-top-half/musl/src/signal/sigaction.c index d2ffaad3d..5adc7420b 100644 --- a/libc-top-half/musl/src/signal/sigaction.c +++ b/libc-top-half/musl/src/signal/sigaction.c @@ -7,6 +7,7 @@ #include "libc.h" #include "lock.h" #include "ksigaction.h" +#include "sigmask.h" static int unmask_done; static unsigned long handler_set[_NSIG/(8*sizeof(long))]; @@ -178,23 +179,119 @@ volatile int __eintr_valid_flag; #ifdef __wasilibc_unmodified_upstream #else -__attribute__((export_name("__wasm_signal"))) -void __wasm_signal(int sig) { - if (sig-32U < 3 || sig-1U >= _NSIG-1) { - return; - } +/* Runs one delivery of `sig`, with the handler's mask installed for the + * duration per POSIX: sa_mask, plus `sig` itself unless SA_NODEFER. */ +static void dispatch_signal(int sig) { LOCK(__eintr_handler_lock); struct k_sigaction ksa = __eintr_handler_callbacks[sig]; + if (ksa.handler != 0 && (ksa.flags & SA_RESETHAND)) { + /* POSIX resets the disposition to SIG_DFL on entry to a handler + * installed with SA_RESETHAND, so a signal re-raised from inside + * the handler takes the default action instead of re-entering the + * handler and recursing until the stack is exhausted. */ + struct k_sigaction reset; + memset(&reset, 0, sizeof reset); + __eintr_handler_callbacks[sig] = reset; + } UNLOCK(__eintr_handler_lock); + pthread_t self = __pthread_self(); + sigset_t saved = self->sigmask; + if (ksa.handler != 0) { - ksa.handler(sig); + __sigset_or(&self->sigmask, ksa.mask); + if (!(ksa.flags & SA_NODEFER)) { + __sigset_add(&self->sigmask, sig); + } + + if (ksa.flags & SA_SIGINFO) { + /* A handler installed through `sa_sigaction` takes three + * arguments. On wasm the argument count is part of the + * function type, so calling it through the one-argument + * `sa_handler` signature traps with "indirect call type + * mismatch" instead of silently ignoring the extra + * arguments the way native ABIs do. Dispatch through a + * matching signature instead. + * + * The signal is delivered by the host rather than raised + * from a faulting instruction, so there is no machine + * context to hand over; `ucontext` is null and `siginfo_t` + * carries just the fields the host can attest to. */ + union { + void (*plain)(int); + void (*with_info)(int, siginfo_t *, void *); + } dispatch = { .plain = ksa.handler }; + siginfo_t si; + memset(&si, 0, sizeof si); + si.si_signo = sig; + si.si_code = SI_USER; + dispatch.with_info(sig, &si, NULL); + } else { + ksa.handler(sig); + } } else { - unsigned long set[_NSIG/(8*sizeof(long))]; - __block_all_sigs(&set); + /* The default actions are not interruptible. sigfillset() covers + * every application signal and deliberately leaves the libc + * internal ones (SIGTIMER/SIGCANCEL/SIGSYNCCALL) alone. */ + sigset_t all; + sigfillset(&all); + __sigset_or(&self->sigmask, &all); default_handler(sig); - __restore_sigs(&set); } + + self->sigmask = saved; +} + +void __sig_deliver_pending(void) { + pthread_t self = __pthread_self(); + for (;;) { + int sig = 0; + for (int i = 1; i < _NSIG; i++) { + if (i-32U < 3) { + continue; + } + if (__sigset_test(&self->sigpending, i) && + !__sigset_test(&self->sigmask, i)) { + sig = i; + break; + } + } + if (!sig) { + return; + } + __sigset_del(&self->sigpending, sig); + /* Iterative, not recursive: a handler that re-raises its own signal + * has that delivery deferred and picked up by the next turn of this + * loop, so the handler runs again sequentially rather than nesting. */ + dispatch_signal(sig); + } +} + +void __sig_register_callback(void) { + /* Unconditional, unlike the a_cas() guards elsewhere: the host tracks the + * registration per instance while __eintr_callback_registered is ordinary + * memory, so after a fork the child looks registered but the host has no + * callback for it and silently eats every signal. */ + a_store(&__eintr_callback_registered, 1); + __wasi_callback_signal("__wasm_signal"); +} + +__attribute__((export_name("__wasm_signal"))) +void __wasm_signal(int sig) { + if (sig-32U < 3 || sig-1U >= _NSIG-1) { + return; + } + + pthread_t self = __pthread_self(); + if (__sigset_test(&self->sigmask, sig)) { + /* Blocked: hold it until something unblocks it. Standard signals do + * not queue, so a repeat while blocked collapses into this one. */ + __sigset_add(&self->sigpending, sig); + return; + } + + dispatch_signal(sig); + __sig_deliver_pending(); } #endif diff --git a/libc-top-half/musl/src/signal/sigpending.c b/libc-top-half/musl/src/signal/sigpending.c index 3d8ebf5a7..fa5d2f695 100644 --- a/libc-top-half/musl/src/signal/sigpending.c +++ b/libc-top-half/musl/src/signal/sigpending.c @@ -2,6 +2,10 @@ #include #ifdef __wasilibc_unmodified_upstream #include "syscall.h" +#else +#include +#include "pthread_impl.h" +#include "sigmask.h" #endif int sigpending(sigset_t *set) @@ -9,6 +13,12 @@ int sigpending(sigset_t *set) #ifdef __wasilibc_unmodified_upstream return syscall(SYS_rt_sigpending, set, _NSIG/8); #else - return EINVAL; + /* A signal the host has queued but not yet handed over is not in our + * pending set yet. Delivery happens at syscall boundaries, so yielding + * flushes the queue into it (blocked ones land in sigpending, deliverable + * ones run their handlers) and makes the answer current. */ + (void)__wasi_sched_yield(); + __sigset_copy(set, &__pthread_self()->sigpending); + return 0; #endif } \ No newline at end of file diff --git a/libc-top-half/musl/src/thread/pthread_create.c b/libc-top-half/musl/src/thread/pthread_create.c index 8a47cd62b..9d668be4f 100644 --- a/libc-top-half/musl/src/thread/pthread_create.c +++ b/libc-top-half/musl/src/thread/pthread_create.c @@ -516,6 +516,13 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att new->robust_list.head = &new->robust_list.head; new->canary = self->canary; new->sysinfo = self->sysinfo; +#ifndef __wasilibc_unmodified_upstream + /* POSIX: a new thread inherits the creator's signal mask and starts with + * nothing pending. Upstream gets this from clone(); the mask lives in the + * thread structure here, so copy it across explicitly. */ + new->sigmask = self->sigmask; + memset(&new->sigpending, 0, sizeof new->sigpending); +#endif /* Setup argument structure for the new thread on its stack. * It's safe to access from the caller only until the thread diff --git a/libc-top-half/musl/src/thread/pthread_sigmask.c b/libc-top-half/musl/src/thread/pthread_sigmask.c index edfc9535e..901c2b29f 100644 --- a/libc-top-half/musl/src/thread/pthread_sigmask.c +++ b/libc-top-half/musl/src/thread/pthread_sigmask.c @@ -19,8 +19,55 @@ int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict ol return ret; } #else +#include "pthread_impl.h" +#include "sigmask.h" + +/* WASIX has no kernel to hold the mask, so it lives in the thread structure + * and is consulted by __wasm_signal() when the host delivers a signal. */ int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict old) { + pthread_t self = __pthread_self(); + + if (set && (unsigned)how - SIG_BLOCK > 2U) return EINVAL; + + if (old) { + __sigset_copy(old, &self->sigmask); + /* The libc-internal signals are not the application's business. */ + if (sizeof old->__bits[0] == 8) { + old->__bits[0] &= ~0x380000000ULL; + } else { + old->__bits[0] &= ~0x80000000UL; + old->__bits[1] &= ~0x3UL; + } + } + + if (set) { + sigset_t next = self->sigmask; + switch (how) { + case SIG_BLOCK: + __sigset_or(&next, set); + break; + case SIG_UNBLOCK: + __sigset_andnot(&next, set); + break; + case SIG_SETMASK: + __sigset_copy(&next, set); + break; + } + + /* An application must never be able to block the signals libc uses + * internally, or pthread_cancel() and timers stop working. */ + __sigset_del(&next, SIGTIMER); + __sigset_del(&next, SIGCANCEL); + __sigset_del(&next, SIGSYNCCALL); + + self->sigmask = next; + + /* Unblocking has to deliver: signals only ever arrive at syscall + * boundaries, so nothing else would pick these up. */ + if (how != SIG_BLOCK) __sig_deliver_pending(); + } + return 0; } #endif \ No newline at end of file