From bf8452c3ba5cb8273d8654bb8212576aa8385edf Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 29 Oct 2026 23:19:55 +0000 Subject: [PATCH 1/6] Resume interrupted futex waits, polls, and selects like Linux Like Linux, a signal that runs no handler no longer makes blocking waits fail with EINTR: - A futex wait with a timeout and poll resume through restart_syscall (ERESTART_RESTARTBLOCK) until their original deadline, the futex wait checking its word again and poll reading its fds again. - ppoll, select, and pselect6 write back the time left on every return, including argument errors, and are reissued (ERESTARTNOHAND). An interrupted call that cannot write the time back fails with EINTR. - ppoll, pselect6, and epoll_pwait keep their temporary signal mask until the signals that interrupt them are delivered (saved_sigmask, like Linux's TIF_RESTORE_SIGMASK), so a signal the mask unblocks reaches its handler, whose frame saves the caller's mask. ppoll and epoll_pwait now honor their mask, and a NULL mask is ignored whatever its size, which glibc's pselect(..., NULL) relies on. FutexManager::wait no longer loses a wake that takes the waiter as its wait times out or is interrupted: like Linux, the waiter then reports the wake. Instant gains a required saturating_add, so deadlines are always representable, and absolute deadlines are capped at KTIME_MAX since boot like relative ones. The Linux-userland broker's Instant now reads CLOCK_MONOTONIC as a Duration. The new wait_restart.c runner test, validated natively, checks these behaviors deterministically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/platform/mock.rs | 7 + litebox/src/sync/futex.rs | 63 +- litebox/src/utilities/loan_list.rs | 1 - .../Cargo.toml | 2 +- .../src/time.rs | 12 +- litebox_common_linux/src/lib.rs | 11 +- litebox_platform/src/time.rs | 5 + litebox_platform_linux_kernel/src/lib.rs | 7 + litebox_platform_linux_userland/src/lib.rs | 5 + litebox_platform_lvbs/src/lib.rs | 8 + litebox_platform_macos_userland/src/lib.rs | 5 + litebox_platform_vm_kernel/src/lib.rs | 5 + litebox_platform_vm_userland/src/lib.rs | 5 + litebox_platform_windows_userland/src/lib.rs | 5 + litebox_runner_linux_userland/tests/helpers.h | 26 + .../tests/nanosleep_restart.c | 47 +- .../tests/wait_restart.c | 814 ++++++++++++++++++ litebox_shim_linux/src/lib.rs | 1 + litebox_shim_linux/src/syscalls/epoll.rs | 297 ++++++- litebox_shim_linux/src/syscalls/file.rs | 294 ++++--- litebox_shim_linux/src/syscalls/futex.rs | 177 +++- litebox_shim_linux/src/syscalls/process.rs | 271 ++++-- .../src/syscalls/signal/aarch64.rs | 2 +- litebox_shim_linux/src/syscalls/signal/mod.rs | 88 +- .../src/syscalls/signal/x86_64.rs | 4 +- litebox_shim_linux/src/syscalls/tests.rs | 66 ++ 26 files changed, 1941 insertions(+), 287 deletions(-) create mode 100644 litebox_runner_linux_userland/tests/wait_restart.c 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..f40854e0d0 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,47 @@ mod tests { assert!((1..=3).contains(&woken)); } + + /// A wake that takes the waiter as its wait is interrupted wakes it, rather than being lost. + #[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..b23d92325d --- /dev/null +++ b/litebox_runner_linux_userland/tests/wait_restart.c @@ -0,0 +1,814 @@ +// 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 timeout of futex, relative even for an absolute wait, and of 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 += w->ts.tv_sec; + 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); +} + +// Even with a timeout longer than it can wait, a futex wait is interrupted by a handler with +// SA_RESTART, which restarts only a wait without a timeout. +static void test_futex_wait_with_huge_timeout_and_restarting_handler(enum call call) { + set_handler(SIGUSR1, handle_signal, SA_RESTART); + static struct waiter w; + init_waiter(&w, call); + w.ts = (struct timespec){.tv_sec = 1000L * 1000 * 1000 * 1000, .tv_nsec = 0}; + launch_waiter(&w); + wait_done(&w, SIGUSR1); + finish_waiter(&w); + set_handler(SIGUSR1, SIG_DFL, 0); + expect_result(&w, -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. +static void test_wait_with_restarting_handler(enum call call) { + set_handler(SIGUSR1, handle_signal, SA_RESTART); + static struct waiter w; + init_waiter(&w, call); + w.fds[0].revents = REVENTS_UNWRITTEN; + 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"); + } + if (call != POLL) { + expect_time_left(&w); + } +} + +// 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); + } +} + +// A handler interrupts ppoll, select, or pselect6 with a timeout longer than it can wait, which +// writes back the time left of the whole timeout. +static void test_interrupted_huge_timeout_writes_back_time_left(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.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); + long sec = time_left_sec(&w); + if (sec <= huge - HOUR || sec > huge) { + printf("FAIL: %s: %ld s left of %ld s\n", call_name(call), sec, huge); + 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, timing out. +static void test_call_mask_discards_ignored_signal(enum call call) { + sigset_t old; + block_and_raise(SIGCHLD, &old); + struct timespec timeout = {.tv_sec = 0, .tv_nsec = 50 * 1000 * 1000}; + errno = 0; + long ret = call_with_empty_mask(call, &timeout); + if (ret != 0) { + printf("FAIL: %s with a mask, ignored signal: ret=%ld errno=%d, expected 0\n", + call_name(call), ret, errno); + exit(1); + } + expect_blocked_not_pending(SIGCHLD, call_name(call)); + TEST_ASSERT(pthread_sigmask(SIG_SETMASK, &old, NULL) == 0, "pthread_sigmask"); +} + +// An ignored signal interrupts ppoll or pselect6 as above, but with its timeout in read-only +// memory, so it cannot write back the time left and fails with EINTR, as reissuing it would wait +// the whole timeout again. +static void test_unwritable_timeout_fails_with_eintr(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"); + *timeout = LIVENESS_TIMESPEC; + 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 (ret != -1 || err != EINTR) { + printf("FAIL: %s with a read-only timeout: ret=%ld errno=%d, expected EINTR\n", + call_name(call), 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]); + } + test_futex_wait_with_huge_timeout_and_restarting_handler(FUTEX_RELATIVE); + test_futex_wait_with_huge_timeout_and_restarting_handler(FUTEX_ABSOLUTE); +#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]); + test_interrupted_huge_timeout_writes_back_time_left(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_unwritable_timeout_fails_with_eintr(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/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..1d9797c288 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,297 @@ mod test { litebox_common_linux::errno::Errno::EBADF ); } + + /// Returns the read end of a new pipe, with a byte ready to read if `ready`. + fn pipe_reader(task: &Task, ready: bool) -> i32 { + let (rfd, wfd) = task.sys_pipe2(OFlags::empty()).unwrap(); + if ready { + let wfd = i32::try_from(wfd).unwrap(); + assert_eq!(task.sys_write(wfd, &[1], None), Ok(1)); + } + i32::try_from(rfd).unwrap() + } + + /// Like Linux, an interrupted `poll` resumes through `restart_syscall` and reads its + /// entries again (see the `wait_restart.c` runner test). SIGCHLD is pending before the poll, + /// which waits until an fd is ready, so the interruption is certain. + #[test] + fn test_interrupted_poll_resumes_through_restart_syscall() { + use litebox::platform::ThreadProvider as _; + + let task = crate::syscalls::tests::init_platform(); + TestPlatform::run_test_thread(|| { + let idle = pipe_reader(&task, false); + let ready = pipe_reader(&task, true); + let in_bits = i16::try_from(Events::IN.bits()).unwrap(); + let mut fds = [litebox_common_linux::Pollfd { + fd: idle, + events: in_bits, + revents: -1, + }]; + let fds_ptr = UserPtrMut::from_ptr(fds.as_mut_ptr()); + + crate::syscalls::tests::queue_ignored_sigchld(&task); + assert_eq!( + task.sys_poll( + fds_ptr, + 1, + litebox_common_linux::TimeParam::Milliseconds(-1) + ), + Err(Errno::ERESTART_RESTARTBLOCK) + ); + // Like Linux, the revents are written back even when interrupted. + assert_eq!(fds[0].revents, 0); + crate::syscalls::tests::discard_pending_signals(&task); + + fds[0].fd = ready; + assert_eq!(task.sys_restart_syscall(), Ok(1)); + assert_eq!(fds[0].revents, in_bits); + }); + } + + /// Like Linux, an interrupted `ppoll` or `select` writes back the time left, which a + /// reissued call waits for. SIGCHLD is pending before each call, so the interruption is + /// certain. + #[test] + fn test_interrupted_ppoll_and_select_write_back_time_left() { + use core::time::Duration; + use litebox::platform::ThreadProvider as _; + use litebox_common_linux::{TimeParam, TimeVal, Timespec}; + + const HOUR: Duration = Duration::from_secs(3600); + // The calls take far less than a minute. + let left_range = HOUR.saturating_sub(Duration::from_secs(60))..=HOUR; + + let task = crate::syscalls::tests::init_platform(); + TestPlatform::run_test_thread(|| { + let idle = pipe_reader(&task, false); + let mut fds = [litebox_common_linux::Pollfd { + fd: idle, + events: i16::try_from(Events::IN.bits()).unwrap(), + revents: 0, + }]; + let mut timespec = Timespec::from(HOUR); + crate::syscalls::tests::queue_ignored_sigchld(&task); + assert_eq!( + task.sys_ppoll( + UserPtrMut::from_ptr(fds.as_mut_ptr()), + 1, + TimeParam::Timespec64(UserPtrMut::from_ptr(&raw mut timespec)), + None, + 0, + ), + Err(Errno::ERESTARTNOHAND) + ); + let left = Duration::try_from(timespec).expect("the time left is valid"); + assert!(left_range.contains(&left), "{left:?}"); + crate::syscalls::tests::discard_pending_signals(&task); + + let mut readfds = [1usize << idle]; + let mut timeval = TimeVal::from(HOUR); + crate::syscalls::tests::queue_ignored_sigchld(&task); + assert_eq!( + task.sys_pselect( + u32::try_from(idle + 1).unwrap(), + Some(UserPtrMut::from_ptr(readfds.as_mut_ptr())), + None, + None, + TimeParam::TimeVal(UserPtrMut::from_ptr(&raw mut timeval)), + None, + ), + Err(Errno::ERESTARTNOHAND) + ); + let left = Duration::try_from(timeval).expect("the time left is valid"); + assert!(left_range.contains(&left), "{left:?}"); + crate::syscalls::tests::discard_pending_signals(&task); + }); + } + + /// Like Linux, an interrupted `pselect` keeps its signal mask until signals are processed, + /// so a signal the mask unblocks is taken rather than interrupting the reissued call again, + /// and only then restores the mask it replaced (see the `wait_restart.c` runner test). + #[test] + fn test_interrupted_pselect_keeps_its_mask_until_signals_are_processed() { + use litebox::platform::ThreadProvider as _; + use litebox_common_linux::signal::{SigSet, Signal}; + + let task = crate::syscalls::tests::init_platform(); + TestPlatform::run_test_thread(|| { + crate::syscalls::tests::block_and_raise(&task, Signal::SIGCHLD); + assert!(!task.has_pending_signals(), "SIGCHLD should be blocked"); + + assert_eq!( + crate::syscalls::tests::pselect_idle_pipe_with_mask(&task, SigSet::empty()), + Err(Errno::ERESTARTNOHAND) + ); + assert_eq!(task.signals.blocked(), SigSet::empty()); + assert!( + task.has_pending_signals(), + "SIGCHLD should be pending under the call's mask" + ); + + crate::syscalls::tests::discard_pending_signals(&task); + assert!(!task.has_pending_signals(), "SIGCHLD should be discarded"); + assert_eq!( + task.signals.blocked(), + SigSet::empty().with(Signal::SIGCHLD) + ); + }); + } + + /// Like Linux, `select` and `pselect` end through the same step whether their arguments are + /// wrong or they wait, restoring the signal mask they set and writing back the time left + /// (see the `wait_restart.c` runner test). + #[test] + fn test_pselect_with_bad_arguments_restores_its_mask() { + use litebox_common_linux::signal::{SigSet, Signal}; + use litebox_common_linux::{SigSetPack, TimeParam, Timespec}; + + let task = crate::syscalls::tests::init_platform(); + 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); + } + } + + /// Like Linux, `pselect` given no mask in its argument block, whatever the block's size, + /// leaves the signal mask alone, as glibc's `pselect` passes for a null mask. + #[test] + fn test_pselect_with_null_mask_leaves_mask_alone() { + use litebox_common_linux::signal::SigSet; + use litebox_common_linux::{SigSetPack, TimeParam, Timespec}; + + let task = crate::syscalls::tests::init_platform(); + for size in [core::mem::size_of::(), 0] { + let sigsetpack = SigSetPack { + sigset: UserPtr::from_usize(0), + size, + }; + let mut timeout = Timespec::from(core::time::Duration::ZERO); + assert_eq!( + task.sys_pselect( + 0, + None, + None, + None, + TimeParam::Timespec64(UserPtrMut::from_ptr(&raw mut timeout)), + Some(UserPtr::from_ptr(&raw const sigsetpack)), + ), + Ok(0) + ); + } + } + + /// Like Linux, an interrupted `epoll_pwait` fails with `EINTR` but keeps its signal mask + /// until signals are processed, so a signal the mask unblocks is taken (see the + /// `wait_restart.c` runner test). + #[test] + fn test_interrupted_epoll_pwait_keeps_its_mask_until_signals_are_processed() { + use litebox::platform::ThreadProvider as _; + use litebox_common_linux::signal::{SigSet, Signal}; + + let task = crate::syscalls::tests::init_platform(); + TestPlatform::run_test_thread(|| { + let epfd = epoll_create(&task); + crate::syscalls::tests::block_and_raise(&task, Signal::SIGCHLD); + let empty = SigSet::empty(); + let mut events = [EpollEvent::new(0, 0)]; + assert_eq!( + task.sys_epoll_pwait( + epfd, + UserPtrMut::from_ptr(events.as_mut_ptr()), + 1, + -1, + Some(UserPtr::from_ptr(&raw const empty)), + core::mem::size_of::(), + ), + Err(Errno::EINTR) + ); + assert_eq!(task.signals.blocked(), empty); + + crate::syscalls::tests::discard_pending_signals(&task); + assert!(!task.has_pending_signals(), "SIGCHLD should be discarded"); + assert_eq!( + task.signals.blocked(), + SigSet::empty().with(Signal::SIGCHLD) + ); + }); + } + + /// Like Linux, an interrupted `ppoll` that cannot write back the time left fails with + /// `EINTR`, as reissued it would wait its whole timeout again (see the `wait_restart.c` runner + /// test). + #[test] + fn test_interrupted_ppoll_without_writable_timeout_fails_with_eintr() { + use litebox::platform::ThreadProvider as _; + use litebox_common_linux::{MapFlags, PAGE_SIZE, ProtFlags, TimeParam, Timespec}; + + let task = crate::syscalls::tests::init_platform(); + TestPlatform::run_test_thread(|| { + let page = task + .sys_mmap( + 0, + PAGE_SIZE, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS, + -1, + 0, + ) + .unwrap(); + let timeout = UserPtrMut::::from_usize(page.as_usize()); + timeout + .write_at_offset::( + 0, + Timespec::from(core::time::Duration::from_secs(3600)), + ) + .unwrap(); + task.sys_mprotect(page, PAGE_SIZE, ProtFlags::PROT_READ) + .unwrap(); + + let idle = pipe_reader(&task, false); + let mut fds = [litebox_common_linux::Pollfd { + fd: idle, + events: i16::try_from(Events::IN.bits()).unwrap(), + revents: 0, + }]; + crate::syscalls::tests::queue_ignored_sigchld(&task); + assert_eq!( + task.sys_ppoll( + UserPtrMut::from_ptr(fds.as_mut_ptr()), + 1, + TimeParam::Timespec64(timeout), + None, + 0, + ), + Err(Errno::EINTR) + ); + crate::syscalls::tests::discard_pending_signals(&task); + }); + } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index b1a4e486e3..6531b18473 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,40 @@ 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) { + // Like Linux, an interrupted poll resumes through `restart_syscall` until `deadline` unless + // a handler runs, reading the entries again, so a handler interrupts it even with + // `SA_RESTART`. + 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 +3032,43 @@ 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, self.poll_select_deadline(&timeout)); + 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())); + Ok(PollSelectTimeout { param, read }) + } + + /// Returns when the wait of `ppoll`, `select`, or `pselect` with `timeout` ends, or `None` if + /// it waits until an fd is ready. + fn poll_select_deadline( + &self, + timeout: &PollSelectTimeout<::Instant>, + ) -> Option<::Instant> { + timeout + .read + .map(|(duration, start)| self.relative_deadline_from(start, duration)) + } + + /// 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, like + /// Linux's `do_sys_poll`. + fn do_poll( + &self, + fds: UserPtrMut, + nfds: usize, + deadline: Option<::Instant>, + ) -> Result { if nfds > self .process() @@ -3012,27 +3089,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 +3121,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 +3156,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 +3185,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 +3231,40 @@ 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, + self.poll_select_deadline(&timeout), + ); + 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 +3275,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 +3497,15 @@ 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)>, +} + #[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..1cd0e3f911 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> { @@ -541,21 +575,21 @@ mod tests { assert_eq!(other.join().unwrap(), Ok(())); } - #[test] - fn shared_futex_wakes_racing_timeouts_are_each_reported_once() { + /// Waits that time out race wakes on a word in a mapping with `map_flags`, with `flags`. + fn futex_wakes_racing_timeouts_are_each_reported_once(map_flags: MapFlags, flags: FutexFlags) { use core::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; const WAITS: usize = 300; let task = init_platform(); - let address = map(&task, 1, MapFlags::MAP_SHARED).as_usize(); + let address = map(&task, 1, map_flags).as_usize(); let done = Arc::new(AtomicBool::new(false)); let waiter = { let done = Arc::clone(&done); task.spawn_clone_for_test(move |task| { let mut woken = 0; for _ in 0..WAITS { - match wait(&task, address, 0, FutexFlags::empty(), Some(1)) { + match wait(&task, address, 0, flags, Some(1)) { Ok(_) => woken += 1, Err(Errno::ETIMEDOUT) => {} Err(error) => panic!("unexpected wait failure: {error:?}"), @@ -570,7 +604,7 @@ mod tests { woken += task .sys_futex(FutexArgs::Wake { addr: word(address), - flags: FutexFlags::empty(), + flags, count: 1, }) .unwrap(); @@ -581,4 +615,63 @@ mod tests { // succeeds if a wake selected it. assert_eq!(waiter.join().unwrap(), woken); } + + #[test] + fn shared_futex_wakes_racing_timeouts_are_each_reported_once() { + futex_wakes_racing_timeouts_are_each_reported_once( + MapFlags::MAP_SHARED, + FutexFlags::empty(), + ); + } + + #[test] + fn private_futex_wakes_racing_timeouts_are_each_reported_once() { + futex_wakes_racing_timeouts_are_each_reported_once( + MapFlags::MAP_PRIVATE, + FutexFlags::PRIVATE, + ); + } + + /// Like Linux, a futex wait with a timeout that a signal interrupts resumes through + /// `restart_syscall`, which waits again, checking the word, and then leaves nothing to resume, + /// while a wait without a timeout is reissued (see the `wait_restart.c` runner test). SIGCHLD + /// is pending before each wait, which lasts an hour or has no timeout, so every interruption + /// is certain. + #[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 = || crate::syscalls::tests::discard_pending_signals(&task); + + 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/process.rs b/litebox_shim_linux/src/syscalls/process.rs index a13483137b..418e9a2476 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -241,16 +241,31 @@ 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 { + /// A relative `clock_nanosleep` that sleeps until `deadline` and reports the time left to + /// `remain`. Nanosleep { - deadline: Option, + deadline: Instant, remain: TimeParam, }, + /// A `futex` wait with a timeout, with `flags`, on the word at `addr`, while it holds `val`, + /// for a wake matching `bitset`, or any wake if `None`, until `deadline`. + Futex { + addr: UserPtrMut, + flags: litebox_common_linux::FutexFlags, + val: u32, + bitset: Option, + deadline: Instant, + }, + /// A `poll` of the `nfds` entries at `fds` until `deadline`, or until ready if `None`. + 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 +332,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 +2260,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,50 +2337,58 @@ 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 })) } } } @@ -2364,15 +2396,34 @@ impl Task { /// 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> { + /// Like Linux, this thread's saved state stays until its next sleep or signal handler return, + /// so resuming a sleep or poll that already finished does it again until the same deadline, + /// at once if that has passed, or for a poll without a timeout, until an fd is ready. A + /// resumed futex wait instead leaves nothing to resume. 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 { 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 +4401,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 +4432,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 +4464,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 +4507,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 +4522,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 +4541,92 @@ mod tests { }); } + /// Like Linux, once no handler takes the mask an interrupted `pselect` replaced, the restored + /// mask lets a signal the call's mask blocked reach its handler before the call is reissued, + /// rather than once the reissued call returns (see the `wait_restart.c` runner test). + #[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. + assert_eq!( + crate::syscalls::tests::pselect_idle_pipe_with_mask( + &task, + SigSet::empty().with(Signal::SIGUSR2) + ), + Err(Errno::ERESTARTNOHAND) + ); + + // 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" + ); + }); + } + + /// Like Linux, an absolute deadline, even one past what the platform's `Instant` can hold, + /// is no later than [`MAX_SLEEP_SINCE_BOOT`]. + #[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 +4647,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 +4670,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..f0ea9c2587 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. `Some` plays the role of Linux's `TIF_RESTORE_SIGMASK`. + 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( @@ -930,26 +975,41 @@ impl Task { /// Deliver any pending signals. /// /// If `restart` is set, `ctx` is the return from a syscall interrupted by a signal, which - /// restarts unless the first handler delivered interrupts it. + /// restarts unless the first handler delivered interrupts it. If the syscall left a temporary + /// mask (see [`Self::set_user_signal_mask`]) that no handler's frame saves, then once nothing + /// more can be delivered under it, the syscall restarts if it was to, and the mask it replaced + /// is restored, after which the signals that mask unblocks are delivered without interrupting + /// the syscall. 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..56fe5e8e44 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -57,6 +57,72 @@ 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"); +} + +/// Takes `task`'s pending signals as on its return to the guest, discarding those that run no +/// handler. +pub(crate) fn discard_pending_signals(task: &crate::Task) { + task.process_signals(&mut PtRegs::default(), None); +} + +/// Calls `pselect` for an hour on a pipe never written to, blocking `mask` for the call. +pub(crate) fn pselect_idle_pipe_with_mask( + task: &crate::Task, + mask: litebox_common_linux::signal::SigSet, +) -> Result { + let (idle, _writer) = task.sys_pipe2(OFlags::empty()).expect("pipe2 failed"); + let sigsetpack = litebox_common_linux::SigSetPack { + sigset: UserPtr::from_ptr(&raw const mask), + size: core::mem::size_of::(), + }; + let mut readfds = [1usize << idle]; + let mut timeout = litebox_common_linux::Timespec::from(core::time::Duration::from_secs(3600)); + 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)), + ) +} + 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"); From d9466a1021b6f54503e581bbab66491fa1b2e4e3 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 29 Oct 2026 23:49:02 +0000 Subject: [PATCH 2/6] clean up --- litebox/src/sync/futex.rs | 1 - litebox_shim_linux/src/syscalls/epoll.rs | 17 ----------------- litebox_shim_linux/src/syscalls/file.rs | 6 +----- litebox_shim_linux/src/syscalls/futex.rs | 5 ----- litebox_shim_linux/src/syscalls/process.rs | 18 +++--------------- litebox_shim_linux/src/syscalls/signal/mod.rs | 8 ++------ 6 files changed, 6 insertions(+), 49 deletions(-) diff --git a/litebox/src/sync/futex.rs b/litebox/src/sync/futex.rs index f40854e0d0..526d7d69f0 100644 --- a/litebox/src/sync/futex.rs +++ b/litebox/src/sync/futex.rs @@ -391,7 +391,6 @@ mod tests { assert!((1..=3).contains(&woken)); } - /// A wake that takes the waiter as its wait is interrupted wakes it, rather than being lost. #[test] fn test_futex_wait_woken_as_interrupted_succeeds() { use crate::event::wait::CheckForInterrupt; diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 1d9797c288..db54f605c2 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -763,9 +763,6 @@ mod test { }); } - /// Like Linux, an interrupted `ppoll` or `select` writes back the time left, which a - /// reissued call waits for. SIGCHLD is pending before each call, so the interruption is - /// certain. #[test] fn test_interrupted_ppoll_and_select_write_back_time_left() { use core::time::Duration; @@ -820,9 +817,6 @@ mod test { }); } - /// Like Linux, an interrupted `pselect` keeps its signal mask until signals are processed, - /// so a signal the mask unblocks is taken rather than interrupting the reissued call again, - /// and only then restores the mask it replaced (see the `wait_restart.c` runner test). #[test] fn test_interrupted_pselect_keeps_its_mask_until_signals_are_processed() { use litebox::platform::ThreadProvider as _; @@ -852,9 +846,6 @@ mod test { }); } - /// Like Linux, `select` and `pselect` end through the same step whether their arguments are - /// wrong or they wait, restoring the signal mask they set and writing back the time left - /// (see the `wait_restart.c` runner test). #[test] fn test_pselect_with_bad_arguments_restores_its_mask() { use litebox_common_linux::signal::{SigSet, Signal}; @@ -893,8 +884,6 @@ mod test { } } - /// Like Linux, `pselect` given no mask in its argument block, whatever the block's size, - /// leaves the signal mask alone, as glibc's `pselect` passes for a null mask. #[test] fn test_pselect_with_null_mask_leaves_mask_alone() { use litebox_common_linux::signal::SigSet; @@ -921,9 +910,6 @@ mod test { } } - /// Like Linux, an interrupted `epoll_pwait` fails with `EINTR` but keeps its signal mask - /// until signals are processed, so a signal the mask unblocks is taken (see the - /// `wait_restart.c` runner test). #[test] fn test_interrupted_epoll_pwait_keeps_its_mask_until_signals_are_processed() { use litebox::platform::ThreadProvider as _; @@ -957,9 +943,6 @@ mod test { }); } - /// Like Linux, an interrupted `ppoll` that cannot write back the time left fails with - /// `EINTR`, as reissued it would wait its whole timeout again (see the `wait_restart.c` runner - /// test). #[test] fn test_interrupted_ppoll_without_writable_timeout_fails_with_eintr() { use litebox::platform::ThreadProvider as _; diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 6531b18473..859e5f6c41 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -3011,9 +3011,6 @@ impl Task { deadline: Option<::Instant>, ) -> Result { match self.do_poll(fds, nfds, deadline) { - // Like Linux, an interrupted poll resumes through `restart_syscall` until `deadline` unless - // a handler runs, reading the entries again, so a handler interrupts it even with - // `SA_RESTART`. Err(Errno::ERESTARTNOHAND) => Err(self.thread.save_restart_block(RestartBlock::Poll { fds, nfds, @@ -3061,8 +3058,7 @@ impl Task { } /// 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, like - /// Linux's `do_sys_poll`. + /// writes back their `revents`, failing with [`Errno::ERESTARTNOHAND`] if interrupted. fn do_poll( &self, fds: UserPtrMut, diff --git a/litebox_shim_linux/src/syscalls/futex.rs b/litebox_shim_linux/src/syscalls/futex.rs index 1cd0e3f911..5ba26034ad 100644 --- a/litebox_shim_linux/src/syscalls/futex.rs +++ b/litebox_shim_linux/src/syscalls/futex.rs @@ -632,11 +632,6 @@ mod tests { ); } - /// Like Linux, a futex wait with a timeout that a signal interrupts resumes through - /// `restart_syscall`, which waits again, checking the word, and then leaves nothing to resume, - /// while a wait without a timeout is reissued (see the `wait_restart.c` runner test). SIGCHLD - /// is pending before each wait, which lasts an hour or has no timeout, so every interruption - /// is certain. #[test] fn test_interrupted_futex_wait_with_timeout_resumes_through_restart_syscall() { use litebox::platform::ThreadProvider as _; diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index 418e9a2476..c1aaeb35b6 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -242,14 +242,10 @@ unsafe impl Send for ThreadState {} /// `restart_syscall` resumes. #[derive(Clone, Copy)] pub(super) enum RestartBlock { - /// A relative `clock_nanosleep` that sleeps until `deadline` and reports the time left to - /// `remain`. Nanosleep { deadline: Instant, remain: TimeParam, }, - /// A `futex` wait with a timeout, with `flags`, on the word at `addr`, while it holds `val`, - /// for a wake matching `bitset`, or any wake if `None`, until `deadline`. Futex { addr: UserPtrMut, flags: litebox_common_linux::FutexFlags, @@ -257,7 +253,6 @@ pub(super) enum RestartBlock { bitset: Option, deadline: Instant, }, - /// A `poll` of the `nfds` entries at `fds` until `deadline`, or until ready if `None`. Poll { fds: UserPtrMut, nfds: usize, @@ -2397,11 +2392,9 @@ impl Task { /// [`Errno::ERESTART_RESTARTBLOCK`]. /// /// Like Linux, this thread's saved state stays until its next sleep or signal handler return, - /// so resuming a sleep or poll that already finished does it again until the same deadline, - /// at once if that has passed, or for a poll without a timeout, until an fd is ready. A - /// resumed futex wait instead leaves nothing to resume. 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. + /// so resuming a sleep or poll that already finished does it again until the same deadline. + /// 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 { match self.thread.restart_block.get() { Some(RestartBlock::Nanosleep { deadline, remain }) => { @@ -4541,9 +4534,6 @@ mod tests { }); } - /// Like Linux, once no handler takes the mask an interrupted `pselect` replaced, the restored - /// mask lets a signal the call's mask blocked reach its handler before the call is reissued, - /// rather than once the reissued call returns (see the `wait_restart.c` runner test). #[test] fn test_signal_unblocked_by_restored_mask_is_delivered_before_reissue() { use crate::syscalls::signal::SyscallRestart; @@ -4600,8 +4590,6 @@ mod tests { }); } - /// Like Linux, an absolute deadline, even one past what the platform's `Instant` can hold, - /// is no later than [`MAX_SLEEP_SINCE_BOOT`]. #[test] fn test_absolute_deadline_is_at_most_max_sleep_since_boot() { use super::MAX_SLEEP_SINCE_BOOT; diff --git a/litebox_shim_linux/src/syscalls/signal/mod.rs b/litebox_shim_linux/src/syscalls/signal/mod.rs index f0ea9c2587..4454716d9a 100644 --- a/litebox_shim_linux/src/syscalls/signal/mod.rs +++ b/litebox_shim_linux/src/syscalls/signal/mod.rs @@ -48,7 +48,7 @@ pub(crate) struct SignalState { /// 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. `Some` plays the role of Linux's `TIF_RESTORE_SIGMASK`. + /// handler's frame saves it. saved_sigmask: Cell>, } @@ -975,11 +975,7 @@ impl Task { /// Deliver any pending signals. /// /// If `restart` is set, `ctx` is the return from a syscall interrupted by a signal, which - /// restarts unless the first handler delivered interrupts it. If the syscall left a temporary - /// mask (see [`Self::set_user_signal_mask`]) that no handler's frame saves, then once nothing - /// more can be delivered under it, the syscall restarts if it was to, and the mask it replaced - /// is restored, after which the signals that mask unblocks are delivered without interrupting - /// the syscall. + /// restarts unless the first handler delivered interrupts it. pub(crate) fn process_signals(&self, ctx: &mut PtRegs, mut restart: Option) { loop { let blocked = self.signals.blocked.get(); From c4f6b4b4c5f178c98b752e5e0b4f216092c795d2 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 29 Oct 2026 23:57:49 +0000 Subject: [PATCH 3/6] Trim overlapping wait restart tests Drop unit tests that the wait_restart.c runner test already covers deterministically, keeping only those that check what it cannot: the lost-wake race, futex resumption, pselect restoring its mask on bad arguments, delivery under the restored mask, and the absolute deadline cap. Restore the shared racing-futex test to its original form, and fold overlapping C test cases together. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/wait_restart.c | 110 +++----- litebox_shim_linux/src/syscalls/epoll.rs | 238 ------------------ litebox_shim_linux/src/syscalls/futex.rs | 29 +-- litebox_shim_linux/src/syscalls/process.rs | 27 +- litebox_shim_linux/src/syscalls/tests.rs | 28 --- 5 files changed, 64 insertions(+), 368 deletions(-) diff --git a/litebox_runner_linux_userland/tests/wait_restart.c b/litebox_runner_linux_userland/tests/wait_restart.c index b23d92325d..93d25291fb 100644 --- a/litebox_runner_linux_userland/tests/wait_restart.c +++ b/litebox_runner_linux_userland/tests/wait_restart.c @@ -58,8 +58,7 @@ struct waiter { long expected_restart_ret; uint32_t word; struct pollfd fds[1]; - // The timeout of futex, relative even for an absolute wait, and of ppoll and pselect6, which - // the last two write back. + // 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; @@ -92,7 +91,7 @@ static long do_call(struct waiter *w) { case FUTEX_ABSOLUTE: { struct timespec deadline; TEST_ASSERT(clock_gettime(CLOCK_MONOTONIC, &deadline) == 0, "clock_gettime"); - deadline.tv_sec += w->ts.tv_sec; + deadline.tv_sec += HOUR; return syscall(SYS_futex, &w->word, FUTEX_WAIT_BITSET_PRIVATE, 0, &deadline, NULL, FUTEX_BITSET_MATCH_ANY); } @@ -295,20 +294,6 @@ static void test_futex_wait_with_restarting_handler(enum call call) { expect_result(&w, call == FUTEX_UNTIMED ? 0 : -1, EINTR); } -// Even with a timeout longer than it can wait, a futex wait is interrupted by a handler with -// SA_RESTART, which restarts only a wait without a timeout. -static void test_futex_wait_with_huge_timeout_and_restarting_handler(enum call call) { - set_handler(SIGUSR1, handle_signal, SA_RESTART); - static struct waiter w; - init_waiter(&w, call); - w.ts = (struct timespec){.tv_sec = 1000L * 1000 * 1000 * 1000, .tv_nsec = 0}; - launch_waiter(&w); - wait_done(&w, SIGUSR1); - finish_waiter(&w); - set_handler(SIGUSR1, SIG_DFL, 0); - expect_result(&w, -1, EINTR); -} - #ifdef SYS_poll static int revents_written_or_done(void *arg) { struct waiter *w = arg; @@ -392,12 +377,15 @@ static void test_wait_reissued_after_ignored_signal(enum call call) { } // Even with SA_RESTART, a handler interrupts poll, ppoll, select, and pselect6, poll writing back -// the revents and the others the time left. +// 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); @@ -406,8 +394,10 @@ static void test_wait_with_restarting_handler(enum call call) { if (call == POLL || call == PPOLL) { TEST_ASSERT(w.fds[0].revents == 0, "an interrupted poll writes back the revents"); } - if (call != POLL) { - expect_time_left(&w); + 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); } } @@ -429,27 +419,6 @@ static void test_timed_out_wait_writes_back_zero(enum call call) { } } -// A handler interrupts ppoll, select, or pselect6 with a timeout longer than it can wait, which -// writes back the time left of the whole timeout. -static void test_interrupted_huge_timeout_writes_back_time_left(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.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); - long sec = time_left_sec(&w); - if (sec <= huge - HOUR || sec > huge) { - printf("FAIL: %s: %ld s left of %ld s\n", call_name(call), sec, huge); - exit(1); - } -} - static atomic_int handled; static atomic_int handler_failed; static sigset_t mask_in_handler; @@ -570,44 +539,35 @@ static void test_call_mask_lets_handler_run(enum call call) { } // An ignored signal pending while blocked, which the call's mask unblocks, interrupts ppoll or -// pselect6, which discards it and is reissued, timing out. +// 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) { - sigset_t old; - block_and_raise(SIGCHLD, &old); - struct timespec timeout = {.tv_sec = 0, .tv_nsec = 50 * 1000 * 1000}; - errno = 0; - long ret = call_with_empty_mask(call, &timeout); - if (ret != 0) { - printf("FAIL: %s with a mask, ignored signal: ret=%ld errno=%d, expected 0\n", - call_name(call), ret, errno); - exit(1); - } - expect_blocked_not_pending(SIGCHLD, call_name(call)); - TEST_ASSERT(pthread_sigmask(SIG_SETMASK, &old, NULL) == 0, "pthread_sigmask"); -} - -// An ignored signal interrupts ppoll or pselect6 as above, but with its timeout in read-only -// memory, so it cannot write back the time left and fails with EINTR, as reissuing it would wait -// the whole timeout again. -static void test_unwritable_timeout_fails_with_eintr(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"); - *timeout = LIVENESS_TIMESPEC; - 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 (ret != -1 || err != EINTR) { - printf("FAIL: %s with a read-only timeout: ret=%ld errno=%d, expected EINTR\n", - call_name(call), ret, err); - exit(1); + 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"); } - 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"); } @@ -779,8 +739,6 @@ int main(void) { test_resumed_futex_wait_checks_word(futex_calls[i]); test_futex_wait_with_restarting_handler(futex_calls[i]); } - test_futex_wait_with_huge_timeout_and_restarting_handler(FUTEX_RELATIVE); - test_futex_wait_with_huge_timeout_and_restarting_handler(FUTEX_ABSOLUTE); #ifdef SYS_poll test_resumed_poll_reads_fds(-1); test_resumed_poll_reads_fds(HOUR * 1000); @@ -797,13 +755,11 @@ int main(void) { 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]); - test_interrupted_huge_timeout_writes_back_time_left(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_unwritable_timeout_fails_with_eintr(masked_calls[i]); test_bad_arguments_restore_mask(masked_calls[i]); test_restored_mask_lets_handler_run(masked_calls[i]); } diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index db54f605c2..76117b321b 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -715,137 +715,6 @@ mod test { ); } - /// Returns the read end of a new pipe, with a byte ready to read if `ready`. - fn pipe_reader(task: &Task, ready: bool) -> i32 { - let (rfd, wfd) = task.sys_pipe2(OFlags::empty()).unwrap(); - if ready { - let wfd = i32::try_from(wfd).unwrap(); - assert_eq!(task.sys_write(wfd, &[1], None), Ok(1)); - } - i32::try_from(rfd).unwrap() - } - - /// Like Linux, an interrupted `poll` resumes through `restart_syscall` and reads its - /// entries again (see the `wait_restart.c` runner test). SIGCHLD is pending before the poll, - /// which waits until an fd is ready, so the interruption is certain. - #[test] - fn test_interrupted_poll_resumes_through_restart_syscall() { - use litebox::platform::ThreadProvider as _; - - let task = crate::syscalls::tests::init_platform(); - TestPlatform::run_test_thread(|| { - let idle = pipe_reader(&task, false); - let ready = pipe_reader(&task, true); - let in_bits = i16::try_from(Events::IN.bits()).unwrap(); - let mut fds = [litebox_common_linux::Pollfd { - fd: idle, - events: in_bits, - revents: -1, - }]; - let fds_ptr = UserPtrMut::from_ptr(fds.as_mut_ptr()); - - crate::syscalls::tests::queue_ignored_sigchld(&task); - assert_eq!( - task.sys_poll( - fds_ptr, - 1, - litebox_common_linux::TimeParam::Milliseconds(-1) - ), - Err(Errno::ERESTART_RESTARTBLOCK) - ); - // Like Linux, the revents are written back even when interrupted. - assert_eq!(fds[0].revents, 0); - crate::syscalls::tests::discard_pending_signals(&task); - - fds[0].fd = ready; - assert_eq!(task.sys_restart_syscall(), Ok(1)); - assert_eq!(fds[0].revents, in_bits); - }); - } - - #[test] - fn test_interrupted_ppoll_and_select_write_back_time_left() { - use core::time::Duration; - use litebox::platform::ThreadProvider as _; - use litebox_common_linux::{TimeParam, TimeVal, Timespec}; - - const HOUR: Duration = Duration::from_secs(3600); - // The calls take far less than a minute. - let left_range = HOUR.saturating_sub(Duration::from_secs(60))..=HOUR; - - let task = crate::syscalls::tests::init_platform(); - TestPlatform::run_test_thread(|| { - let idle = pipe_reader(&task, false); - let mut fds = [litebox_common_linux::Pollfd { - fd: idle, - events: i16::try_from(Events::IN.bits()).unwrap(), - revents: 0, - }]; - let mut timespec = Timespec::from(HOUR); - crate::syscalls::tests::queue_ignored_sigchld(&task); - assert_eq!( - task.sys_ppoll( - UserPtrMut::from_ptr(fds.as_mut_ptr()), - 1, - TimeParam::Timespec64(UserPtrMut::from_ptr(&raw mut timespec)), - None, - 0, - ), - Err(Errno::ERESTARTNOHAND) - ); - let left = Duration::try_from(timespec).expect("the time left is valid"); - assert!(left_range.contains(&left), "{left:?}"); - crate::syscalls::tests::discard_pending_signals(&task); - - let mut readfds = [1usize << idle]; - let mut timeval = TimeVal::from(HOUR); - crate::syscalls::tests::queue_ignored_sigchld(&task); - assert_eq!( - task.sys_pselect( - u32::try_from(idle + 1).unwrap(), - Some(UserPtrMut::from_ptr(readfds.as_mut_ptr())), - None, - None, - TimeParam::TimeVal(UserPtrMut::from_ptr(&raw mut timeval)), - None, - ), - Err(Errno::ERESTARTNOHAND) - ); - let left = Duration::try_from(timeval).expect("the time left is valid"); - assert!(left_range.contains(&left), "{left:?}"); - crate::syscalls::tests::discard_pending_signals(&task); - }); - } - - #[test] - fn test_interrupted_pselect_keeps_its_mask_until_signals_are_processed() { - use litebox::platform::ThreadProvider as _; - use litebox_common_linux::signal::{SigSet, Signal}; - - let task = crate::syscalls::tests::init_platform(); - TestPlatform::run_test_thread(|| { - crate::syscalls::tests::block_and_raise(&task, Signal::SIGCHLD); - assert!(!task.has_pending_signals(), "SIGCHLD should be blocked"); - - assert_eq!( - crate::syscalls::tests::pselect_idle_pipe_with_mask(&task, SigSet::empty()), - Err(Errno::ERESTARTNOHAND) - ); - assert_eq!(task.signals.blocked(), SigSet::empty()); - assert!( - task.has_pending_signals(), - "SIGCHLD should be pending under the call's mask" - ); - - crate::syscalls::tests::discard_pending_signals(&task); - assert!(!task.has_pending_signals(), "SIGCHLD should be discarded"); - assert_eq!( - task.signals.blocked(), - SigSet::empty().with(Signal::SIGCHLD) - ); - }); - } - #[test] fn test_pselect_with_bad_arguments_restores_its_mask() { use litebox_common_linux::signal::{SigSet, Signal}; @@ -883,111 +752,4 @@ mod test { assert_eq!(task.signals.blocked(), blocked); } } - - #[test] - fn test_pselect_with_null_mask_leaves_mask_alone() { - use litebox_common_linux::signal::SigSet; - use litebox_common_linux::{SigSetPack, TimeParam, Timespec}; - - let task = crate::syscalls::tests::init_platform(); - for size in [core::mem::size_of::(), 0] { - let sigsetpack = SigSetPack { - sigset: UserPtr::from_usize(0), - size, - }; - let mut timeout = Timespec::from(core::time::Duration::ZERO); - assert_eq!( - task.sys_pselect( - 0, - None, - None, - None, - TimeParam::Timespec64(UserPtrMut::from_ptr(&raw mut timeout)), - Some(UserPtr::from_ptr(&raw const sigsetpack)), - ), - Ok(0) - ); - } - } - - #[test] - fn test_interrupted_epoll_pwait_keeps_its_mask_until_signals_are_processed() { - use litebox::platform::ThreadProvider as _; - use litebox_common_linux::signal::{SigSet, Signal}; - - let task = crate::syscalls::tests::init_platform(); - TestPlatform::run_test_thread(|| { - let epfd = epoll_create(&task); - crate::syscalls::tests::block_and_raise(&task, Signal::SIGCHLD); - let empty = SigSet::empty(); - let mut events = [EpollEvent::new(0, 0)]; - assert_eq!( - task.sys_epoll_pwait( - epfd, - UserPtrMut::from_ptr(events.as_mut_ptr()), - 1, - -1, - Some(UserPtr::from_ptr(&raw const empty)), - core::mem::size_of::(), - ), - Err(Errno::EINTR) - ); - assert_eq!(task.signals.blocked(), empty); - - crate::syscalls::tests::discard_pending_signals(&task); - assert!(!task.has_pending_signals(), "SIGCHLD should be discarded"); - assert_eq!( - task.signals.blocked(), - SigSet::empty().with(Signal::SIGCHLD) - ); - }); - } - - #[test] - fn test_interrupted_ppoll_without_writable_timeout_fails_with_eintr() { - use litebox::platform::ThreadProvider as _; - use litebox_common_linux::{MapFlags, PAGE_SIZE, ProtFlags, TimeParam, Timespec}; - - let task = crate::syscalls::tests::init_platform(); - TestPlatform::run_test_thread(|| { - let page = task - .sys_mmap( - 0, - PAGE_SIZE, - ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, - MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS, - -1, - 0, - ) - .unwrap(); - let timeout = UserPtrMut::::from_usize(page.as_usize()); - timeout - .write_at_offset::( - 0, - Timespec::from(core::time::Duration::from_secs(3600)), - ) - .unwrap(); - task.sys_mprotect(page, PAGE_SIZE, ProtFlags::PROT_READ) - .unwrap(); - - let idle = pipe_reader(&task, false); - let mut fds = [litebox_common_linux::Pollfd { - fd: idle, - events: i16::try_from(Events::IN.bits()).unwrap(), - revents: 0, - }]; - crate::syscalls::tests::queue_ignored_sigchld(&task); - assert_eq!( - task.sys_ppoll( - UserPtrMut::from_ptr(fds.as_mut_ptr()), - 1, - TimeParam::Timespec64(timeout), - None, - 0, - ), - Err(Errno::EINTR) - ); - crate::syscalls::tests::discard_pending_signals(&task); - }); - } } diff --git a/litebox_shim_linux/src/syscalls/futex.rs b/litebox_shim_linux/src/syscalls/futex.rs index 5ba26034ad..4b750dc149 100644 --- a/litebox_shim_linux/src/syscalls/futex.rs +++ b/litebox_shim_linux/src/syscalls/futex.rs @@ -575,21 +575,21 @@ mod tests { assert_eq!(other.join().unwrap(), Ok(())); } - /// Waits that time out race wakes on a word in a mapping with `map_flags`, with `flags`. - fn futex_wakes_racing_timeouts_are_each_reported_once(map_flags: MapFlags, flags: FutexFlags) { + #[test] + fn shared_futex_wakes_racing_timeouts_are_each_reported_once() { use core::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; const WAITS: usize = 300; let task = init_platform(); - let address = map(&task, 1, map_flags).as_usize(); + let address = map(&task, 1, MapFlags::MAP_SHARED).as_usize(); let done = Arc::new(AtomicBool::new(false)); let waiter = { let done = Arc::clone(&done); task.spawn_clone_for_test(move |task| { let mut woken = 0; for _ in 0..WAITS { - match wait(&task, address, 0, flags, Some(1)) { + match wait(&task, address, 0, FutexFlags::empty(), Some(1)) { Ok(_) => woken += 1, Err(Errno::ETIMEDOUT) => {} Err(error) => panic!("unexpected wait failure: {error:?}"), @@ -604,7 +604,7 @@ mod tests { woken += task .sys_futex(FutexArgs::Wake { addr: word(address), - flags, + flags: FutexFlags::empty(), count: 1, }) .unwrap(); @@ -616,22 +616,6 @@ mod tests { assert_eq!(waiter.join().unwrap(), woken); } - #[test] - fn shared_futex_wakes_racing_timeouts_are_each_reported_once() { - futex_wakes_racing_timeouts_are_each_reported_once( - MapFlags::MAP_SHARED, - FutexFlags::empty(), - ); - } - - #[test] - fn private_futex_wakes_racing_timeouts_are_each_reported_once() { - futex_wakes_racing_timeouts_are_each_reported_once( - MapFlags::MAP_PRIVATE, - FutexFlags::PRIVATE, - ); - } - #[test] fn test_interrupted_futex_wait_with_timeout_resumes_through_restart_syscall() { use litebox::platform::ThreadProvider as _; @@ -639,7 +623,8 @@ mod tests { let task = init_platform(); TestPlatform::run_test_thread(|| { let address = map(&task, 1, MapFlags::MAP_PRIVATE).as_usize(); - let discard_sigchld = || crate::syscalls::tests::discard_pending_signals(&task); + let discard_sigchld = + || task.process_signals(&mut litebox_common_linux::PtRegs::default(), None); crate::syscalls::tests::queue_ignored_sigchld(&task); assert_eq!( diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index c1aaeb35b6..705df07bf2 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -4567,13 +4567,34 @@ mod tests { .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!( - crate::syscalls::tests::pselect_idle_pipe_with_mask( - &task, - SigSet::empty().with(Signal::SIGUSR2) + 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. diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 56fe5e8e44..c372a4119f 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -95,34 +95,6 @@ pub(crate) fn queue_ignored_sigchld(task: &crate::Task) { assert!(task.has_pending_signals(), "SIGCHLD should be pending"); } -/// Takes `task`'s pending signals as on its return to the guest, discarding those that run no -/// handler. -pub(crate) fn discard_pending_signals(task: &crate::Task) { - task.process_signals(&mut PtRegs::default(), None); -} - -/// Calls `pselect` for an hour on a pipe never written to, blocking `mask` for the call. -pub(crate) fn pselect_idle_pipe_with_mask( - task: &crate::Task, - mask: litebox_common_linux::signal::SigSet, -) -> Result { - let (idle, _writer) = task.sys_pipe2(OFlags::empty()).expect("pipe2 failed"); - let sigsetpack = litebox_common_linux::SigSetPack { - sigset: UserPtr::from_ptr(&raw const mask), - size: core::mem::size_of::(), - }; - let mut readfds = [1usize << idle]; - let mut timeout = litebox_common_linux::Timespec::from(core::time::Duration::from_secs(3600)); - 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)), - ) -} - 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"); From 58c4ab076f776079f4fc01388848eb26543f79a4 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 29 Oct 2026 17:14:19 -0700 Subject: [PATCH 4/6] Run fault-recovery tests on LiteBox-managed test threads Windows recovers faulting user-memory accesses only on threads with LiteBox TLS installed, so tests that intentionally fault must run under run_test_thread. This fixes the Windows CI crash in test_pselect_with_bad_arguments_restores_its_mask and re-enables inaccessible_memory_faults and test_fallible_read on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/guest_buffer.rs | 19 +++---- litebox_shim_linux/src/syscalls/epoll.rs | 64 +++++++++++++----------- litebox_shim_linux/src/syscalls/mm.rs | 15 +++--- 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/litebox_shim_linux/src/guest_buffer.rs b/litebox_shim_linux/src/guest_buffer.rs index b3e4ff8fcc..55caa46cd8 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 _ = 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)); + }); } #[test] diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 76117b321b..7b131a364c 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -717,39 +717,43 @@ mod test { #[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(); - 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) + // The faulting read below is recovered only on a LiteBox-managed thread. + TestPlatform::run_test_thread(|| { + let task = crate::syscalls::tests::init_platform(); + let blocked = SigSet::empty().with(Signal::SIGCHLD); + crate::syscalls::tests::sigprocmask( + &task, + litebox_common_linux::signal::SigmaskHow::SIG_BLOCK, + blocked, ); - assert_eq!(task.signals.blocked(), 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/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index a1aa294c44..6509be9afa 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -980,15 +980,16 @@ 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 _ = init_platform(); + + let ptr = UserPtrMut::::from_usize(0xdeadbeef); + let result = ptr.read_at_offset::(0); + assert!(result.is_none()); + }); } #[test] From f594b786eb50cccc4ccdb933a58e947e419839f0 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 30 Oct 2026 00:29:07 +0000 Subject: [PATCH 5/6] Initialize the test platform before entering run_test_thread On macOS, run_test_thread installs per-thread TLS using the key created by MacosUserland::new(). Calling init_platform() inside the closure meant the key did not exist yet in a fresh nextest process, aborting with "macOS TLS is not initialized". Initialize the platform first, as other tests do. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/guest_buffer.rs | 2 +- litebox_shim_linux/src/syscalls/epoll.rs | 2 +- litebox_shim_linux/src/syscalls/mm.rs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/litebox_shim_linux/src/guest_buffer.rs b/litebox_shim_linux/src/guest_buffer.rs index 55caa46cd8..46ad18ec0c 100644 --- a/litebox_shim_linux/src/guest_buffer.rs +++ b/litebox_shim_linux/src/guest_buffer.rs @@ -236,9 +236,9 @@ mod tests { #[test] fn inaccessible_memory_faults() { + let _ = crate::syscalls::tests::init_platform(); // The faulting read is recovered only on a LiteBox-managed thread. ::run_test_thread(|| { - let _ = crate::syscalls::tests::init_platform(); let iov = IoWriteVec { iov_base: UserPtr::from_usize(0xdead_beef), iov_len: 4, diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 7b131a364c..8fa7ab15c5 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -721,9 +721,9 @@ mod test { 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 task = crate::syscalls::tests::init_platform(); let blocked = SigSet::empty().with(Signal::SIGCHLD); crate::syscalls::tests::sigprocmask( &task, diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 6509be9afa..e27b4141cd 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -982,10 +982,9 @@ mod tests { #[test] fn test_fallible_read() { + let _ = init_platform(); // The faulting read is recovered only on a LiteBox-managed thread. ::run_test_thread(|| { - let _ = init_platform(); - let ptr = UserPtrMut::::from_usize(0xdeadbeef); let result = ptr.read_at_offset::(0); assert!(result.is_none()); From 42eaa40fbebe8c70395a16f0a9f35abf64d5cb6f Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 30 Oct 2026 07:18:45 +0000 Subject: [PATCH 6/6] Compute the poll and select deadline once when reading the timeout --- litebox_shim_linux/src/syscalls/file.rs | 30 ++++++++-------------- litebox_shim_linux/src/syscalls/process.rs | 5 ---- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 859e5f6c41..9c45f97e86 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -3031,7 +3031,7 @@ impl Task { ) -> Result { let timeout = self.read_poll_select_timeout(timeout)?; self.set_user_signal_mask(sigmask, sigsetsize)?; - let result = self.do_poll(fds, nfds, self.poll_select_deadline(&timeout)); + let result = self.do_poll(fds, nfds, timeout.deadline); self.finish_poll_or_select(result, &timeout) } @@ -3043,18 +3043,12 @@ impl Task { let read = param .read::()? .map(|duration| (duration, self.global.platform.now())); - Ok(PollSelectTimeout { param, read }) - } - - /// Returns when the wait of `ppoll`, `select`, or `pselect` with `timeout` ends, or `None` if - /// it waits until an fd is ready. - fn poll_select_deadline( - &self, - timeout: &PollSelectTimeout<::Instant>, - ) -> Option<::Instant> { - timeout - .read - .map(|(duration, start)| self.relative_deadline_from(start, duration)) + 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 @@ -3240,13 +3234,7 @@ impl Task { }; let timeout = self.read_poll_select_timeout(timeout)?; self.set_user_signal_mask(sigmask, sigsetsize)?; - let result = self.select_fds( - nfds, - readfds, - writefds, - exceptfds, - self.poll_select_deadline(&timeout), - ); + let result = self.select_fds(nfds, readfds, writefds, exceptfds, timeout.deadline); self.finish_poll_or_select(result, &timeout) } @@ -3500,6 +3488,8 @@ struct PollSelectTimeout { 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)] diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index 705df07bf2..788dac7468 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -2390,11 +2390,6 @@ impl Task { /// 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 or poll that already finished does it again until the same deadline. - /// 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 { match self.thread.restart_block.get() { Some(RestartBlock::Nanosleep { deadline, remain }) => {