diff --git a/litebox/src/platform/mock.rs b/litebox/src/platform/mock.rs index 1faf8e481f..4ac52d1697 100644 --- a/litebox/src/platform/mock.rs +++ b/litebox/src/platform/mock.rs @@ -196,6 +196,13 @@ impl Instant for MockInstant { time: self.time.checked_add(duration_millis)?, }) } + + fn saturating_add(&self, duration: core::time::Duration) -> Self { + let duration_millis = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX); + MockInstant { + time: self.time.saturating_add(duration_millis), + } + } } pub(crate) struct MockSystemTime { diff --git a/litebox/src/sync/futex.rs b/litebox/src/sync/futex.rs index 32556b7a86..526d7d69f0 100644 --- a/litebox/src/sync/futex.rs +++ b/litebox/src/sync/futex.rs @@ -97,8 +97,8 @@ impl /// This function tests once if the futex word matches the expected value, /// returning immediately with /// [`FutexError::ImmediatelyWokenBecauseValueMismatch`] if it does not. - /// Otherwise, it waits until woken by a corresponding until - /// [`FutexManager::wake`] is called targeting the same futex word or until + /// Otherwise, it waits until woken by a corresponding call to + /// [`FutexManager::wake`] targeting the same futex word or until /// the wait times out or is interrupted. /// /// If `bitset` is `Some`, then the waiter is only woken if the wake call's @@ -123,8 +123,8 @@ impl done: AtomicBool::new(false), },)); - // Insert into the bucket's list. It will be removed when woken or the - // entry goes out of scope. + // Insert into the bucket's list. It will be removed when woken, when the + // wait fails, or when the entry goes out of scope. entry.as_mut().insert(bucket); // Check the value once. Do this only after inserting into the list so @@ -135,8 +135,16 @@ impl // Only return when woken--don't reevaluate the futex word. This // ensures that the rate control mechanisms provided by the futex // interface are effective. - cx.wait_until(|| entry.get().done.load(Ordering::Acquire)) - .map_err(FutexError::WaitError) + let result = cx.wait_until(|| entry.get().done.load(Ordering::Acquire)); + if result.is_err() { + // A concurrent wake may occur while we're handling the timeout or interruption. In that case, + // the entry will indicate whether it was woken, and we should respect that. + entry.as_mut().remove(); + if entry.get().done.load(Ordering::Acquire) { + return Ok(()); + } + } + result.map_err(FutexError::WaitError) } /// Wakes waiters on the given futex word. @@ -382,4 +390,46 @@ mod tests { assert!((1..=3).contains(&woken)); } + + #[test] + fn test_futex_wait_woken_as_interrupted_succeeds() { + use crate::event::wait::CheckForInterrupt; + + /// Wakes the futex word, then interrupts the wait. + struct WakeThenInterrupt<'a> { + futex_manager: &'a FutexManager, + futex_addr: ::RawMutPointer, + } + impl CheckForInterrupt for WakeThenInterrupt<'_> { + fn check_for_interrupt(&self) -> bool { + let woken = self + .futex_manager + .wake(self.futex_addr, NonZeroU32::new(1).unwrap(), None) + .unwrap(); + assert_eq!(woken, 1); + true + } + } + + let platform = MockPlatform::new(); + let _litebox = LiteBox::new(platform); + let futex_manager = FutexManager::new(); + let futex_word = AtomicU32::new(0); + let futex_addr = + ::RawMutPointer::from_usize( + futex_word.as_ptr() as usize, + ); + let check = WakeThenInterrupt { + futex_manager: &futex_manager, + futex_addr, + }; + let wait_state = WaitState::new(platform); + let result = futex_manager.wait( + &wait_state.context().with_check_for_interrupt(&check), + futex_addr, + 0, + None, + ); + assert!(result.is_ok(), "{result:?}"); + } } diff --git a/litebox/src/utilities/loan_list.rs b/litebox/src/utilities/loan_list.rs index 54af654500..9e6e02de9e 100644 --- a/litebox/src/utilities/loan_list.rs +++ b/litebox/src/utilities/loan_list.rs @@ -111,7 +111,6 @@ impl<'a, Platform: RawSyncPrimitivesProvider, T> LoanListEntry<'a, Platform, T> /// completes and the entry is fully returned. /// /// If the entry is not currently inserted, this method does nothing. - #[cfg_attr(not(test), expect(dead_code))] pub fn remove(self: Pin<&mut Self>) { if let Some(list) = self.list { list.remove_node(&self.node); diff --git a/litebox_broker_platform_linux_userland/Cargo.toml b/litebox_broker_platform_linux_userland/Cargo.toml index 5fa318ffbf..73d2689cbc 100644 --- a/litebox_broker_platform_linux_userland/Cargo.toml +++ b/litebox_broker_platform_linux_userland/Cargo.toml @@ -9,7 +9,7 @@ litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1. litebox_platform = { path = "../litebox_platform", version = "0.1.0" } # getpgrp, whose result rustix cannot represent in a foreign PID namespace. libc = { version = "0.2.177", default-features = false } -rustix = { version = "1.1.2", default-features = false, features = ["event", "fs", "net", "std", "termios", "thread"] } +rustix = { version = "1.1.2", default-features = false, features = ["event", "fs", "net", "std", "termios", "thread", "time"] } [features] lock_tracing = ["litebox_platform/lock_tracing"] diff --git a/litebox_broker_platform_linux_userland/src/time.rs b/litebox_broker_platform_linux_userland/src/time.rs index 0689b0df16..227a244254 100644 --- a/litebox_broker_platform_linux_userland/src/time.rs +++ b/litebox_broker_platform_linux_userland/src/time.rs @@ -18,7 +18,9 @@ impl TimeProvider for LinuxTimeProvider { type SystemTime = LinuxSystemTime; fn now(&self) -> Self::Instant { - LinuxInstant(std::time::Instant::now()) + let now = rustix::time::clock_gettime(rustix::time::ClockId::Monotonic); + // `CLOCK_MONOTONIC` is never negative. + LinuxInstant(Duration::try_from(now).unwrap_or(Duration::ZERO)) } fn current_time(&self) -> Self::SystemTime { @@ -28,16 +30,20 @@ impl TimeProvider for LinuxTimeProvider { /// Monotonic clock value for a Linux-userland broker. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct LinuxInstant(std::time::Instant); +pub struct LinuxInstant(Duration); impl InstantTrait for LinuxInstant { fn checked_duration_since(&self, earlier: &Self) -> Option { - self.0.checked_duration_since(earlier.0) + self.0.checked_sub(earlier.0) } fn checked_add(&self, duration: Duration) -> Option { self.0.checked_add(duration).map(Self) } + + fn saturating_add(&self, duration: Duration) -> Self { + Self(self.0.saturating_add(duration)) + } } /// Wall-clock value for a Linux-userland broker. diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index b4d8fc058a..54f6e45368 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -2340,7 +2340,7 @@ pub enum FutexOperation { } bitflags::bitflags! { - #[derive(Debug)] + #[derive(Clone, Copy, Debug)] pub struct FutexFlags: i32 { const PRIVATE = 0x80; // FUTEX_PRIVATE_FLAG const CLOCK_REALTIME = 0x100; // FUTEX_CLOCK_REALTIME @@ -2979,6 +2979,11 @@ pub enum SyscallRequest { size: i32, flags: EpollCreateFlags, }, + Poll { + fds: UserPtrMut, + nfds: usize, + timeout: TimeParam, + }, Ppoll { fds: UserPtrMut, nfds: usize, @@ -3578,9 +3583,7 @@ impl SyscallRequest { sys_req!(Ppoll { fds:*, nfds, timeout: { =*> TimeParam::timespec_old }, sigmask:*, sigsetsize }) } #[cfg(target_arch = "x86_64")] - Sysno::poll => { - sys_req!(Ppoll { fds:*, nfds, timeout: { => TimeParam::Milliseconds }, sigmask: { None }, sigsetsize: { 0 } }) - } + Sysno::poll => sys_req!(Poll { fds:*, nfds, timeout: { => TimeParam::Milliseconds } }), #[cfg(target_arch = "x86_64")] Sysno::select => { sys_req!(Pselect { diff --git a/litebox_platform/src/time.rs b/litebox_platform/src/time.rs index f12be56130..0c97440690 100644 --- a/litebox_platform/src/time.rs +++ b/litebox_platform/src/time.rs @@ -32,6 +32,11 @@ pub trait Instant: Copy + Clone + PartialEq + Eq + PartialOrd + Ord + Send + Syn /// Returns a new instant advanced by `duration`, or `None` on overflow. fn checked_add(&self, duration: Duration) -> Option; + + /// Returns a new instant advanced by `duration`, or the latest instant the platform can + /// represent on overflow. + #[must_use] + fn saturating_add(&self, duration: Duration) -> Self; } /// A measurement of the system wall clock. diff --git a/litebox_platform_linux_kernel/src/lib.rs b/litebox_platform_linux_kernel/src/lib.rs index b31d609337..4c8dfdf8f5 100644 --- a/litebox_platform_linux_kernel/src/lib.rs +++ b/litebox_platform_linux_kernel/src/lib.rs @@ -259,6 +259,13 @@ impl InstantTrait for Instant { duration_micros.checked_mul(CPU_MHZ.load(core::sync::atomic::Ordering::Relaxed))?, )?)) } + + fn saturating_add(&self, duration: core::time::Duration) -> Self { + let duration_micros = u64::try_from(duration.as_micros()).unwrap_or(u64::MAX); + Instant(self.0.saturating_add( + duration_micros.saturating_mul(CPU_MHZ.load(core::sync::atomic::Ordering::Relaxed)), + )) + } } impl Instant { diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index e27ddecf2a..3bb06fa5a3 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -1055,6 +1055,11 @@ impl InstantTrait for Instant { inner: self.inner.checked_add(duration)?, }) } + fn saturating_add(&self, duration: Duration) -> Self { + Self { + inner: self.inner.saturating_add(duration), + } + } } pub struct SystemTime { diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index 10e26b75e3..8dd8d55d69 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -935,6 +935,14 @@ impl InstantTrait for Instant { let ticks = nanos / crate::arch::timer::REF_COUNTER_TICK_NANOS; Some(Instant(self.0.checked_add(ticks)?)) } + + fn saturating_add(&self, duration: core::time::Duration) -> Self { + let ticks = duration.as_nanos() / u128::from(crate::arch::timer::REF_COUNTER_TICK_NANOS); + Instant( + self.0 + .saturating_add(u64::try_from(ticks).unwrap_or(u64::MAX)), + ) + } } impl Instant { diff --git a/litebox_platform_macos_userland/src/lib.rs b/litebox_platform_macos_userland/src/lib.rs index d03e328a93..3a9b88289a 100644 --- a/litebox_platform_macos_userland/src/lib.rs +++ b/litebox_platform_macos_userland/src/lib.rs @@ -298,6 +298,11 @@ impl InstantTrait for Instant { inner: self.inner.checked_add(duration)?, }) } + fn saturating_add(&self, duration: Duration) -> Self { + Self { + inner: self.inner.saturating_add(duration), + } + } } pub struct SystemTime { diff --git a/litebox_platform_vm_kernel/src/lib.rs b/litebox_platform_vm_kernel/src/lib.rs index a6e6a0f907..7a565115ac 100644 --- a/litebox_platform_vm_kernel/src/lib.rs +++ b/litebox_platform_vm_kernel/src/lib.rs @@ -900,6 +900,11 @@ impl InstantTrait for Instant { let nanos: u64 = duration.as_nanos().try_into().ok()?; Some(Instant(self.0.checked_add(nanos)?)) } + + fn saturating_add(&self, duration: core::time::Duration) -> Self { + let nanos = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX); + Instant(self.0.saturating_add(nanos)) + } } impl SystemTimeTrait for SystemTime { diff --git a/litebox_platform_vm_userland/src/lib.rs b/litebox_platform_vm_userland/src/lib.rs index d524fdcfce..7567f043ff 100644 --- a/litebox_platform_vm_userland/src/lib.rs +++ b/litebox_platform_vm_userland/src/lib.rs @@ -417,6 +417,11 @@ impl InstantTrait for Instant { let nanos: u64 = duration.as_nanos().try_into().ok()?; Some(Instant(self.0.checked_add(nanos)?)) } + + fn saturating_add(&self, duration: core::time::Duration) -> Self { + let nanos = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX); + Instant(self.0.saturating_add(nanos)) + } } impl SystemTimeTrait for SystemTime { diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 3accfeea64..dfb5b822c3 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -1869,6 +1869,11 @@ impl InstantTrait for Instant { let new = self.0.checked_add(duration_100ns)?; Some(Instant(new)) } + + fn saturating_add(&self, duration: core::time::Duration) -> Self { + let duration_100ns = u64::try_from(duration.as_nanos() / 100).unwrap_or(u64::MAX); + Instant(self.0.saturating_add(duration_100ns)) + } } pub struct SystemTime { diff --git a/litebox_runner_linux_userland/tests/helpers.h b/litebox_runner_linux_userland/tests/helpers.h index 8dc94b83db..c67b1011b7 100644 --- a/litebox_runner_linux_userland/tests/helpers.h +++ b/litebox_runner_linux_userland/tests/helpers.h @@ -195,6 +195,32 @@ static inline void expect_poll_lacks(int fd, short events_mask, short forbidden, } } +// A signal handler that does nothing, so that the signal interrupts what it comes during. +static inline void handle_signal(int signal) { (void)signal; } + +// Sets the action of `signal` to run `handler` with `flags`. +static inline void set_handler(int signal, void (*handler)(int), int flags) { + struct sigaction action; + memset(&action, 0, sizeof(action)); + action.sa_handler = handler; + action.sa_flags = flags; + sigemptyset(&action.sa_mask); + TEST_ASSERT(sigaction(signal, &action, NULL) == 0, "sigaction"); +} + +// Sends `signal` to the process every millisecond until `stop(arg)` holds, for at most `max_ms`. +// Returns whether `stop(arg)` held. +static inline int signal_until(int signal, int (*stop)(void *), void *arg, long max_ms) { + for (long ms = 0; ms < max_ms; ms++) { + if (stop(arg)) { + return 1; + } + TEST_ASSERT(kill(getpid(), signal) == 0, "kill"); + sleep_ms(1); + } + return stop(arg); +} + #if defined(__x86_64__) || defined(__i386__) #define SPIN_HINT() __asm__ __volatile__("pause") #elif defined(__aarch64__) diff --git a/litebox_runner_linux_userland/tests/nanosleep_restart.c b/litebox_runner_linux_userland/tests/nanosleep_restart.c index 681ec95610..0b94136946 100644 --- a/litebox_runner_linux_userland/tests/nanosleep_restart.c +++ b/litebox_runner_linux_userland/tests/nanosleep_restart.c @@ -131,49 +131,26 @@ static void finish_sleeper(struct sleeper *s) { "restart_syscall after a new sleep"); } -static int is_done(struct sleep_result *result) { return atomic_load(&result->done); } +static int is_done(void *arg) { + struct sleep_result *result = arg; + return atomic_load(&result->done); +} -static int remain_written(struct sleep_result *result) { +static int remain_written(void *arg) { + struct sleep_result *result = arg; return __atomic_load_n(&result->remain.tv_sec, __ATOMIC_ACQUIRE) != -1; } -static int remain_written_or_done(struct sleep_result *result) { - return remain_written(result) || is_done(result); -} - -// Sends `signal` to the process every millisecond until `stop` holds, for at most `max_ms`. -// Returns whether `stop` held. -static int signal_until(struct sleep_result *result, int signal, - int (*stop)(struct sleep_result *), long max_ms) { - for (long ms = 0; ms < max_ms; ms++) { - if (stop(result)) { - return 1; - } - TEST_ASSERT(kill(getpid(), signal) == 0, "kill"); - sleep_ms(1); - } - return stop(result); -} +static int remain_written_or_done(void *arg) { return remain_written(arg) || is_done(arg); } // Ends a sleep that ignored signals cannot end with a handled SIGUSR1. static void end_sleep_with_handler(struct sleep_result *result) { - if (!signal_until(result, SIGUSR1, is_done, LIVENESS_MS)) { + if (!signal_until(SIGUSR1, is_done, result, LIVENESS_MS)) { printf("FAIL: SIGUSR1 did not end the sleep\n"); exit(1); } } -static void handle_signal(int signal) { (void)signal; } - -static void set_handler(int signal, void (*handler)(int), int flags) { - struct sigaction action; - memset(&action, 0, sizeof(action)); - action.sa_handler = handler; - action.sa_flags = flags; - sigemptyset(&action.sa_mask); - TEST_ASSERT(sigaction(signal, &action, NULL) == 0, "sigaction"); -} - static void expect_remain_untouched(const struct sleep_result *result, const char *what) { if (result->remain.tv_sec != -1 || result->remain.tv_nsec != -1) { printf("FAIL: %s: remain %ld.%09ld, expected untouched\n", what, @@ -219,7 +196,7 @@ static void test_ignored_signals_never_end_sleep(int flags) { if (interruption > 1) { __atomic_store_n(&s.result.remain.tv_sec, -1, __ATOMIC_RELEASE); } - if (!signal_until(&s.result, SIGCHLD, remain_written_or_done, LIVENESS_MS)) { + if (!signal_until(SIGCHLD, remain_written_or_done, &s.result, LIVENESS_MS)) { printf("FAIL: ignored signal: no interruption %d\n", interruption); exit(1); } @@ -251,7 +228,7 @@ static void test_ignored_signals_never_end_sleep(int flags) { static void test_ignored_signals_resume_sleep_until_deadline(void) { static struct sleeper s; start_sleeper(&s, 0, (struct timespec){.tv_sec = 0, .tv_nsec = SLEEP_NS}); - signal_until(&s.result, SIGCHLD, is_done, SIGNAL_WINDOW_MS); + signal_until(SIGCHLD, is_done, &s.result, SIGNAL_WINDOW_MS); finish_sleeper(&s); expect_slept_through(&s.result, "ignored signal"); @@ -279,7 +256,7 @@ static void test_ignored_signals_resume_sleep_until_deadline(void) { static void test_ignored_signals_restart_absolute_sleep(void) { static struct sleeper s; start_sleeper(&s, 1, (struct timespec){.tv_sec = 0, .tv_nsec = SLEEP_NS}); - signal_until(&s.result, SIGCHLD, is_done, SIGNAL_WINDOW_MS); + signal_until(SIGCHLD, is_done, &s.result, SIGNAL_WINDOW_MS); finish_sleeper(&s); expect_slept_through(&s.result, "absolute sleep, ignored signal"); @@ -294,7 +271,7 @@ static void test_handler_interrupts_absolute_sleep(void) { set_handler(SIGUSR1, handle_signal, SA_RESTART); static struct sleeper s; start_sleeper(&s, 1, (struct timespec){.tv_sec = 3600, .tv_nsec = 0}); - signal_until(&s.result, SIGCHLD, is_done, SIGNAL_WINDOW_MS); + signal_until(SIGCHLD, is_done, &s.result, SIGNAL_WINDOW_MS); if (is_done(&s.result)) { printf("FAIL: absolute sleep: an ignored signal ended it: ret=%d errno=%d\n", s.result.ret, s.result.err); diff --git a/litebox_runner_linux_userland/tests/wait_restart.c b/litebox_runner_linux_userland/tests/wait_restart.c new file mode 100644 index 0000000000..93d25291fb --- /dev/null +++ b/litebox_runner_linux_userland/tests/wait_restart.c @@ -0,0 +1,770 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "helpers.h" + +#define HOUR 3600 +// How long ignored signals are sent at a wait that must survive them. +#define SIGNAL_WINDOW_MS 100 +// Bounds waits for an outcome that is certain, so a broken implementation fails rather than +// hangs. +#define LIVENESS_MS (30L * 1000) +#define LIVENESS_TIMESPEC ((struct timespec){.tv_sec = LIVENESS_MS / 1000, .tv_nsec = 0}) +// The kernel's sigset_t, which raw syscalls take, is 8 bytes; glibc's is larger. +#define KERNEL_SIGSET_SIZE 8 +// Fills the revents of poll's fd until poll writes them. +#define REVENTS_UNWRITTEN 0x7fff + +enum call { + FUTEX_RELATIVE, + FUTEX_ABSOLUTE, + FUTEX_UNTIMED, + POLL, + PPOLL, + SELECT, + PSELECT6, +}; + +static const char *call_name(enum call call) { + static const char *const names[] = {"futex (relative timeout)", "futex (absolute timeout)", + "futex (no timeout)", "poll", + "ppoll", "select", + "pselect6"}; + return names[call]; +} + +struct waiter { + enum call call; + int poll_timeout_ms; + pthread_t thread; + sigset_t old_mask; + atomic_int ready; + atomic_int done; + long ret; + int err; + // What restart_syscall returns after the call, and is expected to return. + long restart_ret; + int restart_err; + long expected_restart_ret; + uint32_t word; + struct pollfd fds[1]; + // The relative timeout of futex, ppoll, and pselect6, which the last two write back. + struct timespec ts; + // Where the call takes `ts` from, which a test may point elsewhere. + struct timespec *ts_ptr; + // The timeout select writes back. + struct timeval tv; + // Never written, so its read end is ready only once a test makes it so. + int idle[2]; + // The signal mask ppoll and pselect6 set, if `use_mask`, and the waiter's mask after the + // call. + int use_mask; + sigset_t mask; + sigset_t mask_after; +}; + +static void test_signal_set(sigset_t *set) { + sigemptyset(set); + sigaddset(set, SIGCHLD); + sigaddset(set, SIGUSR1); + sigaddset(set, SIGUSR2); +} + +static long do_call(struct waiter *w) { + int fd = w->idle[0]; + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(fd, &readfds); + switch (w->call) { + case FUTEX_RELATIVE: + return syscall(SYS_futex, &w->word, FUTEX_WAIT_PRIVATE, 0, w->ts_ptr, NULL, 0); + case FUTEX_ABSOLUTE: { + struct timespec deadline; + TEST_ASSERT(clock_gettime(CLOCK_MONOTONIC, &deadline) == 0, "clock_gettime"); + deadline.tv_sec += HOUR; + return syscall(SYS_futex, &w->word, FUTEX_WAIT_BITSET_PRIVATE, 0, &deadline, NULL, + FUTEX_BITSET_MATCH_ANY); + } + case FUTEX_UNTIMED: + return syscall(SYS_futex, &w->word, FUTEX_WAIT_PRIVATE, 0, NULL, NULL, 0); + case POLL: +#ifdef SYS_poll + return syscall(SYS_poll, w->fds, 1, w->poll_timeout_ms); +#else + break; +#endif + case PPOLL: + return syscall(SYS_ppoll, w->fds, 1, w->ts_ptr, w->use_mask ? &w->mask : NULL, + KERNEL_SIGSET_SIZE); + case SELECT: +#ifdef SYS_select + return syscall(SYS_select, fd + 1, &readfds, NULL, NULL, &w->tv); +#else + break; +#endif + case PSELECT6: { + struct { + const sigset_t *mask; + size_t size; + } mask_arg = {&w->mask, KERNEL_SIGSET_SIZE}; + return syscall(SYS_pselect6, fd + 1, &readfds, NULL, NULL, w->ts_ptr, + w->use_mask ? &mask_arg : NULL); + } + } + die("call not supported on this architecture"); + return -1; +} + +static void *waiter_main(void *arg) { + struct waiter *w = arg; + if (w->use_mask) { + // Blocked outside the call only, so a handler shows which mask it was delivered under. + sigset_t urg; + sigemptyset(&urg); + sigaddset(&urg, SIGURG); + TEST_ASSERT(pthread_sigmask(SIG_BLOCK, &urg, NULL) == 0, "pthread_sigmask"); + } + atomic_store(&w->ready, 1); + errno = 0; + w->ret = do_call(w); + w->err = errno; + TEST_ASSERT(pthread_sigmask(SIG_BLOCK, NULL, &w->mask_after) == 0, "pthread_sigmask"); + // Signals still on their way must not interrupt what follows. + sigset_t set; + test_signal_set(&set); + TEST_ASSERT(pthread_sigmask(SIG_BLOCK, &set, NULL) == 0, "pthread_sigmask"); + errno = 0; + w->restart_ret = syscall(SYS_restart_syscall); + w->restart_err = errno; + atomic_store(&w->done, 1); + return NULL; +} + +// Prepares `call`, with a timeout of an hour if it has one. +static void init_waiter(struct waiter *w, enum call call) { + memset(w, 0, sizeof(*w)); + w->call = call; + w->poll_timeout_ms = -1; + w->expected_restart_ret = -1; + w->ts = (struct timespec){.tv_sec = HOUR, .tv_nsec = 0}; + w->ts_ptr = &w->ts; + w->tv = (struct timeval){.tv_sec = HOUR, .tv_usec = 0}; + TEST_ASSERT(pipe(w->idle) == 0, "pipe"); + w->fds[0] = (struct pollfd){.fd = w->idle[0], .events = POLLIN}; +} + +// Starts a waiter making the prepared call, then blocks the test's signals in the main thread, +// so the waiter takes them, and waits until it is about to call. +static void launch_waiter(struct waiter *w) { + TEST_ASSERT(pthread_create(&w->thread, NULL, waiter_main, w) == 0, "pthread_create"); + // Block the signals only after creating the waiter, which would otherwise inherit the mask. + sigset_t set; + test_signal_set(&set); + TEST_ASSERT(pthread_sigmask(SIG_BLOCK, &set, &w->old_mask) == 0, "pthread_sigmask"); + while (!atomic_load(&w->ready)) { + sched_yield(); + } +} + +static void start_waiter(struct waiter *w, enum call call) { + init_waiter(w, call); + launch_waiter(w); +} + +static int is_done(void *arg) { + struct waiter *w = arg; + return atomic_load(&w->done); +} + +// Sends ignored SIGCHLDs, which must not end the wait. +static void send_ignored_signals(struct waiter *w) { + if (signal_until(SIGCHLD, is_done, w, SIGNAL_WINDOW_MS)) { + printf("FAIL: %s: an ignored signal ended the wait: ret=%ld errno=%d\n", + call_name(w->call), w->ret, w->err); + exit(1); + } +} + +static void wait_done(struct waiter *w, int signal) { + if (!signal_until(signal, is_done, w, LIVENESS_MS)) { + printf("FAIL: %s: the wait did not end\n", call_name(w->call)); + exit(1); + } +} + +// Fails unless the waiter is still waiting. +static void expect_waiting(struct waiter *w, const char *what) { + if (is_done(w)) { + printf("FAIL: %s: %s: ret=%ld errno=%d\n", call_name(w->call), what, w->ret, w->err); + exit(1); + } +} + +// Joins the waiter once it is done, then takes any signals still pending, so a handler must still +// be installed for them. +static void finish_waiter(struct waiter *w) { + TEST_ASSERT(pthread_join(w->thread, NULL) == 0, "pthread_join"); + TEST_ASSERT(pthread_sigmask(SIG_SETMASK, &w->old_mask, NULL) == 0, "pthread_sigmask"); + close(w->idle[0]); + close(w->idle[1]); +} + +// Checks what the call and the restart_syscall after it returned. +static void expect_result(const struct waiter *w, long ret, int err) { + if (w->ret != ret || (ret == -1 && w->err != err)) { + printf("FAIL: %s: ret=%ld errno=%d, expected %ld errno=%d\n", call_name(w->call), w->ret, + w->err, ret, ret == -1 ? err : 0); + exit(1); + } + long restart = w->expected_restart_ret; + if (w->restart_ret != restart || (restart == -1 && w->restart_err != EINTR)) { + printf("FAIL: %s: restart_syscall then: ret=%ld errno=%d, expected %ld errno=%d\n", + call_name(w->call), w->restart_ret, w->restart_err, restart, + restart == -1 ? EINTR : 0); + exit(1); + } +} + +static long futex_wake(uint32_t *word) { + return syscall(SYS_futex, word, FUTEX_WAKE_PRIVATE, 1, NULL, NULL, 0); +} + +// Wakes the waiter once it waits, failing if it ends the wait otherwise. +static void wake_waiter(struct waiter *w) { + for (long ms = 0; ms < LIVENESS_MS; ms++) { + if (futex_wake(&w->word) == 1) { + return; + } + expect_waiting(w, "the wait ended without a wake"); + sleep_ms(1); + } + printf("FAIL: %s: no waiter to wake\n", call_name(w->call)); + exit(1); +} + +// A futex wait goes on through ignored signals until woken. +static void test_futex_wait_survives_ignored_signals(enum call call) { + static struct waiter w; + start_waiter(&w, call); + send_ignored_signals(&w); + wake_waiter(&w); + wait_done(&w, SIGCHLD); + finish_waiter(&w); + expect_result(&w, 0, 0); +} + +// A resumed futex wait checks its word again, so once the word changes, an ignored signal ends +// the wait with EAGAIN. Signals first make the waiter all but certainly wait by then. +static void test_resumed_futex_wait_checks_word(enum call call) { + static struct waiter w; + start_waiter(&w, call); + send_ignored_signals(&w); + __atomic_store_n(&w.word, 1, __ATOMIC_RELEASE); + wait_done(&w, SIGCHLD); + finish_waiter(&w); + expect_result(&w, -1, EAGAIN); +} + +// With SA_RESTART, a handler interrupts a futex wait with a timeout, but restarts one without. +static void test_futex_wait_with_restarting_handler(enum call call) { + set_handler(SIGUSR1, handle_signal, SA_RESTART); + static struct waiter w; + start_waiter(&w, call); + if (call == FUTEX_UNTIMED) { + if (signal_until(SIGUSR1, is_done, &w, SIGNAL_WINDOW_MS)) { + printf("FAIL: %s: a handler with SA_RESTART ended the wait: ret=%ld errno=%d\n", + call_name(call), w.ret, w.err); + exit(1); + } + wake_waiter(&w); + } + wait_done(&w, SIGUSR1); + finish_waiter(&w); + set_handler(SIGUSR1, SIG_DFL, 0); + expect_result(&w, call == FUTEX_UNTIMED ? 0 : -1, EINTR); +} + +#ifdef SYS_poll +static int revents_written_or_done(void *arg) { + struct waiter *w = arg; + return __atomic_load_n(&w->fds[0].revents, __ATOMIC_ACQUIRE) != REVENTS_UNWRITTEN || + is_done(w); +} + +// A resumed poll reads its fds again, so once they include a ready one, an ignored signal ends +// the poll with it ready. The revents poll writes when interrupted show it is resumed by then. +// A later restart_syscall polls the fds again. +static void test_resumed_poll_reads_fds(int timeout_ms) { + int ready[2]; + TEST_ASSERT(pipe(ready) == 0, "pipe"); + TEST_ASSERT(write(ready[1], "x", 1) == 1, "write"); + static struct waiter w; + init_waiter(&w, POLL); + w.poll_timeout_ms = timeout_ms; + w.fds[0].revents = REVENTS_UNWRITTEN; + w.expected_restart_ret = 1; + launch_waiter(&w); + if (!signal_until(SIGCHLD, revents_written_or_done, &w, LIVENESS_MS)) { + printf("FAIL: poll: no interruption\n"); + exit(1); + } + expect_waiting(&w, "an ignored signal ended the wait"); + __atomic_store_n(&w.fds[0].fd, ready[0], __ATOMIC_RELEASE); + wait_done(&w, SIGCHLD); + finish_waiter(&w); + close(ready[0]); + close(ready[1]); + expect_result(&w, 1, 0); + TEST_ASSERT(w.fds[0].revents == POLLIN, "the ready fd is polled"); +} +#endif + +static long time_left_sec(const struct waiter *w) { + return w->call == SELECT ? (long)w->tv.tv_sec : (long)w->ts.tv_sec; +} + +static int timeout_written(void *arg) { + struct waiter *w = arg; + if (w->call == SELECT) { + return __atomic_load_n(&w->tv.tv_sec, __ATOMIC_ACQUIRE) != HOUR || + __atomic_load_n(&w->tv.tv_usec, __ATOMIC_ACQUIRE) != 0; + } + return __atomic_load_n(&w->ts.tv_sec, __ATOMIC_ACQUIRE) != HOUR || + __atomic_load_n(&w->ts.tv_nsec, __ATOMIC_ACQUIRE) != 0; +} + +static int timeout_written_or_done(void *arg) { return timeout_written(arg) || is_done(arg); } + +// Signals the waiter with ignored SIGCHLDs until it writes back the time left, failing if that +// ends the wait. +static void interrupt_reissued_wait(struct waiter *w) { + if (!signal_until(SIGCHLD, timeout_written_or_done, w, LIVENESS_MS)) { + printf("FAIL: %s: no interruption\n", call_name(w->call)); + exit(1); + } + expect_waiting(w, "an ignored signal ended the wait"); +} + +static void expect_time_left(struct waiter *w) { + long sec = time_left_sec(w); + if (!timeout_written(w) || sec < 0 || sec > HOUR) { + printf("FAIL: %s: time left %ld s\n", call_name(w->call), sec); + exit(1); + } +} + +// An ignored signal interrupts ppoll, select, or pselect6, which writes back the time left, then +// goes on until its fd is ready. +static void test_wait_reissued_after_ignored_signal(enum call call) { + static struct waiter w; + start_waiter(&w, call); + interrupt_reissued_wait(&w); + TEST_ASSERT(write(w.idle[1], "x", 1) == 1, "write"); + wait_done(&w, SIGCHLD); + finish_waiter(&w); + expect_result(&w, 1, 0); + expect_time_left(&w); +} + +// Even with SA_RESTART, a handler interrupts poll, ppoll, select, and pselect6, poll writing back +// the revents and the others the time left, even of a timeout longer than they can wait. +static void test_wait_with_restarting_handler(enum call call) { + const long huge = 1000L * 1000 * 1000 * 1000; + set_handler(SIGUSR1, handle_signal, SA_RESTART); + static struct waiter w; + init_waiter(&w, call); + w.fds[0].revents = REVENTS_UNWRITTEN; + w.ts = (struct timespec){.tv_sec = huge, .tv_nsec = 0}; + w.tv = (struct timeval){.tv_sec = huge, .tv_usec = 0}; + launch_waiter(&w); + wait_done(&w, SIGUSR1); + finish_waiter(&w); + set_handler(SIGUSR1, SIG_DFL, 0); + expect_result(&w, -1, EINTR); + if (call == POLL || call == PPOLL) { + TEST_ASSERT(w.fds[0].revents == 0, "an interrupted poll writes back the revents"); + } + long sec = time_left_sec(&w); + if (call != POLL && (sec <= huge - HOUR || sec > huge)) { + printf("FAIL: %s: %ld s left of %ld s\n", call_name(call), sec, huge); + exit(1); + } +} + +// A timed-out ppoll, select, or pselect6 writes back no time left. +static void test_timed_out_wait_writes_back_zero(enum call call) { + struct waiter w; + init_waiter(&w, call); + w.ts = (struct timespec){.tv_sec = 0, .tv_nsec = 1000 * 1000}; + w.tv = (struct timeval){.tv_sec = 0, .tv_usec = 1000}; + long ret = do_call(&w); + close(w.idle[0]); + close(w.idle[1]); + TEST_ASSERT(ret == 0, "the wait times out"); + long sec = time_left_sec(&w); + long frac = call == SELECT ? (long)w.tv.tv_usec : w.ts.tv_nsec; + if (sec != 0 || frac != 0) { + printf("FAIL: %s: timed out with %ld.%ld left\n", call_name(call), sec, frac); + exit(1); + } +} + +static atomic_int handled; +static atomic_int handler_failed; +static sigset_t mask_in_handler; + +static void record_signal(int signal) { + (void)signal; + if (pthread_sigmask(SIG_BLOCK, NULL, &mask_in_handler) != 0) { + atomic_store(&handler_failed, 1); + } + atomic_store(&handled, 1); +} + +static void reset_handled(void) { + atomic_store(&handled, 0); + atomic_store(&handler_failed, 0); +} + +// Whether the handler ran, and could read its mask. +static int handler_ran(void) { + TEST_ASSERT(!atomic_load(&handler_failed), "pthread_sigmask in the handler"); + return atomic_load(&handled); +} + +static void block_signal(int signal, sigset_t *old) { + sigset_t block; + sigemptyset(&block); + sigaddset(&block, signal); + TEST_ASSERT(pthread_sigmask(SIG_BLOCK, &block, old) == 0, "pthread_sigmask"); +} + +// Blocks `signal` and makes it pending. +static void block_and_raise(int signal, sigset_t *old) { + block_signal(signal, old); + TEST_ASSERT(raise(signal) == 0, "raise"); +} + +// Calls ppoll or pselect6 in this thread, with no signal blocked for the call, waiting at most +// `*timeout`. +static long call_with_empty_mask(enum call call, struct timespec *timeout) { + struct waiter w; + init_waiter(&w, call); + w.use_mask = 1; + sigemptyset(&w.mask); + w.ts_ptr = timeout; + long ret = do_call(&w); + int err = errno; + close(w.idle[0]); + close(w.idle[1]); + errno = err; + return ret; +} + +static atomic_int probe_hits; + +static void count_probe_hit(int signal) { + (void)signal; + atomic_fetch_add(&probe_hits, 1); +} + +// Fails unless `signal` is blocked and not pending, which unblocking it under a handler shows. +static void expect_blocked_not_pending(int signal, const char *what) { + sigset_t mask; + TEST_ASSERT(pthread_sigmask(SIG_BLOCK, NULL, &mask) == 0, "pthread_sigmask"); + int blocked = sigismember(&mask, signal); + struct sigaction probe, old_action; + memset(&probe, 0, sizeof(probe)); + probe.sa_handler = count_probe_hit; + sigemptyset(&probe.sa_mask); + TEST_ASSERT(sigaction(signal, &probe, &old_action) == 0, "sigaction"); + atomic_store(&probe_hits, 0); + sigset_t only; + sigemptyset(&only); + sigaddset(&only, signal); + TEST_ASSERT(pthread_sigmask(SIG_UNBLOCK, &only, NULL) == 0, "pthread_sigmask"); + TEST_ASSERT(pthread_sigmask(SIG_BLOCK, &only, NULL) == 0, "pthread_sigmask"); + TEST_ASSERT(sigaction(signal, &old_action, NULL) == 0, "sigaction"); + int pending = atomic_load(&probe_hits) != 0; + if (!blocked || pending) { + printf("FAIL: %s: signal %d blocked=%d pending=%d, expected blocked and not pending\n", + what, signal, blocked, pending); + exit(1); + } +} + +// Fails unless the call failed with EINTR after the handler ran with the call's empty mask +// besides its own signal, SIGUSR1, and both SIGUSR1 and SIGCHLD are blocked again and handled. +static void expect_handler_ran_with_call_mask(long ret, int err, const char *what) { + if (ret != -1 || err != EINTR || !handler_ran()) { + printf("FAIL: %s with a mask: ret=%ld errno=%d handled=%d, expected EINTR, handled\n", + what, ret, err, atomic_load(&handled)); + exit(1); + } + if (!sigismember(&mask_in_handler, SIGUSR1) || sigismember(&mask_in_handler, SIGCHLD)) { + printf("FAIL: %s with a mask: SIGUSR1 blocked=%d SIGCHLD blocked=%d in the handler\n", + what, sigismember(&mask_in_handler, SIGUSR1), + sigismember(&mask_in_handler, SIGCHLD)); + exit(1); + } + expect_blocked_not_pending(SIGUSR1, what); + expect_blocked_not_pending(SIGCHLD, what); +} + +// A signal pending while blocked, which the call's mask unblocks, interrupts ppoll or pselect6 +// and reaches its handler, which runs with the call's mask, and whose frame saves the mask from +// before the call. +static void test_call_mask_lets_handler_run(enum call call) { + set_handler(SIGUSR1, record_signal, 0); + reset_handled(); + sigset_t old; + block_and_raise(SIGUSR1, &old); + block_signal(SIGCHLD, NULL); + struct timespec timeout = LIVENESS_TIMESPEC; + errno = 0; + long ret = call_with_empty_mask(call, &timeout); + expect_handler_ran_with_call_mask(ret, errno, call_name(call)); + TEST_ASSERT(pthread_sigmask(SIG_SETMASK, &old, NULL) == 0, "pthread_sigmask"); + set_handler(SIGUSR1, SIG_DFL, 0); +} + +// An ignored signal pending while blocked, which the call's mask unblocks, interrupts ppoll or +// pselect6, which discards it and is reissued with the time left, timing out. With its timeout in +// read-only memory, the call cannot write back the time left, so it fails with EINTR instead, as +// reissuing it would wait the whole timeout again. +static void test_call_mask_discards_ignored_signal(enum call call) { + long page = sysconf(_SC_PAGESIZE); + struct timespec *timeout = + mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + TEST_ASSERT(timeout != MAP_FAILED, "mmap"); + for (int read_only = 0; read_only <= 1; read_only++) { + TEST_ASSERT(mprotect(timeout, page, PROT_READ | PROT_WRITE) == 0, "mprotect"); + // A read-only timeout bounds how long a broken implementation waits. + *timeout = read_only ? LIVENESS_TIMESPEC + : (struct timespec){.tv_sec = 0, .tv_nsec = 50 * 1000 * 1000}; + if (read_only) { + TEST_ASSERT(mprotect(timeout, page, PROT_READ) == 0, "mprotect"); + } + sigset_t old; + block_and_raise(SIGCHLD, &old); + errno = 0; + long ret = call_with_empty_mask(call, timeout); + int err = errno; + if (read_only ? ret != -1 || err != EINTR : ret != 0) { + printf("FAIL: %s with a mask, ignored signal, %s timeout: ret=%ld errno=%d\n", + call_name(call), read_only ? "read-only" : "writable", ret, err); + exit(1); + } + expect_blocked_not_pending(SIGCHLD, call_name(call)); + TEST_ASSERT(pthread_sigmask(SIG_SETMASK, &old, NULL) == 0, "pthread_sigmask"); + } + TEST_ASSERT(munmap(timeout, page) == 0, "munmap"); +} + +// A ppoll or pselect6 that fails on its arguments after setting its mask restores the mask. +static void test_bad_arguments_restore_mask(enum call call) { + sigset_t unblock, old, mask; + sigemptyset(&unblock); + sigaddset(&unblock, SIGUSR1); + TEST_ASSERT(pthread_sigmask(SIG_UNBLOCK, &unblock, &old) == 0, "pthread_sigmask"); + struct timespec timeout = {.tv_sec = 5, .tv_nsec = 0}; + struct { + const sigset_t *mask; + size_t size; + } mask_arg = {&unblock, KERNEL_SIGSET_SIZE}; + struct pollfd fds[1] = {{.fd = -1, .events = POLLIN}}; + errno = 0; + long ret; + int expected; + if (call == PPOLL) { + // More fds than RLIMIT_NOFILE allows. + ret = syscall(SYS_ppoll, fds, UINT32_MAX, &timeout, &unblock, KERNEL_SIGSET_SIZE); + expected = EINVAL; + } else { + ret = syscall(SYS_pselect6, 1, (void *)1, NULL, NULL, &timeout, &mask_arg); + expected = EFAULT; + } + int err = errno; + TEST_ASSERT(pthread_sigmask(SIG_BLOCK, NULL, &mask) == 0, "pthread_sigmask"); + if (ret != -1 || err != expected || sigismember(&mask, SIGUSR1)) { + printf("FAIL: %s with bad arguments: ret=%ld errno=%d SIGUSR1 blocked=%d, expected " + "errno=%d and the mask restored\n", + call_name(call), ret, err, sigismember(&mask, SIGUSR1), expected); + exit(1); + } + TEST_ASSERT(timeout.tv_sec >= 0 && timeout.tv_sec <= 5 && timeout.tv_nsec >= 0 && + timeout.tv_nsec < 1000L * 1000 * 1000, + "the time left is valid"); + TEST_ASSERT(pthread_sigmask(SIG_SETMASK, &old, NULL) == 0, "pthread_sigmask"); +} + +// ppoll and pselect6 ignore the size of a NULL mask, as glibc's pselect passes a NULL mask with +// the size of the kernel's sigset_t. +static void test_null_mask_ignores_size(void) { + int idle[2]; + TEST_ASSERT(pipe(idle) == 0, "pipe"); + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(idle[0], &readfds); + struct timespec zero = {0}; + TEST_ASSERT(pselect(idle[0] + 1, &readfds, NULL, NULL, &zero, NULL) == 0, + "glibc pselect with a NULL mask"); + FD_SET(idle[0], &readfds); + struct { + const sigset_t *mask; + size_t size; + } mask_arg = {NULL, 999}; + TEST_ASSERT(syscall(SYS_pselect6, idle[0] + 1, &readfds, NULL, NULL, &zero, &mask_arg) == 0, + "pselect6 with a NULL mask of a bad size"); + struct pollfd fds[1] = {{.fd = idle[0], .events = POLLIN}}; + TEST_ASSERT(syscall(SYS_ppoll, fds, 1, &zero, NULL, 999) == 0, + "ppoll with a NULL mask of a bad size"); + close(idle[0]); + close(idle[1]); +} + +static atomic_int usr2_handled; +static atomic_int usr2_handler_failed; +static sigset_t mask_in_usr2_handler; + +static void record_usr2(int signal) { + (void)signal; + if (pthread_sigmask(SIG_BLOCK, NULL, &mask_in_usr2_handler) != 0) { + atomic_store(&usr2_handler_failed, 1); + } + atomic_store(&usr2_handled, 1); +} + +static int usr2_handled_or_done(void *arg) { return atomic_load(&usr2_handled) || is_done(arg); } + +// A signal pending while ppoll or pselect6 blocks it with its mask, but unblocked without it, +// reaches its handler once an ignored signal interrupts the call, before the call is reissued. +// The handler runs with the mask from before the call, which the waiter blocks SIGURG in, and the +// call keeps that mask once it returns. +static void test_restored_mask_lets_handler_run(enum call call) { + set_handler(SIGUSR2, record_usr2, 0); + atomic_store(&usr2_handled, 0); + atomic_store(&usr2_handler_failed, 0); + static struct waiter w; + init_waiter(&w, call); + w.use_mask = 1; + sigemptyset(&w.mask); + sigaddset(&w.mask, SIGUSR2); + launch_waiter(&w); + // The call is underway once it writes back the time left, so SIGUSR2 all but certainly + // comes while the call blocks it. + interrupt_reissued_wait(&w); + TEST_ASSERT(kill(getpid(), SIGUSR2) == 0, "kill"); + if (!signal_until(SIGCHLD, usr2_handled_or_done, &w, LIVENESS_MS) || is_done(&w)) { + printf("FAIL: %s with a mask: handled=%d done=%d, expected the handler to run first\n", + call_name(call), atomic_load(&usr2_handled), is_done(&w)); + exit(1); + } + TEST_ASSERT(!atomic_load(&usr2_handler_failed), "pthread_sigmask in the handler"); + if (!sigismember(&mask_in_usr2_handler, SIGURG) || + !sigismember(&mask_in_usr2_handler, SIGUSR2)) { + printf("FAIL: %s with a mask: SIGURG blocked=%d SIGUSR2 blocked=%d in the handler\n", + call_name(call), sigismember(&mask_in_usr2_handler, SIGURG), + sigismember(&mask_in_usr2_handler, SIGUSR2)); + exit(1); + } + TEST_ASSERT(write(w.idle[1], "x", 1) == 1, "write"); + wait_done(&w, SIGCHLD); + finish_waiter(&w); + set_handler(SIGUSR2, SIG_DFL, 0); + expect_result(&w, 1, 0); + if (!sigismember(&w.mask_after, SIGURG) || sigismember(&w.mask_after, SIGUSR2)) { + printf("FAIL: %s with a mask: SIGURG blocked=%d SIGUSR2 blocked=%d after the call\n", + call_name(call), sigismember(&w.mask_after, SIGURG), + sigismember(&w.mask_after, SIGUSR2)); + exit(1); + } +} + +// epoll_pwait keeps its mask until the signal that interrupts it is delivered, like ppoll, but +// fails with EINTR even when that signal is ignored. +static void test_epoll_pwait_mask(void) { + int epfd = epoll_create1(0); + TEST_ASSERT(epfd >= 0, "epoll_create1"); + int idle[2]; + TEST_ASSERT(pipe(idle) == 0, "pipe"); + struct epoll_event event = {.events = EPOLLIN, .data.fd = idle[0]}; + TEST_ASSERT(epoll_ctl(epfd, EPOLL_CTL_ADD, idle[0], &event) == 0, "epoll_ctl"); + sigset_t empty; + sigemptyset(&empty); + struct epoll_event events[1]; + + set_handler(SIGUSR1, record_signal, 0); + reset_handled(); + sigset_t old; + block_and_raise(SIGUSR1, &old); + block_signal(SIGCHLD, NULL); + errno = 0; + long ret = + syscall(SYS_epoll_pwait, epfd, events, 1, (int)LIVENESS_MS, &empty, KERNEL_SIGSET_SIZE); + expect_handler_ran_with_call_mask(ret, errno, "epoll_pwait"); + set_handler(SIGUSR1, SIG_DFL, 0); + + TEST_ASSERT(raise(SIGCHLD) == 0, "raise"); + errno = 0; + ret = syscall(SYS_epoll_pwait, epfd, events, 1, (int)LIVENESS_MS, &empty, KERNEL_SIGSET_SIZE); + if (ret != -1 || errno != EINTR) { + printf("FAIL: epoll_pwait with a mask, ignored signal: ret=%ld errno=%d, expected " + "EINTR\n", + ret, errno); + exit(1); + } + expect_blocked_not_pending(SIGCHLD, "epoll_pwait"); + TEST_ASSERT(pthread_sigmask(SIG_SETMASK, &old, NULL) == 0, "pthread_sigmask"); + close(idle[0]); + close(idle[1]); + close(epfd); +} + +int main(void) { + printf("Testing wait restarts...\n"); + static const enum call futex_calls[] = {FUTEX_RELATIVE, FUTEX_ABSOLUTE, FUTEX_UNTIMED}; + for (size_t i = 0; i < sizeof(futex_calls) / sizeof(futex_calls[0]); i++) { + test_futex_wait_survives_ignored_signals(futex_calls[i]); + test_resumed_futex_wait_checks_word(futex_calls[i]); + test_futex_wait_with_restarting_handler(futex_calls[i]); + } +#ifdef SYS_poll + test_resumed_poll_reads_fds(-1); + test_resumed_poll_reads_fds(HOUR * 1000); + test_wait_with_restarting_handler(POLL); +#endif + static const enum call reissued_calls[] = { +#ifdef SYS_select + SELECT, +#endif + PPOLL, + PSELECT6, + }; + for (size_t i = 0; i < sizeof(reissued_calls) / sizeof(reissued_calls[0]); i++) { + test_wait_reissued_after_ignored_signal(reissued_calls[i]); + test_wait_with_restarting_handler(reissued_calls[i]); + test_timed_out_wait_writes_back_zero(reissued_calls[i]); + } + static const enum call masked_calls[] = {PPOLL, PSELECT6}; + for (size_t i = 0; i < sizeof(masked_calls) / sizeof(masked_calls[0]); i++) { + test_call_mask_lets_handler_run(masked_calls[i]); + test_call_mask_discards_ignored_signal(masked_calls[i]); + test_bad_arguments_restore_mask(masked_calls[i]); + test_restored_mask_lets_handler_run(masked_calls[i]); + } + test_null_mask_ignores_size(); + test_epoll_pwait_mask(); + printf("All wait restart tests passed.\n"); + return 0; +} diff --git a/litebox_shim_linux/src/guest_buffer.rs b/litebox_shim_linux/src/guest_buffer.rs index b3e4ff8fcc..46ad18ec0c 100644 --- a/litebox_shim_linux/src/guest_buffer.rs +++ b/litebox_shim_linux/src/guest_buffer.rs @@ -234,17 +234,18 @@ mod tests { assert_eq!(raw, [2, 3, 4, 5]); } - // Signal support for Windows is not ready yet. - #[cfg(not(target_os = "windows"))] #[test] fn inaccessible_memory_faults() { let _ = crate::syscalls::tests::init_platform(); - let iov = IoWriteVec { - iov_base: UserPtr::from_usize(0xdead_beef), - iov_len: 4, - }; - let buffer = GuestWriteBuffer::::single(&iov); - assert_eq!(buffer.copy_to_slice_at(0, &mut [0; 4]), Err(Fault)); + // The faulting read is recovered only on a LiteBox-managed thread. + ::run_test_thread(|| { + let iov = IoWriteVec { + iov_base: UserPtr::from_usize(0xdead_beef), + iov_len: 4, + }; + let buffer = GuestWriteBuffer::::single(&iov); + assert_eq!(buffer.copy_to_slice_at(0, &mut [0; 4]), Err(Fault)); + }); } #[test] diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 48b34eebd8..25e598beaa 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -1367,6 +1367,7 @@ impl Task { .ok_or(Errno::EFAULT) }) }), + SyscallRequest::Poll { fds, nfds, timeout } => self.sys_poll(fds, nfds, timeout), SyscallRequest::Ppoll { fds, nfds, diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 99b2136dc9..8fa7ab15c5 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -672,7 +672,7 @@ mod test { Some(&mut rfds), None, None, - Some(core::time::Duration::from_mins(1)), + Some(task.relative_deadline(core::time::Duration::from_mins(1))), ) .expect("pselect failed"); @@ -704,7 +704,7 @@ mod test { Some(&mut rfds), None, None, - Some(core::time::Duration::from_secs(1)), + Some(task.relative_deadline(core::time::Duration::from_secs(1))), ); // Expect pselect to return EBADF @@ -714,4 +714,46 @@ mod test { litebox_common_linux::errno::Errno::EBADF ); } + + #[test] + fn test_pselect_with_bad_arguments_restores_its_mask() { + use litebox::platform::ThreadProvider; + use litebox_common_linux::signal::{SigSet, Signal}; + use litebox_common_linux::{SigSetPack, TimeParam, Timespec}; + + let task = crate::syscalls::tests::init_platform(); + // The faulting read below is recovered only on a LiteBox-managed thread. + TestPlatform::run_test_thread(|| { + let blocked = SigSet::empty().with(Signal::SIGCHLD); + crate::syscalls::tests::sigprocmask( + &task, + litebox_common_linux::signal::SigmaskHow::SIG_BLOCK, + blocked, + ); + let empty = SigSet::empty(); + let sigsetpack = SigSetPack { + sigset: UserPtr::from_ptr(&raw const empty), + size: core::mem::size_of::(), + }; + let mut timeout = Timespec::from(core::time::Duration::from_secs(5)); + // A negative `int` count of fds, or one whose sets cannot be read. + for (nfds, readfds, errno) in [ + (u32::MAX, None, Errno::EINVAL), + (1, Some(UserPtrMut::from_usize(8)), Errno::EFAULT), + ] { + assert_eq!( + task.sys_pselect( + nfds, + readfds, + None, + None, + TimeParam::Timespec64(UserPtrMut::from_ptr(&raw mut timeout)), + Some(UserPtr::from_ptr(&raw const sigsetpack)), + ), + Err(errno) + ); + assert_eq!(task.signals.blocked(), blocked); + } + }); + } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index b1a4e486e3..9c45f97e86 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -37,11 +37,12 @@ use litebox_common_linux::{ signal::Signal, }; use litebox_platform::buffer::{Buffer as _, Fault, ReadBuffer, WriteBuffer}; +use litebox_platform::time::{Instant as _, TimeProvider}; use thiserror::Error; use super::{ - epoll::EpollFd, eventfd::EventfdFd, inet::InetSocketFd, pipe::PipeFd, timerfd::TimerfdFd, - unix::UnixSocketFd, + epoll::EpollFd, eventfd::EventfdFd, inet::InetSocketFd, pipe::PipeFd, process::RestartBlock, + timerfd::TimerfdFd, unix::UnixSocketFd, }; use crate::guest_buffer::{GuestReadBuffer, GuestWriteBuffer}; use crate::{GlobalState, ShimPlatform, Task, UserPtr, UserPtrMut, syscalls::signal}; @@ -2926,11 +2927,24 @@ impl Task { maxevents: u32, timeout: i32, sigmask: Option>, - _sigsetsize: usize, + sigsetsize: usize, + ) -> Result { + self.set_user_signal_mask(sigmask, sigsetsize)?; + let result = self.do_epoll_wait(epfd, events, maxevents, timeout); + // Keep the call's mask if interrupted, so the interrupting signal is delivered under it. + self.restore_signal_mask_unless(result == Err(Errno::EINTR)); + result + } + + /// Waits for at most `maxevents` events on the epoll at `epfd` for `timeout` milliseconds, + /// or until one comes if it is negative, and writes them to `events`, for `epoll_pwait`. + fn do_epoll_wait( + &self, + epfd: i32, + events: UserPtrMut, + maxevents: u32, + timeout: i32, ) -> Result { - if sigmask.is_some() { - todo!("sigmask not supported"); - } let Ok(epfd) = u32::try_from(epfd) else { return Err(Errno::EBADF); }; @@ -2975,6 +2989,37 @@ impl Task { }) } + /// Handle syscall `poll`. + pub(crate) fn sys_poll( + &self, + fds: UserPtrMut, + nfds: usize, + timeout: TimeParam, + ) -> Result { + let deadline = timeout + .read::()? + .map(|timeout| self.relative_deadline(timeout)); + self.poll_until(fds, nfds, deadline) + } + + /// Polls the `nfds` entries at `fds` until `deadline`, or until one is ready if `None`, for + /// `poll`. + pub(super) fn poll_until( + &self, + fds: UserPtrMut, + nfds: usize, + deadline: Option<::Instant>, + ) -> Result { + match self.do_poll(fds, nfds, deadline) { + Err(Errno::ERESTARTNOHAND) => Err(self.thread.save_restart_block(RestartBlock::Poll { + fds, + nfds, + deadline, + })), + result => result, + } + } + /// Handle syscall `ppoll`. pub fn sys_ppoll( &self, @@ -2984,14 +3029,36 @@ impl Task { sigmask: Option>, sigsetsize: usize, ) -> Result { - if sigmask.is_some() { - if sigsetsize != core::mem::size_of::() { - // Expected via ppoll(2) manpage - unimplemented!() - } - unimplemented!("no sigmask support yet"); - } - let timeout = timeout.read::()?; + let timeout = self.read_poll_select_timeout(timeout)?; + self.set_user_signal_mask(sigmask, sigsetsize)?; + let result = self.do_poll(fds, nfds, timeout.deadline); + self.finish_poll_or_select(result, &timeout) + } + + /// Reads the timeout of `ppoll`, `select`, or `pselect` from `param`, noting when. + fn read_poll_select_timeout( + &self, + param: TimeParam, + ) -> Result::Instant>, Errno> { + let read = param + .read::()? + .map(|duration| (duration, self.global.platform.now())); + let deadline = read.map(|(duration, start)| self.relative_deadline_from(start, duration)); + Ok(PollSelectTimeout { + param, + read, + deadline, + }) + } + + /// Polls the `nfds` entries at `fds` until `deadline`, or until one is ready if `None`, and + /// writes back their `revents`, failing with [`Errno::ERESTARTNOHAND`] if interrupted. + fn do_poll( + &self, + fds: UserPtrMut, + nfds: usize, + deadline: Option<::Instant>, + ) -> Result { if nfds > self .process() @@ -3012,27 +3079,21 @@ impl Task { set.add_fd(fd.fd, events); } - match set.wait( + let result = match set.wait( &self.global, - &self.wait_cx().with_timeout(timeout), + &self.wait_cx().with_deadline(deadline), &self.files.borrow(), ) { - Ok(()) => {} - Err(WaitError::Interrupted) => { - // TODO: Like Linux, restart unless a handler runs, rather than failing with - // `EINTR`: `poll` through `restart_syscall`, until its original deadline if it - // has a timeout (`ERESTART_RESTARTBLOCK`, see `RestartBlock`), and `ppoll` by - // reissuing the syscall once it writes the remaining time back - // (`ERESTARTNOHAND`). - return Err(Errno::EINTR); - } + Ok(()) => Ok(()), + Err(WaitError::Interrupted) => Err(Errno::ERESTARTNOHAND), Err(WaitError::TimedOut) => { // A timeout occurred. Scan one last time. set.scan(&self.global, &self.files.borrow()); + Ok(()) } - } + }; - // Write just the revents back. + // Like Linux, write just the revents back, even when interrupted. let fds_base_addr = fds.as_usize(); let mut ready_count = 0; for (i, revents) in set.revents().enumerate() { @@ -3050,7 +3111,33 @@ impl Task { ready_count += 1; } } - Ok(ready_count) + result.map(|()| ready_count) + } + + /// Ends `ppoll`, `select`, or `pselect` with `result` after waiting at most `timeout`, like + /// Linux's `poll_select_finish`. + fn finish_poll_or_select( + &self, + result: Result, + timeout: &PollSelectTimeout<::Instant>, + ) -> Result { + let interrupted = result == Err(Errno::ERESTARTNOHAND); + self.restore_signal_mask_unless(interrupted); + let Some((duration, start)) = timeout.read.filter(|(duration, _)| !duration.is_zero()) + else { + return result; + }; + let elapsed = self + .global + .platform + .now() + .checked_duration_since(&start) + .unwrap_or_default(); + let left = duration.saturating_sub(elapsed); + match timeout.param.write::(left) { + Err(_) if interrupted => Err(Errno::EINTR), + Ok(()) | Err(_) => result, + } } pub(crate) fn do_pselect( @@ -3059,7 +3146,7 @@ impl Task { readfds: Option<&mut bitvec::vec::BitVec>, writefds: Option<&mut bitvec::vec::BitVec>, exceptfds: Option<&mut bitvec::vec::BitVec>, - timeout: Option, + deadline: Option<::Instant>, ) -> Result { // XXX: semantic issue likely should be fixed here to make sure EBADF is triggered early // enough if needed. Previously, `file_table_len` used to be @@ -3088,18 +3175,12 @@ impl Task { match set.wait( &self.global, - &self.wait_cx().with_timeout(timeout), + &self.wait_cx().with_deadline(deadline), &self.files.borrow(), ) { Ok(()) => {} - Err(WaitError::Interrupted) => { - // TODO: Like Linux, write the remaining time back and restart unless a handler - // runs (`ERESTARTNOHAND`), rather than failing with `EINTR`. `pselect` must first - // keep its signal mask until signal delivery, like Linux's - // `restore_saved_sigmask_unless`: restoring it here would block a signal it - // unblocked, so the reissued call would be interrupted again forever. - return Err(Errno::EINTR); - } + // Like Linux, the call is reissued unless a handler runs. + Err(WaitError::Interrupted) => return Err(Errno::ERESTARTNOHAND), Err(WaitError::TimedOut) => { // A timeout occurred. Scan one last time. set.scan(&self.global, &self.files.borrow()); @@ -3140,23 +3221,34 @@ impl Task { timeout: TimeParam, sigsetpack: Option>, ) -> Result { - let sigmask = if let Some(sigsetpack) = sigsetpack { - let sigsetpack = sigsetpack - .read_at_offset::(0) - .ok_or(Errno::EFAULT)?; - if sigsetpack.size != core::mem::size_of::() { - return Err(Errno::EINVAL); - } - Some( - sigsetpack - .sigset + // Like Linux, read where the signal mask is before the timeout, and the mask after it. + let (sigmask, sigsetsize) = match sigsetpack { + Some(sigsetpack) => { + let sigsetpack = sigsetpack .read_at_offset::(0) - .ok_or(Errno::EFAULT)?, - ) - } else { - None + .ok_or(Errno::EFAULT)?; + let sigmask = (!sigsetpack.sigset.is_null()).then_some(sigsetpack.sigset); + (sigmask, sigsetpack.size) + } + None => (None, 0), }; - let timeout = timeout.read::()?; + let timeout = self.read_poll_select_timeout(timeout)?; + self.set_user_signal_mask(sigmask, sigsetsize)?; + let result = self.select_fds(nfds, readfds, writefds, exceptfds, timeout.deadline); + self.finish_poll_or_select(result, &timeout) + } + + /// Waits until `deadline`, or until ready if `None`, for the fds in the `nfds`-bit sets at + /// `readfds`, `writefds`, and `exceptfds`, writing back which are ready, for `select` and + /// `pselect`. + fn select_fds( + &self, + nfds: u32, + readfds: Option>, + writefds: Option>, + exceptfds: Option>, + deadline: Option<::Instant>, + ) -> Result { if nfds >= i32::MAX as u32 || nfds as usize > self @@ -3167,53 +3259,34 @@ impl Task { return Err(Errno::EINVAL); } let len = (nfds as usize).div_ceil(core::mem::size_of::() * 8); - let mut kreadfds = readfds - .map(|fds| fds.to_owned_slice::(len).ok_or(Errno::EFAULT)) - .transpose()? - .map(|fds| bitvec::vec::BitVec::from_vec(fds.into_vec())); - let mut kwritefds = writefds - .map(|fds| fds.to_owned_slice::(len).ok_or(Errno::EFAULT)) - .transpose()? - .map(|fds| bitvec::vec::BitVec::from_vec(fds.into_vec())); - let mut kexceptfds = exceptfds - .map(|fds| fds.to_owned_slice::(len).ok_or(Errno::EFAULT)) - .transpose()? - .map(|fds| bitvec::vec::BitVec::from_vec(fds.into_vec())); - - let mut do_pselect = || { - self.do_pselect( - nfds, - kreadfds.as_mut(), - kwritefds.as_mut(), - kexceptfds.as_mut(), - timeout, - ) + let read_fd_set = |fds: Option>| { + fds.map(|fds| { + fds.to_owned_slice::(len) + .map(|fds| bitvec::vec::BitVec::from_vec(fds.into_vec())) + .ok_or(Errno::EFAULT) + }) + .transpose() }; - let count = if let Some(sigmask) = sigmask { - self.with_temporary_signal_mask(sigmask, do_pselect) - } else { - do_pselect() - }?; - - if let Some(fds) = kreadfds { - readfds - .unwrap() - .write_slice_at_offset::(0, fds.as_raw_slice()) - .ok_or(Errno::EFAULT)?; - } - if let Some(fds) = kwritefds { - writefds - .unwrap() - .write_slice_at_offset::(0, fds.as_raw_slice()) - .ok_or(Errno::EFAULT)?; - } - if let Some(fds) = kexceptfds { - exceptfds - .unwrap() - .write_slice_at_offset::(0, fds.as_raw_slice()) - .ok_or(Errno::EFAULT)?; + let mut kreadfds = read_fd_set(readfds)?; + let mut kwritefds = read_fd_set(writefds)?; + let mut kexceptfds = read_fd_set(exceptfds)?; + let count = self.do_pselect( + nfds, + kreadfds.as_mut(), + kwritefds.as_mut(), + kexceptfds.as_mut(), + deadline, + )?; + for (fds, kfds) in [ + (readfds, kreadfds), + (writefds, kwritefds), + (exceptfds, kexceptfds), + ] { + if let (Some(fds), Some(kfds)) = (fds, kfds) { + fds.write_slice_at_offset::(0, kfds.as_raw_slice()) + .ok_or(Errno::EFAULT)?; + } } - Ok(count) } @@ -3408,6 +3481,17 @@ impl Task { } } +/// The timeout of `ppoll`, `select`, or `pselect`, which, like Linux, they write +/// the time left back to. +struct PollSelectTimeout { + /// Where the timeout is. + param: TimeParam, + /// The timeout read from `param`, if any, and when it was read. + read: Option<(core::time::Duration, Instant)>, + /// When the wait ends, or `None` if it waits until an fd is ready. + deadline: Option, +} + #[cfg(test)] mod tests { use super::*; diff --git a/litebox_shim_linux/src/syscalls/futex.rs b/litebox_shim_linux/src/syscalls/futex.rs index 2178dc0de5..4b750dc149 100644 --- a/litebox_shim_linux/src/syscalls/futex.rs +++ b/litebox_shim_linux/src/syscalls/futex.rs @@ -11,14 +11,16 @@ use litebox_broker_protocol::shared_futex::{SharedFutexKey, SharedMemoryId}; use litebox_common_linux::errno::Errno; use litebox_common_linux::{ClockId, FutexArgs, FutexFlags}; -use crate::wait::wait_errno; +use litebox_platform::time::TimeProvider; + +use crate::syscalls::process::RestartBlock; use crate::{ShimPlatform, Task, UserPtrMut}; impl Task { /// Handle syscall `futex` pub(crate) fn sys_futex(&self, arg: FutexArgs) -> Result { let res = match arg { - FutexArgs::Wake { addr, flags, count } => self.futex_wake(addr, &flags, count, None)?, + FutexArgs::Wake { addr, flags, count } => self.futex_wake(addr, flags, count, None)?, FutexArgs::WakeBitset { addr, flags, @@ -27,7 +29,7 @@ impl Task { } => { // Like Linux, an empty mask fails even when there is nothing to wake. let bitmask = NonZeroU32::new(bitmask).ok_or(Errno::EINVAL)?; - self.futex_wake(addr, &flags, count, Some(bitmask))? + self.futex_wake(addr, flags, count, Some(bitmask))? } FutexArgs::Wait { addr, @@ -35,18 +37,11 @@ impl Task { val, timeout, } => { - let timeout = timeout.read::()?; - self.futex_wait( - &self.wait_cx().with_timeout(timeout), - addr, - &flags, - val, - None, - ) - // TODO: Like Linux, resume a wait with a timeout through `restart_syscall` until - // its original deadline unless a handler runs (`ERESTART_RESTARTBLOCK`, see - // `RestartBlock`), rather than failing with `EINTR`. Also below. - .map_err(|error| wait_errno(timeout, error))?; + // Like Linux, a relative timeout ends at a deadline, which a resumed wait keeps. + let deadline = timeout + .read::()? + .map(|timeout| self.relative_deadline(timeout)); + self.futex_wait_for(addr, flags, val, None, deadline)?; 0 } FutexArgs::WaitBitset { @@ -56,27 +51,20 @@ impl Task { timeout, bitmask, } => { - let timeout = timeout.read::()?; - let deadline = if let Some(timeout) = timeout { - let clock_id = if flags.contains(FutexFlags::CLOCK_REALTIME) { - ClockId::RealTime - } else { - ClockId::Monotonic - }; - self.duration_since_epoch_to_deadline(clock_id, timeout)? - } else { - None - }; + let deadline = timeout + .read::()? + .map(|timeout| { + let clock_id = if flags.contains(FutexFlags::CLOCK_REALTIME) { + ClockId::RealTime + } else { + ClockId::Monotonic + }; + self.duration_since_epoch_to_deadline(clock_id, timeout) + }) + .transpose()?; // Like Linux, an empty mask fails rather than waiting for a wake that never comes. let bitmask = NonZeroU32::new(bitmask).ok_or(Errno::EINVAL)?; - self.futex_wait( - &self.wait_cx().with_deadline(deadline), - addr, - &flags, - val, - Some(bitmask), - ) - .map_err(|error| wait_errno(timeout, error))?; + self.futex_wait_for(addr, flags, val, Some(bitmask), deadline)?; 0 } _ => unimplemented!("Unsupported futex operation"), @@ -107,7 +95,7 @@ impl Task { fn shared_futex_key_for( &self, addr: UserPtrMut, - flags: &FutexFlags, + flags: FutexFlags, ) -> Result, Errno> { if !addr.as_usize().is_multiple_of(align_of::()) { return Err(Errno::EINVAL); @@ -126,7 +114,7 @@ impl Task { fn futex_wake( &self, addr: UserPtrMut, - flags: &FutexFlags, + flags: FutexFlags, count: u32, bitset: Option, ) -> Result { @@ -149,7 +137,7 @@ impl Task { &self, cx: &WaitContext<'_, Platform>, addr: UserPtrMut, - flags: &FutexFlags, + flags: FutexFlags, val: u32, bitset: Option, ) -> Result<(), Errno> { @@ -163,6 +151,52 @@ impl Task { Ok(()) } + /// Waits like [`Self::futex_wait`] until `deadline`, or until woken if `None`. + fn futex_wait_for( + &self, + addr: UserPtrMut, + flags: FutexFlags, + val: u32, + bitset: Option, + deadline: Option<::Instant>, + ) -> Result<(), Errno> { + match deadline { + None => self.futex_wait(&self.wait_cx(), addr, flags, val, bitset), + Some(deadline) => self.futex_wait_until(addr, flags, val, bitset, deadline), + } + } + + /// Waits like [`Self::futex_wait`] until `deadline`, for a wait with a timeout. + /// + /// Like Linux, an interrupted wait resumes through `restart_syscall` until `deadline` unless + /// a handler runs, rather than being reissued with its whole timeout, so a handler interrupts + /// it even with `SA_RESTART`. + pub(super) fn futex_wait_until( + &self, + addr: UserPtrMut, + flags: FutexFlags, + val: u32, + bitset: Option, + deadline: ::Instant, + ) -> Result<(), Errno> { + match self.futex_wait( + &self.wait_cx().with_deadline(deadline), + addr, + flags, + val, + bitset, + ) { + Err(Errno::ERESTARTSYS) => Err(self.thread.save_restart_block(RestartBlock::Futex { + addr, + flags, + val, + bitset, + deadline, + })), + result => result, + } + } + /// Returns this thread's waiter for futexes in shared memory, creating it in the broker on /// first use. fn get_or_create_shared_futex_waiter(&self) -> Result<&SharedFutexWaiter, Errno> { @@ -581,4 +615,43 @@ mod tests { // succeeds if a wake selected it. assert_eq!(waiter.join().unwrap(), woken); } + + #[test] + fn test_interrupted_futex_wait_with_timeout_resumes_through_restart_syscall() { + use litebox::platform::ThreadProvider as _; + + let task = init_platform(); + TestPlatform::run_test_thread(|| { + let address = map(&task, 1, MapFlags::MAP_PRIVATE).as_usize(); + let discard_sigchld = + || task.process_signals(&mut litebox_common_linux::PtRegs::default(), None); + + crate::syscalls::tests::queue_ignored_sigchld(&task); + assert_eq!( + wait(&task, address, 0, FutexFlags::PRIVATE, None), + Err(Errno::ERESTARTSYS) + ); + discard_sigchld(); + assert_eq!(task.sys_restart_syscall(), Err(Errno::EINTR)); + + crate::syscalls::tests::queue_ignored_sigchld(&task); + assert_eq!( + wait(&task, address, 0, FutexFlags::PRIVATE, Some(3_600_000)), + Err(Errno::ERESTART_RESTARTBLOCK) + ); + discard_sigchld(); + // Interrupted again, the resumed wait saves itself again. + crate::syscalls::tests::queue_ignored_sigchld(&task); + assert_eq!( + task.sys_restart_syscall(), + Err(Errno::ERESTART_RESTARTBLOCK) + ); + discard_sigchld(); + + // Once the word changes, the resumed wait ends, leaving nothing to resume. + word(address).write_at_offset::(0, 1).unwrap(); + assert_eq!(task.sys_restart_syscall(), Err(Errno::EAGAIN)); + assert_eq!(task.sys_restart_syscall(), Err(Errno::EINTR)); + }); + } } diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index a1aa294c44..e27b4141cd 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -980,15 +980,15 @@ mod tests { task.sys_munmap(addr, 0x2000).unwrap(); } - // Signal support for Windows is not ready yet. - #[cfg(not(target_os = "windows"))] #[test] fn test_fallible_read() { let _ = init_platform(); - - let ptr = UserPtrMut::::from_usize(0xdeadbeef); - let result = ptr.read_at_offset::(0); - assert!(result.is_none()); + // The faulting read is recovered only on a LiteBox-managed thread. + ::run_test_thread(|| { + let ptr = UserPtrMut::::from_usize(0xdeadbeef); + let result = ptr.read_at_offset::(0); + assert!(result.is_none()); + }); } #[test] diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index a13483137b..788dac7468 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -241,16 +241,26 @@ unsafe impl Send for ThreadState {} /// The state of a syscall interrupted with [`Errno::ERESTART_RESTARTBLOCK`], which /// `restart_syscall` resumes. #[derive(Clone, Copy)] -enum RestartBlock { - /// A relative `clock_nanosleep` that sleeps until `deadline`, or until interrupted if - /// `None`, and reports the time left to `remain`. +pub(super) enum RestartBlock { Nanosleep { - deadline: Option, + deadline: Instant, remain: TimeParam, }, + Futex { + addr: UserPtrMut, + flags: litebox_common_linux::FutexFlags, + val: u32, + bitset: Option, + deadline: Instant, + }, + Poll { + fds: UserPtrMut, + nfds: usize, + deadline: Option, + }, } -/// The latest time since boot a sleep can last until, like Linux's `KTIME_MAX`. +/// The latest time since boot a sleep or wait can last until, like Linux's `KTIME_MAX`. const MAX_SLEEP_SINCE_BOOT: Duration = Duration::from_nanos(i64::MAX.cast_unsigned()); impl ThreadState { @@ -317,6 +327,16 @@ impl ThreadState { self.clear_child_tid.set(user_ptr(clear_child_tid)); } + /// Saves what `restart_syscall` resumes, returning [`Errno::ERESTART_RESTARTBLOCK`] for the + /// interrupted syscall to fail with, like Linux's `set_restart_fn`. + pub(super) fn save_restart_block( + &self, + block: RestartBlock<::Instant>, + ) -> Errno { + self.restart_block.set(Some(block)); + Errno::ERESTART_RESTARTBLOCK + } + /// Discards what `restart_syscall` would resume. pub(crate) fn discard_restart_block(&self) { self.restart_block.set(None); @@ -2235,32 +2255,31 @@ impl Task { /// Convert an absolute time, specified as a duration since the epoch of the /// given clock, to a `Platform::Instant` suitable for use as a deadline. /// - /// If the time is so far in the future that it cannot be represented as an - /// `Instant`, returns `Ok(None)`. If the time occurs in the past, returns - /// the current time. + /// Like Linux, the deadline is no later than [`MAX_SLEEP_SINCE_BOOT`]. If + /// the time occurs in the past, returns the current time. pub(super) fn duration_since_epoch_to_deadline( &self, clock_id: litebox_common_linux::ClockId, duration: Duration, - ) -> Result::Instant>, Errno> { - match clock_id { + ) -> Result<::Instant, Errno> { + let deadline = match clock_id { litebox_common_linux::ClockId::Monotonic | litebox_common_linux::ClockId::MonotonicCoarse => { // No need to compute the current time since the offset from the // request to `Instant` is known. - Ok(self.global.boot_time.checked_add(duration)) + self.global.boot_time.saturating_add(duration) } _ => { // Convert between time domains. If the requested time is in the past, // return the current time. let current_time = self.gettime_as_duration(clock_id)?; - Ok(self - .global + self.global .platform .now() - .checked_add(duration.checked_sub(current_time).unwrap_or(Duration::ZERO))) + .saturating_add(duration.saturating_sub(current_time)) } - } + }; + Ok(self.cap_at_max_sleep_since_boot(deadline)) } /// Handle syscall `clock_getres`. @@ -2313,66 +2332,86 @@ impl Task { }; } // Relative. Treat all clocks the same. TODO: handle the different clocks differently. - self.nanosleep_until(self.relative_sleep_deadline(request), remain) + self.nanosleep_until(self.relative_deadline(request), remain) } - /// Returns when a relative sleep of `duration` from now ends, which, like Linux, is no - /// later than [`MAX_SLEEP_SINCE_BOOT`], or `None` if the platform cannot represent it. - fn relative_sleep_deadline( + /// Returns when a sleep or wait of `duration` from now ends, which, like Linux, is no later + /// than [`MAX_SLEEP_SINCE_BOOT`]. + pub(super) fn relative_deadline( &self, duration: Duration, - ) -> Option<::Instant> { - let deadline = self.global.platform.now().checked_add(duration); - let latest = self.global.boot_time.checked_add(MAX_SLEEP_SINCE_BOOT); - match (deadline, latest) { - (Some(deadline), Some(latest)) => Some(deadline.min(latest)), - (deadline, latest) => deadline.or(latest), - } + ) -> ::Instant { + self.relative_deadline_from(self.global.platform.now(), duration) + } + + /// Returns when a sleep or wait of `duration` from `start` ends, like + /// [`Self::relative_deadline`]. + pub(super) fn relative_deadline_from( + &self, + start: ::Instant, + duration: Duration, + ) -> ::Instant { + self.cap_at_max_sleep_since_boot(start.saturating_add(duration)) } - /// Sleeps until `deadline`, or until interrupted if `None`, for a relative - /// `clock_nanosleep`. + /// Returns `deadline`, or the instant [`MAX_SLEEP_SINCE_BOOT`] after boot if that is earlier. + fn cap_at_max_sleep_since_boot( + &self, + deadline: ::Instant, + ) -> ::Instant { + deadline.min(self.global.boot_time.saturating_add(MAX_SLEEP_SINCE_BOOT)) + } + + /// Sleeps until `deadline`, for a relative `clock_nanosleep`. /// /// Like Linux, an interrupted sleep reports the time left to `remain`, then resumes through /// `restart_syscall` until `deadline` unless a handler runs. Restarting the syscall instead /// would sleep its whole duration again. fn nanosleep_until( &self, - deadline: Option<::Instant>, + deadline: ::Instant, remain: TimeParam, ) -> Result<(), Errno> { let wait_cx = self.wait_cx().with_deadline(deadline); match wait_cx.sleep() { WaitError::TimedOut => Ok(()), WaitError::Interrupted => { - // Without a representable deadline, there is no time left to report. - if deadline.is_some() { - let Some(remaining_timeout) = wait_cx.remaining_timeout() else { - // Time ran out after getting interrupted. Treat this as a timeout. - return Ok(()); - }; - remain.write::(remaining_timeout)?; - } - self.thread - .restart_block - .set(Some(RestartBlock::Nanosleep { deadline, remain })); - Err(Errno::ERESTART_RESTARTBLOCK) + let Some(remaining_timeout) = wait_cx.remaining_timeout() else { + // Time ran out after getting interrupted. Treat this as a timeout. + return Ok(()); + }; + remain.write::(remaining_timeout)?; + Err(self + .thread + .save_restart_block(RestartBlock::Nanosleep { deadline, remain })) } } } /// Handle syscall `restart_syscall`, which resumes the syscall a signal interrupted with /// [`Errno::ERESTART_RESTARTBLOCK`]. - /// - /// Like Linux, this thread's saved state stays until its next sleep or signal handler - /// return, so resuming a sleep that already finished returns at once, and new threads start - /// with a copy. Unlike Linux, a process duplicated by `fork`, or started by `execve` in a - /// fresh runner, starts with nothing to resume. - pub(crate) fn sys_restart_syscall(&self) -> Result<(), Errno> { + pub(crate) fn sys_restart_syscall(&self) -> Result { match self.thread.restart_block.get() { Some(RestartBlock::Nanosleep { deadline, remain }) => { - self.nanosleep_until(deadline, remain) + self.nanosleep_until(deadline, remain).map(|()| 0) + } + Some(RestartBlock::Futex { + addr, + flags, + val, + bitset, + deadline, + }) => { + // Like Linux, a resumed futex wait leaves nothing to resume once it ends. + self.thread.discard_restart_block(); + self.futex_wait_until(addr, flags, val, bitset, deadline) + .map(|()| 0) } + Some(RestartBlock::Poll { + fds, + nfds, + deadline, + }) => self.poll_until(fds, nfds, deadline), // Like Linux, nothing is saved unless an interrupted syscall saved its state. None => Err(Errno::EINTR), } @@ -4350,28 +4389,6 @@ mod tests { return regs.regs[8]; } - /// Makes SIGCHLD pending for `task`. SIGCHLD is ignored by default, but is queued while - /// blocked, so once unblocked it interrupts waits, yet runs no handler. - fn queue_ignored_sigchld(task: &crate::Task) { - use litebox_common_linux::signal::{SigSet, SigmaskHow, Signal}; - - let set = SigSet::empty().with(Signal::SIGCHLD); - let sigprocmask = |how| { - task.sys_rt_sigprocmask( - how, - Some(UserPtr::from_ptr(&raw const set)), - None, - core::mem::size_of::(), - ) - .expect("rt_sigprocmask failed"); - }; - sigprocmask(SigmaskHow::SIG_BLOCK); - task.sys_tkill(task.tid(), Signal::SIGCHLD.as_i32()) - .expect("tkill failed"); - sigprocmask(SigmaskHow::SIG_UNBLOCK); - assert!(task.has_pending_signals(), "SIGCHLD should be pending"); - } - /// Returns the registers of a syscall that restarts. The test platforms enter syscalls through /// the rewriter's trampolines: x86-64's 6-byte `JMP [RIP + disp32]` or AArch64's 4-byte `B`. fn restarted_state() -> (usize, usize) { @@ -4403,7 +4420,7 @@ mod tests { Some(SyscallRestart::NoIntr), Some(SyscallRestart::NoHandler), ] { - queue_ignored_sigchld(&task); + crate::syscalls::tests::queue_ignored_sigchld(&task); let mut regs = interrupted_syscall_regs(&mut stack); task.process_signals(&mut regs, restart); @@ -4435,13 +4452,13 @@ mod tests { else { panic!("no sleep saved for restart_syscall"); }; - (deadline.expect("the sleep should end"), remain) + (deadline, remain) }; // Nothing to resume yet. assert_eq!(task.sys_restart_syscall(), Err(Errno::EINTR)); - queue_ignored_sigchld(&task); + crate::syscalls::tests::queue_ignored_sigchld(&task); let mut request = Timespec::from(SLEEP); let mut remain = Timespec { tv_sec: 0, @@ -4478,7 +4495,7 @@ mod tests { // Interrupted again, the resumed sleep keeps when it ends, rather than sleeping its // whole duration again, and reports less time left. - queue_ignored_sigchld(&task); + crate::syscalls::tests::queue_ignored_sigchld(&task); assert_eq!( task.sys_restart_syscall(), Err(Errno::ERESTART_RESTARTBLOCK) @@ -4493,11 +4510,11 @@ mod tests { task.thread .restart_block .set(Some(super::RestartBlock::Nanosleep { - deadline: Some(before), + deadline: before, remain: saved_remain, })); - assert_eq!(task.sys_restart_syscall(), Ok(())); - assert_eq!(task.sys_restart_syscall(), Ok(())); + assert_eq!(task.sys_restart_syscall(), Ok(0)); + assert_eq!(task.sys_restart_syscall(), Ok(0)); // A new sleep leaves nothing to resume. let mut request = Timespec::from(Duration::from_micros(1)); @@ -4512,6 +4529,108 @@ mod tests { }); } + #[test] + fn test_signal_unblocked_by_restored_mask_is_delivered_before_reissue() { + use crate::syscalls::signal::SyscallRestart; + use litebox_common_linux::errno::Errno; + use litebox_common_linux::signal::{SaFlags, SigAction, SigSet, Signal}; + + const HANDLER: usize = 0x1000; + + let task = crate::syscalls::tests::init_platform(); + ::run_test_thread(|| { + let act = SigAction { + sigaction: HANDLER, + flags: SaFlags::RESTORER, + #[cfg(target_pointer_width = "64")] + __pad: 0, + restorer: 0, + mask: SigSet::empty(), + }; + task.sys_rt_sigaction( + Signal::SIGUSR2, + Some(UserPtr::from_ptr(&raw const act)), + None, + core::mem::size_of::(), + ) + .expect("rt_sigaction failed"); + // SIGCHLD, ignored by default, stays pending while blocked. SIGUSR2 is pending + // unblocked, as if it came during the call. + let original = SigSet::empty().with(Signal::SIGCHLD); + crate::syscalls::tests::block_and_raise(&task, Signal::SIGCHLD); + task.sys_tkill(task.tid(), Signal::SIGUSR2.as_i32()) + .expect("tkill failed"); + + // The call's mask unblocks SIGCHLD, which interrupts the call, and blocks SIGUSR2. + let call_mask = SigSet::empty().with(Signal::SIGUSR2); + let sigsetpack = litebox_common_linux::SigSetPack { + sigset: UserPtr::from_ptr(&raw const call_mask), + size: core::mem::size_of::(), + }; + let (idle, _writer) = task + .sys_pipe2(litebox_common_linux::OFlags::empty()) + .expect("pipe2 failed"); + let mut readfds = [1usize << idle]; + let mut timeout = litebox_common_linux::Timespec::from(Duration::from_secs(3600)); + assert_eq!( + task.sys_pselect( + idle + 1, + Some(UserPtrMut::from_ptr(readfds.as_mut_ptr())), + None, + None, + litebox_common_linux::TimeParam::Timespec64(UserPtrMut::from_ptr( + &raw mut timeout + )), + Some(UserPtr::from_ptr(&raw const sigsetpack)), + ), + Err(Errno::ERESTARTNOHAND) + ); + assert_eq!( + task.signals.blocked(), + call_mask, + "the call's mask should stay until its signals are processed" + ); + + // SIGCHLD is discarded, then SIGUSR2's handler runs, its frame resuming the reissued + // call. + let mut stack = [0u8; 2 * litebox_common_linux::PAGE_SIZE]; + let mut regs = interrupted_syscall_regs(&mut stack); + task.process_signals(&mut regs, Some(SyscallRestart::NoHandler)); + assert!(!task.has_pending_signals(), "SIGUSR2 should be delivered"); + assert_eq!(regs.get_ip(), HANDLER); + assert_eq!( + task.signals.blocked(), + original.with(Signal::SIGUSR2), + "the handler should run with the restored mask" + ); + }); + } + + #[test] + fn test_absolute_deadline_is_at_most_max_sleep_since_boot() { + use super::MAX_SLEEP_SINCE_BOOT; + use litebox_common_linux::ClockId; + use litebox_platform::time::Instant as _; + + let task = crate::syscalls::tests::init_platform(); + let latest = task + .global + .boot_time + .checked_add(MAX_SLEEP_SINCE_BOOT) + .expect("the platform can represent the latest deadline"); + for duration in [Duration::from_secs(i64::MAX.cast_unsigned()), Duration::MAX] { + for (name, clock_id) in [ + ("monotonic", ClockId::Monotonic), + ("realtime", ClockId::RealTime), + ] { + assert!( + task.duration_since_epoch_to_deadline(clock_id, duration) == Ok(latest), + "{name} {duration:?}" + ); + } + } + } + /// Like Linux, a relative sleep too long to end ends at [`MAX_SLEEP_SINCE_BOOT`], so an /// interruption still reports the time left, and so does interrupting it again once resumed /// (see the `nanosleep_restart.c` runner test). @@ -4532,7 +4651,7 @@ mod tests { tv_sec: i64::MAX, tv_nsec: 999_999_999, }; - queue_ignored_sigchld(&task); + crate::syscalls::tests::queue_ignored_sigchld(&task); let result = task.sys_clock_nanosleep( ClockId::Monotonic, TimerFlags::empty(), @@ -4555,7 +4674,7 @@ mod tests { tv_sec: 0, tv_nsec: 0, }; - queue_ignored_sigchld(&task); + crate::syscalls::tests::queue_ignored_sigchld(&task); assert_eq!(task.sys_restart_syscall(), Err(Errno::ERESTART_RESTARTBLOCK)); let second = left(remain); assert!((earliest..=first).contains(&second), "{second:?}"); diff --git a/litebox_shim_linux/src/syscalls/signal/aarch64.rs b/litebox_shim_linux/src/syscalls/signal/aarch64.rs index 9d9e43d3f5..5407b343d3 100644 --- a/litebox_shim_linux/src/syscalls/signal/aarch64.rs +++ b/litebox_shim_linux/src/syscalls/signal/aarch64.rs @@ -170,7 +170,7 @@ impl SignalState { flags: 0, link: 0, // core::ptr::null_mut(), stack: self.altstack.get(), - sigmask: self.blocked.get(), + sigmask: self.mask_to_save(), __unused: [0; _], __align_pad: [0; _], mcontext: Sigcontext { diff --git a/litebox_shim_linux/src/syscalls/signal/mod.rs b/litebox_shim_linux/src/syscalls/signal/mod.rs index f60548cbb9..4454716d9a 100644 --- a/litebox_shim_linux/src/syscalls/signal/mod.rs +++ b/litebox_shim_linux/src/syscalls/signal/mod.rs @@ -47,6 +47,9 @@ pub(crate) struct SignalState { altstack: Cell, /// The last exception info recorded for signal delivery. last_exception: Cell, + /// The mask a syscall's temporary mask replaced, until signal processing restores it or a + /// handler's frame saves it. + saved_sigmask: Cell>, } impl SignalState { @@ -74,6 +77,7 @@ impl SignalState { esr: 0, kernel_mode: false, }), + saved_sigmask: Cell::new(None), } } @@ -97,6 +101,7 @@ impl SignalState { .into(), // Preserve last exception last_exception: self.last_exception.clone(), + saved_sigmask: Cell::new(None), } } @@ -218,6 +223,9 @@ impl SignalState { } = parent; self.pending.replace(pending); self.blocked.set(blocked); + // The child cannot make the syscalls that set a temporary mask, but in case, none of its + // state outlives the window. The suspended parent has no temporary mask of its own. + self.saved_sigmask.set(None); self.handlers.replace(handlers); self.altstack.set(altstack); self.last_exception.set(last_exception); @@ -478,6 +486,14 @@ pub(crate) fn siginfo_kill_from(signal: Signal, pid: i32, uid: u32) -> Siginfo { } impl SignalState { + /// Returns the mask a handler's frame saves for its return to restore: the mask a syscall + /// replaced while it waited, if any, or else the current one. + fn mask_to_save(&self) -> SigSet { + self.saved_sigmask + .get() + .unwrap_or_else(|| self.blocked.get()) + } + /// Updates the blocked signal mask. Like Linux, SIGKILL and SIGSTOP are never blocked. fn set_signal_mask(&self, mut mask: SigSet) { mask.remove(Signal::SIGKILL); @@ -567,6 +583,9 @@ impl SignalState { ctx, sigreturn_trampoline, )?; + // The frame saved the mask a syscall replaced, which the handler's return restores, while + // the handler, like Linux, runs with the syscall's mask. + self.saved_sigmask.set(None); let mut mask = self.blocked.get() | action.mask; if !action.flags.contains(SaFlags::NODEFER) { @@ -639,12 +658,38 @@ fn syscall_instruction_len(platform: &impl litebox::platform::SystemInfoProvider } impl Task { - pub(crate) fn with_temporary_signal_mask(&self, mask: SigSet, f: impl FnOnce() -> R) -> R { - let old = self.signals.blocked.get(); + /// Blocks the mask at `mask`, if any, for the rest of a syscall, like Linux's + /// `set_user_sigmask`. The syscall must end with [`Self::restore_signal_mask_unless`]. + pub(super) fn set_user_signal_mask( + &self, + mask: Option>, + size: usize, + ) -> Result<(), Errno> { + let Some(mask) = mask else { + return Ok(()); + }; + if size != core::mem::size_of::() { + return Err(Errno::EINVAL); + } + let mask = mask.read_at_offset::(0).ok_or(Errno::EFAULT)?; + self.signals + .saved_sigmask + .set(Some(self.signals.blocked.get())); self.signals.set_signal_mask(mask); - let result = f(); - self.signals.set_signal_mask(old); - result + Ok(()) + } + + /// Ends a syscall that may have set a temporary mask, restoring the mask it replaced unless + /// `interrupted`, like Linux's `restore_saved_sigmask_unless`. + /// + /// An interrupted syscall keeps its mask until the signals that interrupted it are processed, + /// so that a signal the mask unblocked is delivered, saving the replaced mask in its handler's + /// frame; otherwise the replaced mask is restored then. Restoring it now would block the + /// signal again, so a reissued syscall would be interrupted again forever. + pub(super) fn restore_signal_mask_unless(&self, interrupted: bool) { + if !interrupted && let Some(mask) = self.signals.saved_sigmask.take() { + self.signals.set_signal_mask(mask); + } } pub(crate) fn sys_rt_sigprocmask( @@ -934,22 +979,33 @@ impl Task { pub(crate) fn process_signals(&self, ctx: &mut PtRegs, mut restart: Option) { loop { let blocked = self.signals.blocked.get(); - let (signal, siginfo) = { + let next = { let mut pending = self.signals.pending.borrow_mut(); if let Some(signal) = pending.next(blocked) { - (signal, pending.remove(signal)) + Some((signal, pending.remove(signal))) } else if self.vfork.borrow().is_some() { // Process-directed signals belong to the suspended vfork parent. - break; + None } else { // Then try shared pending. let mut shared = self.signals.shared_pending.lock(); - if let Some(signal) = shared.next(blocked) { - (signal, shared.remove(signal)) - } else { - break; - } + shared + .next(blocked) + .map(|signal| (signal, shared.remove(signal))) + } + }; + let Some((signal, siginfo)) = next else { + // Like Linux, once no handler takes the mask an interrupted syscall replaced, + // resolve the syscall and restore the mask, then take the signals it unblocks + // before the guest runs, and so before a reissued syscall waits again. + let Some(mask) = self.signals.saved_sigmask.take() else { + break; + }; + if let Some(restart) = restart.take() { + self.reissue_syscall(ctx, restart); } + self.signals.set_signal_mask(mask); + continue; }; if self.is_exiting() { // Don't deliver any more signals if exiting. diff --git a/litebox_shim_linux/src/syscalls/signal/x86_64.rs b/litebox_shim_linux/src/syscalls/signal/x86_64.rs index a5d45172ab..7c06e12f38 100644 --- a/litebox_shim_linux/src/syscalls/signal/x86_64.rs +++ b/litebox_shim_linux/src/syscalls/signal/x86_64.rs @@ -270,12 +270,12 @@ impl SignalState { ss: ctx.ss.trunc(), err: last_exception.error_code.into(), trapno: last_exception.exception.0.into(), - oldmask: self.blocked.get().as_u64(), + oldmask: self.mask_to_save().as_u64(), cr2: last_exception.cr2 as u64, fpstate: fpstate as u64, reserved1: [0; 8], }, - sigmask: self.blocked.get(), + sigmask: self.mask_to_save(), }, siginfo: siginfo.clone(), }; diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 9b14c0f333..c372a4119f 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -57,6 +57,44 @@ pub(crate) fn init_platform() -> crate::Task { shim_builder.build().0.new_test_task() } +/// Blocks `signal` for `task`, then sends it to `task`, so it stays pending. +pub(crate) fn block_and_raise(task: &crate::Task, signal: Signal) { + sigprocmask( + task, + litebox_common_linux::signal::SigmaskHow::SIG_BLOCK, + litebox_common_linux::signal::SigSet::empty().with(signal), + ); + task.sys_tkill(task.tid(), signal.as_i32()) + .expect("tkill failed"); +} + +/// Changes `task`'s signal mask with `set` as `how` says. +pub(crate) fn sigprocmask( + task: &crate::Task, + how: litebox_common_linux::signal::SigmaskHow, + set: litebox_common_linux::signal::SigSet, +) { + task.sys_rt_sigprocmask( + how, + Some(UserPtr::from_ptr(&raw const set)), + None, + core::mem::size_of::(), + ) + .expect("rt_sigprocmask failed"); +} + +/// Makes SIGCHLD pending for `task`. SIGCHLD is ignored by default, but is queued while +/// blocked, so once unblocked it interrupts waits, yet runs no handler. +pub(crate) fn queue_ignored_sigchld(task: &crate::Task) { + block_and_raise(task, Signal::SIGCHLD); + sigprocmask( + task, + litebox_common_linux::signal::SigmaskHow::SIG_UNBLOCK, + litebox_common_linux::signal::SigSet::empty().with(Signal::SIGCHLD), + ); + assert!(task.has_pending_signals(), "SIGCHLD should be pending"); +} + pub(crate) fn create_directory(task: &crate::Task, path: &str) { task.sys_mkdirat(litebox_common_linux::AT_FDCWD, path, 0o777) .expect("the test directory must be created");