diff --git a/Makefile b/Makefile index 9a0ed40f..c7dbcade 100644 --- a/Makefile +++ b/Makefile @@ -212,6 +212,11 @@ $(BUILD_DIR)/test-tgkill-directed: tests/test-tgkill-directed.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread +# test-sigtimedwait uses a sender pthread to verify thread-directed waits. +$(BUILD_DIR)/test-sigtimedwait: tests/test-sigtimedwait.c | $(BUILD_DIR) + @echo " CROSS $< (with -lpthread)" + $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + # test-osync-requeue drives a raw FUTEX_REQUEUE against a plain-FUTEX_WAIT # waiter (musl unlock_requeue pattern) to guard the os_sync wake-at-source # degradation; needs -lpthread. diff --git a/scripts/check-syscall-coverage.py b/scripts/check-syscall-coverage.py index e1610dc6..81faacc1 100644 --- a/scripts/check-syscall-coverage.py +++ b/scripts/check-syscall-coverage.py @@ -25,6 +25,7 @@ "eventfd2": {"eventfd"}, "rt_sigaction": {"sigaction"}, "rt_sigprocmask": {"sigprocmask"}, + "rt_sigtimedwait": {"sigwait", "sigwaitinfo", "sigtimedwait"}, "signalfd4": {"signalfd"}, } diff --git a/src/syscall/abi.h b/src/syscall/abi.h index 2c27320b..f619c62b 100644 --- a/src/syscall/abi.h +++ b/src/syscall/abi.h @@ -107,6 +107,7 @@ #define SYS_rt_sigaction 134 #define SYS_rt_sigprocmask 135 #define SYS_rt_sigpending 136 +#define SYS_rt_sigtimedwait 137 #define SYS_rt_sigqueueinfo 138 #define SYS_rt_sigreturn 139 #define SYS_setpriority 140 diff --git a/src/syscall/dispatch.tbl b/src/syscall/dispatch.tbl index 7962dfa6..9ef54cbc 100644 --- a/src/syscall/dispatch.tbl +++ b/src/syscall/dispatch.tbl @@ -114,6 +114,7 @@ SYS_rt_sigsuspend sc_rt_sigsuspend 1 SYS_rt_sigaction sc_rt_sigaction 1 SYS_rt_sigprocmask sc_rt_sigprocmask 1 SYS_rt_sigpending sc_rt_sigpending 0 +SYS_rt_sigtimedwait sc_rt_sigtimedwait 1 SYS_rt_sigreturn sc_rt_sigreturn 1 SYS_rt_sigqueueinfo sc_rt_sigqueueinfo 1 SYS_rt_tgsigqueueinfo sc_rt_tgsigqueueinfo 1 diff --git a/src/syscall/signal.c b/src/syscall/signal.c index d6f628a9..d85f3023 100644 --- a/src/syscall/signal.c +++ b/src/syscall/signal.c @@ -1332,7 +1332,162 @@ int64_t signal_rt_sigpending(guest_t *g, uint64_t set_gva, uint64_t sigsetsize) return 0; } -/* sigaltstack. */ +/* rt_sigtimedwait. */ + +/* Try to consume one signal from the set under sig_lock. + * Returns the signal number if one was found and dequeued, 0 otherwise. + * Populates *info_out with the queued siginfo metadata. + */ +static int sigtimedwait_try_dequeue(uint64_t mask, signal_rt_info_t *info_out) +{ + pthread_mutex_lock(&sig_lock); + + /* Private (thread-directed) set first, then shared (process-directed), + * matching Linux dequeue_signal() priority. + */ + signal_pending_t *tp = current_thread ? ¤t_thread->tpending : NULL; + uint64_t thread_m = tp ? (tp->pending & mask) : 0; + uint64_t shared_m = sig_state.shared.pending & mask; + + if ((thread_m | shared_m) == 0) { + pthread_mutex_unlock(&sig_lock); + return 0; + } + + int signum; + signal_pending_t *src; + if (thread_m) { + signum = bit_ctz64(thread_m) + 1; + src = tp; + } else { + signum = bit_ctz64(shared_m) + 1; + src = &sig_state.shared; + } + + /* Dequeue: same logic as signal_deliver. */ + if (signum >= LINUX_SIGRTMIN) { + signal_rt_dequeue_locked(src, signum, info_out); + } else { + *info_out = signal_standard_peek_locked(src, signum); + src->std_info_valid[signum - 1] = false; + src->pending &= ~sig_bit(signum); + } + refresh_pending_hint_locked(); + + pthread_mutex_unlock(&sig_lock); + return signum; +} + +int64_t signal_rt_sigtimedwait(guest_t *g, + uint64_t set_gva, + uint64_t info_gva, + uint64_t timeout_gva, + uint64_t sigsetsize) +{ + if (sigsetsize != 8) + return -LINUX_EINVAL; + if (!set_gva) + return -LINUX_EFAULT; + + uint64_t mask; + if (guest_read_small(g, set_gva, &mask, sizeof(mask)) < 0) + return -LINUX_EFAULT; + + /* SIGKILL and SIGSTOP cannot be caught or waited for. Remove them from + * the wait mask silently, matching Linux do_sigtimedwait behavior. + */ + uint64_t unmaskable = sig_bit(LINUX_SIGKILL) | sig_bit(LINUX_SIGSTOP); + mask &= ~unmaskable; + + /* Determine deadline: NULL timeout_gva means block indefinitely. + * Zero timespec means poll once. + */ + bool has_timeout = (timeout_gva != 0); + int64_t remaining_ns = 0; + + if (has_timeout) { + linux_timespec_t lts; + if (guest_read_small(g, timeout_gva, <s, sizeof(lts)) < 0) + return -LINUX_EFAULT; + /* Negative tv_sec is invalid (matches kernel hrtimer validation). */ + if (lts.tv_sec < 0 || lts.tv_nsec < 0 || lts.tv_nsec >= 1000000000LL) + return -LINUX_EINVAL; + /* Saturate to a large positive to avoid overflow. */ + if (lts.tv_sec > (INT64_MAX / 1000000000LL)) + remaining_ns = INT64_MAX; + else + remaining_ns = lts.tv_sec * 1000000000LL + lts.tv_nsec; + } + + /* Poll/wait loop. */ +#define SIGWAIT_CHUNK_NS 1000000LL /* 1ms chunks */ + + while (1) { + signal_rt_info_t info; + int signum = sigtimedwait_try_dequeue(mask, &info); + if (signum > 0) { + /* Populate guest siginfo_t if the caller wants it. */ + if (info_gva) { + linux_siginfo_t si; + memset(&si, 0, sizeof(si)); + si.si_signo = signum; + si.si_code = info.si_code; + si.si_pid = info.si_pid; + si.si_uid = (int32_t) info.si_uid; + si.si_value = info.si_ptr; + if (guest_write_small(g, info_gva, &si, sizeof(si)) < 0) + return -LINUX_EFAULT; + } + return signum; + } + + /* For a zero timeout (poll-once), bail immediately. */ + if (has_timeout && remaining_ns <= 0) + return -LINUX_EAGAIN; + + /* Exit if the process is tearing down. */ + if (proc_exit_group_requested()) + return -LINUX_EINTR; + + /* If a non-waited signal is pending, return -EINTR so the caller's + * library (musl/glibc) can restart or propagate. + */ + uint64_t *blocked = thread_blocked_ptr(); + uint64_t deliverable; + pthread_mutex_lock(&sig_lock); + deliverable = self_pending_locked() & ~*blocked & ~mask; + pthread_mutex_unlock(&sig_lock); + if (deliverable) + return -LINUX_EINTR; + + /* Sleep one chunk, then recheck. */ + int64_t sleep_ns = + has_timeout ? (remaining_ns < SIGWAIT_CHUNK_NS ? remaining_ns + : SIGWAIT_CHUNK_NS) + : SIGWAIT_CHUNK_NS; + struct timespec req = { + .tv_sec = sleep_ns / 1000000000LL, + .tv_nsec = sleep_ns % 1000000000LL, + }; + struct timespec rem = {0}; + if (nanosleep(&req, &rem) < 0) { + /* Host EINTR: account for time already slept. */ + int64_t slept = + sleep_ns - (rem.tv_sec * 1000000000LL + rem.tv_nsec); + if (slept < 0) + slept = 0; + if (has_timeout) + remaining_ns -= slept; + /* Recheck immediately (will catch deliverable signal). */ + continue; + } + if (has_timeout) + remaining_ns -= sleep_ns; + } + +#undef SIGWAIT_CHUNK_NS +} + int64_t signal_sigaltstack(guest_t *g, uint64_t ss_gva, uint64_t old_ss_gva) { diff --git a/src/syscall/signal.h b/src/syscall/signal.h index 4bb29d9e..237ec3fc 100644 --- a/src/syscall/signal.h +++ b/src/syscall/signal.h @@ -361,6 +361,20 @@ int64_t signal_rt_sigsuspend(guest_t *g, /* Handle rt_sigpending (SYS 136). */ int64_t signal_rt_sigpending(guest_t *g, uint64_t set_gva, uint64_t sigsetsize); +/* Handle rt_sigtimedwait (SYS 137). + * Synchronously consume a pending signal whose number is in *set*. + * info_gva (may be 0): if non-zero, populate the guest siginfo_t there. + * timeout_gva (may be 0): if zero, block indefinitely; otherwise block for + * at most the specified duration. Returns the signal number on success, + * -EAGAIN if the timeout expired with no matching signal, or -EINTR if an + * unrelated signal arrived while waiting. + */ +int64_t signal_rt_sigtimedwait(guest_t *g, + uint64_t set_gva, + uint64_t info_gva, + uint64_t timeout_gva, + uint64_t sigsetsize); + /* Handle sigaltstack (SYS 132). ss_gva is pointer to new stack_t (or 0), * old_ss_gva is pointer to receive current stack_t (or 0). */ diff --git a/src/syscall/syscall.c b/src/syscall/syscall.c index bac5168b..ecab15fb 100644 --- a/src/syscall/syscall.c +++ b/src/syscall/syscall.c @@ -353,6 +353,8 @@ SC_FORWARD(sc_rt_sigprocmask, signal_rt_sigprocmask(g, (int) x0, x1, x2, x3)) SC_FORWARD(sc_sigaltstack, signal_sigaltstack(g, x0, x1)) SC_FORWARD(sc_rt_sigsuspend, signal_rt_sigsuspend(g, x0, x1)) SC_FORWARD(sc_rt_sigpending, signal_rt_sigpending(g, x0, x1)) +SC_FORWARD(sc_rt_sigtimedwait, + signal_rt_sigtimedwait(g, x0, x1, x2, x3)) /* System info */ SC_FORWARD(sc_uname, sys_uname(g, x0)) diff --git a/tests/test-sigtimedwait.c b/tests/test-sigtimedwait.c new file mode 100644 index 00000000..038e0474 --- /dev/null +++ b/tests/test-sigtimedwait.c @@ -0,0 +1,196 @@ +/* + * Test rt_sigtimedwait (SYS 137) + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Verifies: + * 1. sigwait() consumes a pending signal synchronously. + * 2. sigwaitinfo() returns the correct signal number and + * populates siginfo_t. + * 3. sigtimedwait() with a zero timeout returns EAGAIN when + * no signal is pending (poll-once semantic). + * 4. sigtimedwait() with a short timeout returns EAGAIN after + * expiry when no signal arrives. + * 5. A signal sent from another thread is consumed by + * sigwaitinfo() in the waiting thread. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +static int failures; + +#define CHECK(cond, fmt, ...) \ + do { \ + if (!(cond)) { \ + printf("FAIL: " fmt "\n", ##__VA_ARGS__); \ + failures++; \ + } \ + } while (0) + +/* ------------------------------------------------------------------ */ +/* Test 1: sigwait() consumes a pending SIGUSR1 */ +/* ------------------------------------------------------------------ */ +static void test_sigwait_basic(void) +{ + printf("test-sigtimedwait: 1. sigwait basic... "); + + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGUSR1); + sigprocmask(SIG_BLOCK, &set, NULL); + + /* Queue SIGUSR1 to ourselves so it is pending. */ + kill(getpid(), SIGUSR1); + + int sig = 0; + int rc = sigwait(&set, &sig); + CHECK(rc == 0, "sigwait returned %d (errno %d)", rc, errno); + CHECK(sig == SIGUSR1, "sigwait returned sig %d, expected %d", sig, SIGUSR1); + + sigprocmask(SIG_UNBLOCK, &set, NULL); + printf("PASS\n"); +} + +/* ------------------------------------------------------------------ */ +/* Test 2: sigwaitinfo() populates siginfo_t */ +/* ------------------------------------------------------------------ */ +static void test_sigwaitinfo_info(void) +{ + printf("test-sigtimedwait: 2. sigwaitinfo populates siginfo... "); + + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGUSR2); + sigprocmask(SIG_BLOCK, &set, NULL); + + kill(getpid(), SIGUSR2); + + siginfo_t info; + memset(&info, 0, sizeof(info)); + int rc = sigwaitinfo(&set, &info); + CHECK(rc == SIGUSR2, "sigwaitinfo returned %d, expected %d", rc, SIGUSR2); + CHECK(info.si_signo == SIGUSR2, "si_signo=%d, expected %d", info.si_signo, + SIGUSR2); + + sigprocmask(SIG_UNBLOCK, &set, NULL); + printf("PASS\n"); +} + +/* ------------------------------------------------------------------ */ +/* Test 3: sigtimedwait() poll-once (zero timeout) -> EAGAIN */ +/* ------------------------------------------------------------------ */ +static void test_sigtimedwait_poll_eagain(void) +{ + printf("test-sigtimedwait: 3. zero timeout EAGAIN... "); + + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGUSR1); + sigprocmask(SIG_BLOCK, &set, NULL); + + /* No signal queued. */ + struct timespec zero = {0, 0}; + siginfo_t info; + int rc = sigtimedwait(&set, &info, &zero); + CHECK(rc == -1 && errno == EAGAIN, "expected EAGAIN, got rc=%d errno=%d", + rc, errno); + + sigprocmask(SIG_UNBLOCK, &set, NULL); + printf("PASS\n"); +} + +/* ------------------------------------------------------------------ */ +/* Test 4: sigtimedwait() with short timeout -> EAGAIN after expiry */ +/* ------------------------------------------------------------------ */ +static void test_sigtimedwait_timeout(void) +{ + printf("test-sigtimedwait: 4. short timeout EAGAIN... "); + + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGUSR1); + sigprocmask(SIG_BLOCK, &set, NULL); + + /* 20ms timeout; no signal will arrive. */ + struct timespec ts = {0, 20000000}; + siginfo_t info; + int rc = sigtimedwait(&set, &info, &ts); + CHECK(rc == -1 && errno == EAGAIN, "expected EAGAIN, got rc=%d errno=%d", + rc, errno); + + sigprocmask(SIG_UNBLOCK, &set, NULL); + printf("PASS\n"); +} + +/* ------------------------------------------------------------------ */ +/* Test 5: signal sent from another thread is consumed by sigwaitinfo */ +/* ------------------------------------------------------------------ */ +static pthread_t waiter_tid; + +static void *sender_fn(void *arg) +{ + (void) arg; + /* Give the main thread time to enter sigwaitinfo. */ + struct timespec ts = {0, 50000000}; /* 50ms */ + nanosleep(&ts, NULL); + pthread_kill(waiter_tid, SIGUSR1); + return NULL; +} + +static void test_sigwaitinfo_thread(void) +{ + printf("test-sigtimedwait: 5. cross-thread sigwaitinfo... "); + + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGUSR1); + /* Block SIGUSR1 so it does not fire the default handler. */ + pthread_sigmask(SIG_BLOCK, &set, NULL); + + waiter_tid = pthread_self(); + + pthread_t sender; + pthread_create(&sender, NULL, sender_fn, NULL); + + /* Wait up to 2 seconds for SIGUSR1. */ + struct timespec ts = {2, 0}; + siginfo_t info; + memset(&info, 0, sizeof(info)); + int rc = sigtimedwait(&set, &info, &ts); + + pthread_join(sender, NULL); + + CHECK(rc == SIGUSR1, "sigtimedwait returned %d (errno %d)", rc, errno); + if (rc == SIGUSR1) + CHECK(info.si_signo == SIGUSR1, "si_signo=%d", info.si_signo); + + pthread_sigmask(SIG_UNBLOCK, &set, NULL); + printf("PASS\n"); +} + +/* ------------------------------------------------------------------ */ + +int main(void) +{ + printf("test-sigtimedwait: starting\n"); + + test_sigwait_basic(); + test_sigwaitinfo_info(); + test_sigtimedwait_poll_eagain(); + test_sigtimedwait_timeout(); + test_sigwaitinfo_thread(); + + int total = 5; + int passed = total - failures; + printf("test-sigtimedwait: %d passed, %d failed - %s\n", passed, failures, + failures ? "FAIL" : "PASS"); + return failures ? 1 : 0; +}