diff --git a/src/runtime/forkipc.c b/src/runtime/forkipc.c index c9eb8ec8..3f0349c2 100644 --- a/src/runtime/forkipc.c +++ b/src/runtime/forkipc.c @@ -67,6 +67,7 @@ #include "syscall/linux-wire.h" #include "syscall/chown-overlay.h" +#include "syscall/fs.h" #include "syscall/internal.h" #include "syscall/mem.h" #include "syscall/net.h" /* absock namespace IPC state */ @@ -83,11 +84,13 @@ * sys_clone further down. */ #define LINUX_CLONE_VM 0x00000100 +#define LINUX_CLONE_PIDFD 0x00001000 #define LINUX_CLONE_VFORK 0x00004000 #define LINUX_CLONE_THREAD 0x00010000 #define LINUX_CLONE_SETTLS 0x00080000 #define LINUX_CLONE_PARENT_SETTID 0x00100000 #define LINUX_CLONE_CHILD_CLEARTID 0x00200000 +#define LINUX_CLONE_DETACHED 0x00400000 #define LINUX_CLONE_CHILD_SETTID 0x01000000 /* LINUX_SIGCHLD defined in syscall_signal.h (included above) */ @@ -1162,6 +1165,25 @@ static void *thread_create_and_run(void *arg) */ static void *vm_clone_thread_run(void *arg); +/* Publish the pidfd before the child can run. Copyout failure must cancel + * creation, as in Linux copy_process(). + */ +static int clone_create_pidfd(guest_t *g, + int64_t child_pid, + pid_t host_pid, + uint64_t pidfd_gva) +{ + int fd = pidfd_create(g, child_pid, host_pid); + if (fd < 0) + return fd; + int32_t value = fd; + if (guest_write_small(g, pidfd_gva, &value, sizeof(value)) < 0) { + sys_close(fd); + return -LINUX_EFAULT; + } + return fd; +} + static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu, guest_t *g, uint64_t flags, @@ -1169,6 +1191,7 @@ static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu, uint64_t stack_map_start, uint64_t stack_map_end, uint64_t ptid_gva, + uint64_t pidfd_gva, uint64_t tls, uint64_t ctid_gva, bool verbose) @@ -1233,6 +1256,17 @@ static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu, return -LINUX_EFAULT; } + int child_pidfd = -1; + if (flags & LINUX_CLONE_PIDFD) { + child_pidfd = clone_create_pidfd(g, child_tid, -1, pidfd_gva); + if (child_pidfd < 0) { + clone_rollback_tid_flags(g, &tid_rollback); + free(tca); + thread_deactivate(t); + return child_pidfd; + } + } + /* Create the host pthread */ pthread_t host_thread; pthread_attr_t attr; @@ -1244,6 +1278,8 @@ static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu, if (err != 0) { log_error("clone_vm: pthread_create failed: %s", strerror(err)); + if (child_pidfd >= 0) + sys_close(child_pidfd); free(tca); thread_deactivate(t); @@ -1516,6 +1552,7 @@ int64_t sys_clone(hv_vcpu_t vcpu, uint64_t stack_map_start, uint64_t stack_map_end, uint64_t ptid_gva, + uint64_t pidfd_gva, uint64_t tls, uint64_t ctid_gva, bool verbose) @@ -1528,6 +1565,16 @@ int64_t sys_clone(hv_vcpu_t vcpu, if ((flags & ~(uint64_t) 0xff) & LINUX_CLONE3_NS_FLAGS) return -LINUX_EINVAL; + bool want_pidfd = (flags & LINUX_CLONE_PIDFD) != 0; + /* clone shares the parent_tid and pidfd argument; clone3 keeps them + * separate. Linux rejects aliased outputs and CLONE_DETACHED with PIDFD. + * Thread pidfds are unsupported here. + */ + if (want_pidfd && + ((flags & (LINUX_CLONE_THREAD | LINUX_CLONE_DETACHED)) || + ((flags & LINUX_CLONE_PARENT_SETTID) && pidfd_gva == ptid_gva))) + return -LINUX_EINVAL; + /* CLONE_THREAD: create a new thread in the same VM (not a new process) */ if (flags & LINUX_CLONE_THREAD) { return sys_clone_thread(vcpu, g, flags, child_stack, stack_map_start, @@ -1558,7 +1605,8 @@ int64_t sys_clone(hv_vcpu_t vcpu, if ((flags & LINUX_CLONE_VM) && !(flags & LINUX_CLONE_THREAD) && !is_vfork) { return sys_clone_vm(vcpu, g, flags, child_stack, stack_map_start, - stack_map_end, ptid_gva, tls, ctid_gva, verbose); + stack_map_end, ptid_gva, pidfd_gva, tls, ctid_gva, + verbose); } log_debug("clone(flags=0x%llx, vfork=%d)", (unsigned long long) flags, @@ -1738,6 +1786,7 @@ int64_t sys_clone(hv_vcpu_t vcpu, int snapshot_shm_fd = -1; bool siblings_quiesced = false; int64_t fail_rc = -LINUX_ENOMEM; + int clone_pidfd = -1; /* Quiesce sibling vCPUs for snapshot consistency. In multithreaded guests, * sibling vCPUs may be actively mutating guest memory during the fork @@ -2003,6 +2052,21 @@ int64_t sys_clone(hv_vcpu_t vcpu, log_error("clone: failed to commit child bookkeeping"); goto fail_snapshot; } + /* Publish the parent's pidfd after the child fd snapshot, but before the + * child can run. A failed allocation or copyout cancels the held child. + */ + if (want_pidfd) { + clone_pidfd = + clone_create_pidfd(g, child_guest_pid, child_host_pid, pidfd_gva); + if (clone_pidfd < 0) { + fail_rc = clone_pidfd; + goto fail_snapshot; + } + } + if (flags & LINUX_CLONE_PARENT_SETTID) { + int32_t tid = (int32_t) child_guest_pid; + (void) guest_write_small(g, ptid_gva, &tid, sizeof(tid)); + } uint8_t admission_ready = 1; if (fork_ipc_write_all(ipc_sock, &admission_ready, sizeof(admission_ready)) < 0) { @@ -2075,6 +2139,8 @@ int64_t sys_clone(hv_vcpu_t vcpu, return child_guest_pid; fail_snapshot: + if (clone_pidfd >= 0) + sys_close(clone_pidfd); proc_cancel_child(child_guest_pid); free(regions_snapshot); if (snapshot_shm_fd >= 0) @@ -2134,7 +2200,6 @@ struct linux_clone_args { #define CLONE_ARGS_SIZE_VER0 64 /* v5.3: first 8 fields (flags..tls) */ /* Unsupported clone3-only flags: reject early rather than silently ignoring. */ -#define LINUX_CLONE_PIDFD 0x00001000 #define LINUX_CLONE_INTO_CGROUP 0x200000000ULL int64_t sys_clone3(hv_vcpu_t vcpu, @@ -2204,13 +2269,9 @@ int64_t sys_clone3(hv_vcpu_t vcpu, /* Merge exit_signal into flags for sys_clone compatibility. clone3 moved * exit_signal out of the flags field; sys_clone expects it in the low byte. - * Safe because validation confirmed ca.flags low byte is zero. Strip - * CLONE_PIDFD before passing to sys_clone (which does not understand it). - * Pidfd creation happens after the clone returns. + * Validation confirmed ca.flags low byte is zero. */ - bool want_pidfd = (ca.flags & LINUX_CLONE_PIDFD) != 0; - uint64_t flags = - (ca.flags & ~(uint64_t) LINUX_CLONE_PIDFD) | ca.exit_signal; + uint64_t flags = ca.flags | ca.exit_signal; /* Compute child stack pointer. clone: child_stack is the TOP of the stack * (SP value). clone3: stack is the BOTTOM, stack_size is the length. SP = @@ -2231,28 +2292,7 @@ int64_t sys_clone3(hv_vcpu_t vcpu, (unsigned long long) ca.stack, (unsigned long long) ca.stack_size, (unsigned long long) ca.tls, (unsigned long long) cl_args_size); - int64_t ret = sys_clone(vcpu, g, flags, child_stack, ca.stack, - ca.stack + ca.stack_size, ca.parent_tid, ca.tls, - ca.child_tid, verbose); - - /* If clone succeeded and CLONE_PIDFD was requested, create a pidfd for the - * child and write the guest FD number to ca.pidfd. - */ - if (ret > 0 && want_pidfd && ca.pidfd != 0) { - /* A CLONE_VM child has no host pid of its own, so it resolves to -1 and - * its pidfd carries no monitor. - */ - int pfd = pidfd_create(g, ret, proc_resolve_guest_pid(ret)); - if (pfd >= 0) { - int32_t pfd32 = (int32_t) pfd; - if (guest_write_small(g, ca.pidfd, &pfd32, sizeof(pfd32)) < 0) { - /* GVA invalid; close the newly created pidfd. */ - fd_entry_t snap; - if (fd_snapshot_and_close(pfd, &snap)) - fd_cleanup_entry(pfd, &snap); - } - } - } - - return ret; + return sys_clone(vcpu, g, flags, child_stack, ca.stack, + ca.stack + ca.stack_size, ca.parent_tid, ca.pidfd, ca.tls, + ca.child_tid, verbose); } diff --git a/src/runtime/forkipc.h b/src/runtime/forkipc.h index 36e39285..ca851dd4 100644 --- a/src/runtime/forkipc.h +++ b/src/runtime/forkipc.h @@ -39,6 +39,7 @@ int64_t sys_clone(hv_vcpu_t vcpu, uint64_t stack_map_start, uint64_t stack_map_end, uint64_t ptid_gva, + uint64_t pidfd_gva, uint64_t tls, uint64_t ctid_gva, bool verbose); diff --git a/src/syscall/abi.h b/src/syscall/abi.h index 369bfda5..45eb3896 100644 --- a/src/syscall/abi.h +++ b/src/syscall/abi.h @@ -97,6 +97,11 @@ #define SYS_getitimer 102 #define SYS_setitimer 103 #define SYS_nanosleep 101 +#define SYS_timer_create 107 +#define SYS_timer_gettime 108 +#define SYS_timer_getoverrun 109 +#define SYS_timer_settime 110 +#define SYS_timer_delete 111 #define SYS_clock_gettime 113 #define SYS_clock_getres 114 #define SYS_clock_nanosleep 115 diff --git a/src/syscall/dispatch.tbl b/src/syscall/dispatch.tbl index 1f47ff0b..77220ab1 100644 --- a/src/syscall/dispatch.tbl +++ b/src/syscall/dispatch.tbl @@ -124,6 +124,11 @@ SYS_rt_tgsigqueueinfo sc_rt_tgsigqueueinfo 1 SYS_nanosleep sc_nanosleep 1 SYS_getitimer sc_getitimer 1 SYS_setitimer sc_setitimer 1 +SYS_timer_create sc_timer_create 0 +SYS_timer_gettime sc_timer_gettime 0 +SYS_timer_getoverrun sc_timer_getoverrun 0 +SYS_timer_settime sc_timer_settime 1 +SYS_timer_delete sc_timer_delete 0 SYS_clock_gettime sc_clock_gettime 0 SYS_clock_getres sc_clock_getres 0 SYS_clock_nanosleep sc_clock_nanosleep 1 diff --git a/src/syscall/fd.c b/src/syscall/fd.c index b905da4d..b3267b74 100644 --- a/src/syscall/fd.c +++ b/src/syscall/fd.c @@ -82,25 +82,12 @@ static int sfd_alloc_slot(const void *state, size_t count, size_t stride) * timerfd return an 8-byte counter of expirations. */ -/* Linux itimerspec for timerfd_settime/gettime */ -typedef struct { - int64_t it_interval_sec, it_interval_nsec, it_value_sec, it_value_nsec; -} linux_itimerspec_t; - /* Per-timerfd state (stored alongside the FD_TIMERFD entry) */ #define TIMERFD_MAX 32 #define LINUX_TFD_NONBLOCK LINUX_O_NONBLOCK #define LINUX_TFD_CLOEXEC LINUX_O_CLOEXEC #define LINUX_TFD_TIMER_ABSTIME 1 #define LINUX_TFD_TIMER_CANCEL_ON_SET 2 -#define LINUX_CLOCK_REALTIME 0 -#define LINUX_CLOCK_MONOTONIC 1 - -/* Linux CLOCK_BOOTTIME counts time including suspend; macOS has no equivalent. - * timerfd_settime treats non-REALTIME slots as MONOTONIC for ABSTIME - * conversion, which matches translate_clockid() in time.c. - */ -#define LINUX_CLOCK_BOOTTIME 7 static struct { int guest_fd; /* Guest fd (-1 if unused) */ @@ -1318,10 +1305,15 @@ int64_t signalfd_read(int guest_fd, memset(&info, 0, sizeof(info)); info.ssi_signo = (uint32_t) pending[i].signum; info.ssi_code = pending[i].si_code; - info.ssi_pid = (uint32_t) pending[i].si_pid; - info.ssi_uid = pending[i].si_uid; info.ssi_int = pending[i].si_int; info.ssi_ptr = pending[i].si_ptr; + if (info.ssi_code == LINUX_SI_TIMER) { + info.ssi_tid = (uint32_t) pending[i].si_pid; + info.ssi_overrun = pending[i].si_uid; + } else { + info.ssi_pid = (uint32_t) pending[i].si_pid; + info.ssi_uid = pending[i].si_uid; + } uint64_t off = i * sizeof(linux_signalfd_siginfo_t); if (guest_write_small(g, buf_gva + off, &info, sizeof(info)) < 0) { diff --git a/src/syscall/linux-wire.h b/src/syscall/linux-wire.h index 7c7e1410..a87d8d62 100644 --- a/src/syscall/linux-wire.h +++ b/src/syscall/linux-wire.h @@ -377,6 +377,22 @@ typedef struct { int64_t tv_sec, tv_nsec; } linux_timespec_t; +/* Linux clocks and POSIX timer flags. */ +#define LINUX_CLOCK_REALTIME 0 +#define LINUX_CLOCK_MONOTONIC 1 +#define LINUX_CLOCK_BOOTTIME 7 +#define LINUX_TIMER_ABSTIME 1 +#define LINUX_SIGEV_SIGNAL 0 +#define LINUX_SIGEV_NONE 1 +#define LINUX_SIGEV_THREAD_ID 4 + +/* Linux struct itimerspec, shared by POSIX timers and timerfd. */ +typedef struct { + int64_t it_interval_sec, it_interval_nsec, it_value_sec, it_value_nsec; +} linux_itimerspec_t; + +_Static_assert(sizeof(linux_itimerspec_t) == 32, "Linux itimerspec size"); + /* Linux struct timeval (aarch64). */ typedef struct { int64_t tv_sec, tv_usec; diff --git a/src/syscall/proc-pidfd.c b/src/syscall/proc-pidfd.c index ad24c839..3ed65e0c 100644 --- a/src/syscall/proc-pidfd.c +++ b/src/syscall/proc-pidfd.c @@ -165,6 +165,7 @@ int pidfd_create(guest_t *g, int64_t target_pid, pid_t host_pid) close(pfd[1]); return -LINUX_EMFILE; } + fd_publish_linux_flags(gfd, LINUX_O_CLOEXEC); pthread_mutex_lock(&pidfd_lock); pidfd_entry_t *entry = pidfd_find_free_entry(); diff --git a/src/syscall/signal.c b/src/syscall/signal.c index ccf62ad3..ef65e636 100644 --- a/src/syscall/signal.c +++ b/src/syscall/signal.c @@ -71,6 +71,7 @@ #include "syscall/fd.h" /* signalfd_notify */ #include "syscall/proc.h" /* proc_get_pid, proc_get_uid, SYSCALL_EXEC_HAPPENED */ #include "proved/sigframe.h" +#include "proved/timespec.h" #include "syscall/signal.h" #include "syscall/time.h" /* linux_timespec_valid, linux_timespec_to_ns_sat */ #include "syscall/wakeup-pipe.h" @@ -248,6 +249,59 @@ static guest_itimer_t guest_itimer; /* ITIMER_REAL -> SIGALRM */ static guest_itimer_t guest_itimer_virt; /* ITIMER_VIRTUAL -> SIGVTALRM */ static guest_itimer_t guest_itimer_prof; /* ITIMER_PROF -> SIGPROF */ +/* Bounded per-process storage; Linux does not inherit POSIX timers at fork. */ +#define POSIX_TIMER_MAX 64 +typedef enum { + POSIX_TIMER_NONE, + POSIX_TIMER_WAITING, + POSIX_TIMER_QUEUED, +} posix_timer_notification_t; + +typedef struct { + int32_t id, guest_clockid; + int signum; + int overruns, last_overrun; + clockid_t host_clock; + uint64_t value; + int64_t expiry_ns, interval_ns; + bool used; + posix_timer_notification_t notification; +} guest_posix_timer_t; + +/* sig_lock protects slots, IDs and queue positions. Release stores to the + * polling hint pair with acquire loads in posix_timer_check and + * signal_attention_needed; slot access still requires sig_lock. + */ +static guest_posix_timer_t posix_timers[POSIX_TIMER_MAX]; +/* Keep IDs unique so a deleted timer's queued signal cannot name a new timer. + */ +static int posix_next_id; +static int posix_timer_next_slot[LINUX_NSIG]; +static _Atomic bool posix_timers_need_poll; + +static guest_posix_timer_t *posix_timer_find_locked(int id) +{ + for (int i = 0; i < POSIX_TIMER_MAX; i++) { + if (posix_timers[i].used && posix_timers[i].id == id) + return &posix_timers[i]; + } + return NULL; +} + +static void posix_timer_dequeued_locked(signal_rt_info_t *info) +{ + if (info->si_code != LINUX_SI_TIMER) + return; + guest_posix_timer_t *timer = posix_timer_find_locked(info->si_pid); + if (!timer) + return; + /* Linux's timer ID and overrun share the siginfo pid/uid union slots. */ + info->si_uid = (uint32_t) timer->overruns; + timer->last_overrun = timer->overruns; + timer->overruns = 0; + timer->notification = POSIX_TIMER_NONE; +} + /* Default disposition table. Index 0 unused (signals are 1-based). */ static const sig_disposition_t default_dispositions[LINUX_NSIG + 1] = { [0] = SIG_DISP_IGN, /* Invalid signal 0 */ @@ -373,8 +427,10 @@ static bool signal_rt_dequeue_locked(signal_pending_t *p, return false; } + signal_rt_info_t info = p->rt_info[idx][p->rt_head[idx]]; + posix_timer_dequeued_locked(&info); if (out) - *out = p->rt_info[idx][p->rt_head[idx]]; + *out = info; p->rt_head[idx] = (uint8_t) ((p->rt_head[idx] + 1) % RT_SIGQUEUE_MAX); p->rt_queue[idx]--; if (p->rt_queue[idx] == 0) { @@ -427,6 +483,46 @@ static void shared_claim_release_locked(int signum) shared_claim_owner[signum - 1] = NULL; } +/* A negative ID removes all POSIX timer signals at exec. Keep other entries + * in order, including user signals with the same signal number. + */ +static void posix_timer_discard_pending_locked(signal_pending_t *p, int id) +{ + for (uint64_t bits = pending_load(&p->pending); bits; bits &= bits - 1) { + int signum = bit_ctz64(bits) + 1; + if (signum < LINUX_SIGRTMIN) { + int idx = signum - 1; + const signal_rt_info_t *info = &p->std_info[idx]; + if (!p->std_info_valid[idx] || info->si_code != LINUX_SI_TIMER || + (id >= 0 && info->si_pid != id)) + continue; + p->std_info_valid[idx] = false; + pending_clear(&p->pending, sig_bit(signum)); + } else { + int idx = signum - LINUX_SIGRTMIN; + int kept = 0; + for (int n = 0; n < p->rt_queue[idx]; n++) { + int slot = (p->rt_head[idx] + n) % RT_SIGQUEUE_MAX; + signal_rt_info_t info = p->rt_info[idx][slot]; + if (info.si_code == LINUX_SI_TIMER && + (id < 0 || info.si_pid == id)) + continue; + int dest = (p->rt_head[idx] + kept++) % RT_SIGQUEUE_MAX; + p->rt_info[idx][dest] = info; + } + if (kept == p->rt_queue[idx]) + continue; + p->rt_queue[idx] = kept; + if (!kept) { + p->rt_head[idx] = 0; + pending_clear(&p->pending, sig_bit(signum)); + } + } + if (p == &sig_state.shared) + shared_claim_release_locked(signum); + } +} + /* The shared set as the current thread may take it. */ static uint64_t shared_visible_locked(void) { @@ -575,9 +671,16 @@ void signal_reset_for_exec(void) thread_entry_t *t = current_thread; pthread_mutex_lock(&sig_lock); + posix_timer_discard_pending_locked(&sig_state.shared, -1); + if (t) + posix_timer_discard_pending_locked(&t->tpending, -1); + refresh_pending_hint_locked(); + memset(posix_timers, 0, sizeof(posix_timers)); + memset(posix_timer_next_slot, 0, sizeof(posix_timer_next_slot)); + atomic_store_explicit(&posix_timers_need_poll, false, memory_order_release); for (int i = 0; i < LINUX_NSIG; i++) { /* POSIX: handlers reset to SIG_DFL, except SIG_IGN stays SIG_IGN. - * Pending signals and signal mask are preserved across exec. + * Non-timer pending signals and the signal mask survive exec. */ if (sig_state.actions[i].sa_handler != LINUX_SIG_IGN) { sig_state.actions[i].sa_handler = LINUX_SIG_DFL; @@ -806,13 +909,13 @@ bool signal_attention_needed(void) if (hint != 0 && thread_signal_deliverable(hint)) return true; - /* Active guest itimers: even if no signal is queued YET, the timer can fire - * at any moment, and signal_check_timer needs an HVC #5 epilogue to notice - * it. Keep attention raised while any timer is armed. + /* signal_check_timer needs an HVC #5 epilogue to see expired timers or + * retry POSIX notifications waiting for queue space. */ if (atomic_load_explicit(&guest_itimer.active, memory_order_acquire) || atomic_load_explicit(&guest_itimer_virt.active, memory_order_acquire) || - atomic_load_explicit(&guest_itimer_prof.active, memory_order_acquire)) + atomic_load_explicit(&guest_itimer_prof.active, memory_order_acquire) || + atomic_load_explicit(&posix_timers_need_poll, memory_order_acquire)) return true; return false; } @@ -1153,6 +1256,7 @@ size_t signal_take_signalfd_exact(const signal_rt_info_t *expected, current.si_int != want->si_int || current.si_ptr != want->si_ptr) break; + posix_timer_dequeued_locked(¤t); sp->std_info_valid[signum - 1] = false; pending_clear(&sp->pending, bit); } @@ -1389,6 +1493,276 @@ void signal_get_itimer(struct timeval *value, struct timeval *interval) pthread_mutex_unlock(&sig_lock); } +static int64_t posix_timer_now(clockid_t clockid) +{ + struct timespec now; + clock_gettime(clockid, &now); + return timespec_to_ns_sat(now.tv_sec, now.tv_nsec); +} + +static void posix_timer_refresh_poll_hint_locked(void) +{ + bool needs_poll = false; + for (int i = 0; i < POSIX_TIMER_MAX; i++) { + const guest_posix_timer_t *timer = &posix_timers[i]; + needs_poll |= + timer->used && timer->signum && + (timer->expiry_ns || timer->notification == POSIX_TIMER_WAITING); + } + atomic_store_explicit(&posix_timers_need_poll, needs_poll, + memory_order_release); +} + +/* Count expired periods, then disarm or advance past now. */ +static uint64_t posix_timer_advance_locked(guest_posix_timer_t *timer) +{ + if (!timer->used || !timer->expiry_ns) + return 0; + int64_t now = posix_timer_now(timer->host_clock); + if (now < timer->expiry_ns) + return 0; + if (!timer->interval_ns) { + timer->expiry_ns = 0; + return 1; + } + + int64_t elapsed = now - timer->expiry_ns; + uint64_t count = 1 + (uint64_t) (elapsed / timer->interval_ns); + int64_t step = timer->interval_ns - elapsed % timer->interval_ns; + if (__builtin_add_overflow(now, step, &timer->expiry_ns)) + timer->expiry_ns = INT64_MAX; + return count; +} + +static void posix_timer_value_locked(guest_posix_timer_t *timer, + linux_itimerspec_t *value) +{ + /* Silent timers have no delivery work, so advance them only on query. */ + if (!timer->signum) + posix_timer_advance_locked(timer); + int64_t now = posix_timer_now(timer->host_clock); + int64_t remaining = timer->expiry_ns > now ? timer->expiry_ns - now : 0; + *value = (linux_itimerspec_t) { + .it_interval_sec = timer->interval_ns / NSEC_PER_SEC, + .it_interval_nsec = timer->interval_ns % NSEC_PER_SEC, + .it_value_sec = remaining / NSEC_PER_SEC, + .it_value_nsec = remaining % NSEC_PER_SEC, + }; +} + +/* signalfd peeks before consuming, so its queued copy needs current overruns. + */ +static void posix_timer_update_pending_locked(const guest_posix_timer_t *timer) +{ + signal_pending_t *shared = &sig_state.shared; + if (timer->signum < LINUX_SIGRTMIN) { + signal_rt_info_t *info = &shared->std_info[timer->signum - 1]; + if (info->si_code == LINUX_SI_TIMER && info->si_pid == timer->id) + info->si_uid = (uint32_t) timer->overruns; + return; + } + + int idx = timer->signum - LINUX_SIGRTMIN; + for (int n = 0; n < shared->rt_queue[idx]; n++) { + int slot = (shared->rt_head[idx] + n) % RT_SIGQUEUE_MAX; + signal_rt_info_t *info = &shared->rt_info[idx][slot]; + if (info->si_code == LINUX_SI_TIMER && info->si_pid == timer->id) + info->si_uid = (uint32_t) timer->overruns; + } +} + +static bool posix_timer_enqueue_waiting_locked(int signum) +{ + bool queued = false; + int start = posix_timer_next_slot[signum - 1]; + /* Resume after the last queued timer so fast periodic timers cannot + * keep taking queue space from another timer on the same signal. + */ + for (int offset = 0; offset < POSIX_TIMER_MAX; offset++) { + int i = (start + offset) % POSIX_TIMER_MAX; + guest_posix_timer_t *timer = &posix_timers[i]; + if (!timer->used || timer->signum != signum || + timer->notification != POSIX_TIMER_WAITING) + continue; + /* Each timer retains its notification until the shared queue has room, + * including when another timer holds the same standard signal. + */ + if (signum < LINUX_SIGRTMIN && + (pending_load(&sig_state.shared.pending) & sig_bit(signum))) + break; + if (signum >= LINUX_SIGRTMIN && + sig_state.shared.rt_queue[signum - LINUX_SIGRTMIN] >= + RT_SIGQUEUE_MAX) + break; + signal_rt_info_t info = { + .signum = timer->signum, + .si_code = LINUX_SI_TIMER, + .si_pid = timer->id, + .si_uid = (uint32_t) timer->overruns, + .si_int = (int32_t) timer->value, + .si_ptr = timer->value, + }; + signal_enqueue_locked(&sig_state.shared, timer->signum, &info); + timer->notification = POSIX_TIMER_QUEUED; + posix_timer_next_slot[signum - 1] = (i + 1) % POSIX_TIMER_MAX; + queued = true; + } + return queued; +} + +static void posix_timer_check(void) +{ + if (!atomic_load_explicit(&posix_timers_need_poll, memory_order_acquire)) + return; + uint64_t waiting = 0, notify = 0; + pthread_mutex_lock(&sig_lock); + for (int i = 0; i < POSIX_TIMER_MAX; i++) { + guest_posix_timer_t *timer = &posix_timers[i]; + if (!timer->used || !timer->signum) + continue; + uint64_t count = posix_timer_advance_locked(timer); + if (count) { + uint64_t missed = count; + if (timer->notification == POSIX_TIMER_NONE) { + timer->notification = POSIX_TIMER_WAITING; + missed--; + } + timer->overruns = missed > (uint64_t) (INT32_MAX - timer->overruns) + ? INT32_MAX + : timer->overruns + (int) missed; + } + if (timer->notification == POSIX_TIMER_QUEUED) + posix_timer_update_pending_locked(timer); + else if (timer->notification == POSIX_TIMER_WAITING) + waiting |= sig_bit(timer->signum); + } + for (uint64_t pending = waiting; pending; pending &= pending - 1) { + int signum = bit_ctz64(pending) + 1; + if (posix_timer_enqueue_waiting_locked(signum)) + notify |= sig_bit(signum); + } + posix_timer_refresh_poll_hint_locked(); + refresh_pending_hint_locked(); + pthread_mutex_unlock(&sig_lock); + for (uint64_t pending = notify; pending; pending &= pending - 1) + signalfd_notify(bit_ctz64(pending) + 1); + if (notify) + attention_raise(); +} + +int signal_posix_timer_create(int clockid, + int notify, + int signum, + uint64_t value, + bool default_event) +{ + if (clockid != LINUX_CLOCK_REALTIME && clockid != LINUX_CLOCK_MONOTONIC && + clockid != LINUX_CLOCK_BOOTTIME) + return -LINUX_EINVAL; + if (notify == LINUX_SIGEV_THREAD_ID) + return -LINUX_ENOSYS; + if (notify != LINUX_SIGEV_SIGNAL && notify != LINUX_SIGEV_NONE) + return -LINUX_EINVAL; + if (notify == LINUX_SIGEV_SIGNAL && !RANGE_CHECK(signum, 1, LINUX_NSIG)) + return -LINUX_EINVAL; + int ret = -LINUX_EAGAIN; + pthread_mutex_lock(&sig_lock); + for (int i = 0; i < POSIX_TIMER_MAX && posix_next_id < INT32_MAX; i++) { + if (posix_timers[i].used) + continue; + ret = posix_next_id++; + posix_timers[i] = (guest_posix_timer_t) { + .id = ret, + .guest_clockid = clockid, + .host_clock = CLOCK_MONOTONIC, + .signum = notify == LINUX_SIGEV_SIGNAL ? signum : 0, + .value = default_event ? (uint64_t) ret : value, + .used = true, + }; + break; + } + pthread_mutex_unlock(&sig_lock); + return ret; +} + +int signal_posix_timer_get(int id, linux_itimerspec_t *value) +{ + posix_timer_check(); + pthread_mutex_lock(&sig_lock); + guest_posix_timer_t *timer = posix_timer_find_locked(id); + if (timer) + posix_timer_value_locked(timer, value); + pthread_mutex_unlock(&sig_lock); + return timer ? 0 : -LINUX_EINVAL; +} + +int signal_posix_timer_set(int id, + int flags, + const linux_itimerspec_t *value, + linux_itimerspec_t *old) +{ + posix_timer_check(); + pthread_mutex_lock(&sig_lock); + guest_posix_timer_t *timer = posix_timer_find_locked(id); + if (!timer) { + pthread_mutex_unlock(&sig_lock); + return -LINUX_EINVAL; + } + posix_timer_value_locked(timer, old); + /* Queue removal and rearming share sig_lock, so an old notification + * cannot consume the new arm's overrun state. + */ + posix_timer_discard_pending_locked(&sig_state.shared, id); + timer->notification = POSIX_TIMER_NONE; + timer->overruns = 0; + timer->last_overrun = 0; + bool absolute = (flags & LINUX_TIMER_ABSTIME) != 0; + /* Relative realtime timers must ignore wall-clock adjustments. */ + timer->host_clock = absolute && timer->guest_clockid == LINUX_CLOCK_REALTIME + ? CLOCK_REALTIME + : CLOCK_MONOTONIC; + int64_t value_ns = + timespec_to_ns_sat(value->it_value_sec, value->it_value_nsec); + timer->expiry_ns = value_ns; + if (value_ns && !absolute && + __builtin_add_overflow(posix_timer_now(timer->host_clock), value_ns, + &timer->expiry_ns)) + timer->expiry_ns = INT64_MAX; + timer->interval_ns = value_ns ? timespec_to_ns_sat(value->it_interval_sec, + value->it_interval_nsec) + : 0; + bool notify = value_ns && timer->signum; + posix_timer_refresh_poll_hint_locked(); + refresh_pending_hint_locked(); + pthread_mutex_unlock(&sig_lock); + if (notify) + attention_raise(); + return 0; +} + +int signal_posix_timer_delete(int id) +{ + pthread_mutex_lock(&sig_lock); + guest_posix_timer_t *timer = posix_timer_find_locked(id); + if (timer) { + posix_timer_discard_pending_locked(&sig_state.shared, id); + memset(timer, 0, sizeof(*timer)); + } + posix_timer_refresh_poll_hint_locked(); + refresh_pending_hint_locked(); + pthread_mutex_unlock(&sig_lock); + return timer ? 0 : -LINUX_EINVAL; +} + +int signal_posix_timer_overrun(int id) +{ + pthread_mutex_lock(&sig_lock); + guest_posix_timer_t *timer = posix_timer_find_locked(id); + int ret = timer ? timer->last_overrun : -LINUX_EINVAL; + pthread_mutex_unlock(&sig_lock); + return ret; +} + /* Check a single timer; if expired, re-arm or deactivate, return signal to * queue. Must be called with sig_lock held. * @@ -1427,6 +1801,7 @@ static bool check_one_timer(guest_itimer_t *timer, const struct timeval *now) */ static void signal_check_timers(bool cpu_timers) { + posix_timer_check(); if (!atomic_load_explicit(&guest_itimer.active, memory_order_acquire) && (!cpu_timers || (!atomic_load_explicit(&guest_itimer_virt.active, memory_order_acquire) && @@ -1742,6 +2117,7 @@ static int signal_claim_waking_locked(uint64_t blocked) bool signal_claim_interruption(void) { + posix_timer_check(); /* Same lock-free fast path as signal_pending(). */ uint64_t hint = atomic_load_explicit(&sig_pending_hint, memory_order_acquire); @@ -1924,6 +2300,7 @@ static int sigtimedwait_try_dequeue(uint64_t mask, signal_rt_info_t *info_out) signal_rt_dequeue_locked(src, signum, info_out); } else { *info_out = signal_standard_peek_locked(src, signum); + posix_timer_dequeued_locked(info_out); src->std_info_valid[signum - 1] = false; pending_clear(&src->pending, sig_bit(signum)); } @@ -1976,6 +2353,7 @@ int64_t signal_rt_sigtimedwait(guest_t *g, while (1) { signal_rt_info_t info; + posix_timer_check(); int signum = sigtimedwait_try_dequeue(mask, &info); if (signum > 0) { /* Populate guest siginfo_t if the caller wants it. */ @@ -2793,6 +3171,7 @@ static int signal_deliver_one(hv_vcpu_t vcpu, guest_t *g, int *exit_code) signal_rt_dequeue_locked(src, signum, &rt_info); } else { rt_info = signal_standard_peek_locked(src, signum); + posix_timer_dequeued_locked(&rt_info); src->std_info_valid[signum - 1] = false; pending_clear(&src->pending, sig_bit(signum)); } diff --git a/src/syscall/signal.h b/src/syscall/signal.h index 25083d0b..909b4de7 100644 --- a/src/syscall/signal.h +++ b/src/syscall/signal.h @@ -22,6 +22,7 @@ #include #include #include "core/guest.h" +#include "syscall/linux-wire.h" #include "linux-limits.h" /* Linux signal numbers (1-based, matching kernel). */ @@ -117,6 +118,7 @@ typedef struct { /* si_code values */ #define LINUX_SI_USER 0 +#define LINUX_SI_TIMER (-2) /* si_code values for SIGTRAP */ #define LINUX_TRAP_BRKPT 1 /* Process breakpoint (BRK instruction) */ @@ -709,3 +711,20 @@ void signal_check_timer(void); * host call spends none. */ void signal_check_timer_real(void); + +/* Process-wide POSIX timers with REALTIME/MONOTONIC clocks and + * SIGEV_SIGNAL/SIGEV_NONE notification. BOOTTIME uses monotonic time, as in + * translate_clockid. These helpers acquire sig_lock. + */ +int signal_posix_timer_create(int clockid, + int notify, + int signum, + uint64_t value, + bool default_event); +int signal_posix_timer_get(int id, linux_itimerspec_t *value); +int signal_posix_timer_set(int id, + int flags, + const linux_itimerspec_t *value, + linux_itimerspec_t *old); +int signal_posix_timer_delete(int id); +int signal_posix_timer_overrun(int id); diff --git a/src/syscall/syscall.c b/src/syscall/syscall.c index 572d23a9..0902245c 100644 --- a/src/syscall/syscall.c +++ b/src/syscall/syscall.c @@ -357,6 +357,11 @@ SC_FORWARD(sc_nanosleep, sys_nanosleep(g, x0, x1)) SC_FORWARD(sc_clock_nanosleep, sys_clock_nanosleep(g, (int) x0, (int) x1, x2, x3)) SC_FORWARD(sc_gettimeofday, sys_gettimeofday(g, x0, x1)) SC_FORWARD(sc_setitimer, sys_setitimer(g, (int) x0, x1, x2)) +SC_FORWARD(sc_timer_create, sys_timer_create(g, (int) x0, x1, x2)) +SC_FORWARD(sc_timer_gettime, sys_timer_gettime(g, (int) x0, x1)) +SC_FORWARD(sc_timer_getoverrun, signal_posix_timer_overrun((int) x0)) +SC_FORWARD(sc_timer_settime, sys_timer_settime(g, (int) x0, (int) x1, x2, x3)) +SC_FORWARD(sc_timer_delete, signal_posix_timer_delete((int) x0)) SC_FORWARD(sc_getitimer, sys_getitimer(g, (int) x0, x1)) SC_FORWARD(sc_times, sys_times(g, x0)) @@ -2304,7 +2309,7 @@ static int64_t sc_clone(guest_t *g, (unsigned long long) x0, (unsigned long long) x1, (unsigned long long) x2, (unsigned long long) x3, (unsigned long long) x4); - return sys_clone(current_thread->vcpu, g, x0, x1, 0, 0, x2, x3, x4, + return sys_clone(current_thread->vcpu, g, x0, x1, 0, 0, x2, x2, x3, x4, verbose); } diff --git a/src/syscall/time.c b/src/syscall/time.c index ed41cf31..99e71d83 100644 --- a/src/syscall/time.c +++ b/src/syscall/time.c @@ -97,6 +97,70 @@ static struct timespec ns_to_host_timespec(int64_t ns) }; } +/* Linux aarch64 sigevent. Thread notification fields are unsupported. */ +typedef struct { + uint64_t value; + int32_t signum, notify; + int32_t reserved[12]; +} linux_sigevent_t; + +_Static_assert(sizeof(linux_sigevent_t) == 64, + "Linux sigevent must be 64 bytes"); + +int64_t sys_timer_create(guest_t *g, + int clockid, + uint64_t event_gva, + uint64_t id_gva) +{ + linux_sigevent_t event = { + .signum = LINUX_SIGALRM, + .notify = LINUX_SIGEV_SIGNAL, + }; + if (event_gva && guest_read(g, event_gva, &event, sizeof(event)) < 0) + return -LINUX_EFAULT; + int32_t id = signal_posix_timer_create(clockid, event.notify, event.signum, + event.value, event_gva == 0); + if (id < 0) + return id; + if (guest_write_small(g, id_gva, &id, sizeof(id)) < 0) { + signal_posix_timer_delete(id); + return -LINUX_EFAULT; + } + return 0; +} + +int64_t sys_timer_gettime(guest_t *g, int id, uint64_t value_gva) +{ + linux_itimerspec_t value; + int ret = signal_posix_timer_get(id, &value); + if (ret < 0) + return ret; + if (guest_write(g, value_gva, &value, sizeof(value)) < 0) + return -LINUX_EFAULT; + return 0; +} + +int64_t sys_timer_settime(guest_t *g, + int id, + int flags, + uint64_t value_gva, + uint64_t old_gva) +{ + linux_itimerspec_t value, old; + if (guest_read(g, value_gva, &value, sizeof(value)) < 0) + return -LINUX_EFAULT; + if ((flags & ~LINUX_TIMER_ABSTIME) || + !timespec_valid(value.it_value_sec, value.it_value_nsec) || + !timespec_valid(value.it_interval_sec, value.it_interval_nsec)) + return -LINUX_EINVAL; + int ret = signal_posix_timer_set(id, flags, &value, &old); + if (ret < 0) + return ret; + if (old_gva && guest_write(g, old_gva, &old, sizeof(old)) < 0) + return -LINUX_EFAULT; + return 0; +} + static int write_remaining_sleep(guest_t *g, uint64_t rem_gva, int64_t remaining_ns) diff --git a/src/syscall/time.h b/src/syscall/time.h index 293f973e..fd36d215 100644 --- a/src/syscall/time.h +++ b/src/syscall/time.h @@ -44,3 +44,13 @@ int64_t sys_setitimer(guest_t *g, uint64_t new_gva, uint64_t old_gva); int64_t sys_getitimer(guest_t *g, int which, uint64_t val_gva); +int64_t sys_timer_create(guest_t *g, + int clockid, + uint64_t event_gva, + uint64_t id_gva); +int64_t sys_timer_gettime(guest_t *g, int id, uint64_t value_gva); +int64_t sys_timer_settime(guest_t *g, + int id, + int flags, + uint64_t value_gva, + uint64_t old_gva); diff --git a/tests/manifest.txt b/tests/manifest.txt index 5a55653d..26fa8405 100644 --- a/tests/manifest.txt +++ b/tests/manifest.txt @@ -113,6 +113,8 @@ test-stress # diff=skip test-mprotect-mt # diff=skip [section] Signal + thread tests +test-posix-timers +test-posix-timers elfuse test-signal-thread test-signal-in-shim # diff=skip test-shim-sigreturn-x8 diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 0aa6894f..215a2be0 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -723,6 +723,12 @@ run_unit_tests() test_rc "$runner" "test-pidfd" 0 "$bindir/test-pidfd" printf "\nSignal tests\n" + test_check "$runner" "test-posix-timers" "0 failed" "$bindir/test-posix-timers" + if [ "$runner" = "run_elfuse" ]; then + # Pins unsupported thread notification and current Linux timer cleanup. + test_check "$runner" "test-posix-timers-elfuse" "0 failed" \ + "$bindir/test-posix-timers" elfuse + fi test_check "$runner" "test-signal" "PASS|0 failed" "$bindir/test-signal" test_check "$runner" "test-signal-thread" "PASS|0 failed" "$bindir/test-signal-thread" test_check "$runner" "test-signal-in-shim" "0 failed" \ diff --git a/tests/test-pidfd.c b/tests/test-pidfd.c index 89d686f2..f6a5dba6 100644 --- a/tests/test-pidfd.c +++ b/tests/test-pidfd.c @@ -29,6 +29,7 @@ #define CLONE_PIDFD 0x00001000 #define CLONE_VM 0x00000100 +#define CLONE_PARENT_SETTID 0x00100000 #define __NR_nanosleep 101 #define SIGCHLD 17 @@ -46,6 +47,26 @@ struct linux_pollfd { short events, revents; }; +static void test_clone_failure(uint64_t output, long error) +{ + struct clone_args ca = { + .flags = CLONE_PIDFD, + .pidfd = output, + .exit_signal = SIGCHLD, + }; + long child = raw_syscall2(__NR_clone3, (long) &ca, sizeof(ca)); + if (child == 0) { + raw_syscall1(__NR_exit, 42); + __builtin_unreachable(); + } + if (child > 0) + raw_syscall4(__NR_wait4, child, 0, 0, 0); + EXPECT_EQ(child, error, "wrong clone3 error"); + TEST("failed clone leaves no child"); + EXPECT_RAW_ERRNO(raw_syscall4(__NR_wait4, -1, 0, 1, 0), -ECHILD, + "failed clone left a child"); +} + int main(void) { printf("test-pidfd: pidfd subsystem\n"); @@ -95,7 +116,31 @@ int main(void) EXPECT_TRUE(r == -38 /* LINUX_ENOSYS */, "expected -ENOSYS"); } - /* clone3 with CLONE_PIDFD */ + TEST("clone3 rejects null pidfd output"); + test_clone_failure(0, -EFAULT); + TEST("clone3 rejects unmapped pidfd output"); + test_clone_failure(UINT64_MAX - 4095, -EFAULT); + + /* Fill elfuse's bounded pidfd table before clone creates its child. */ + { + long fds[32]; + int count = 0; + while (count < 32) { + long fd = raw_syscall2(__NR_pidfd_open, pid, 0); + if (fd < 0) + break; + fds[count++] = fd; + } + TEST("failed clones release pidfds"); + EXPECT_EQ(count, 32, "failed clone leaked a pidfd"); + int32_t output = -1; + TEST("clone3 cancels on pidfd allocation failure"); + test_clone_failure((uint64_t) &output, -EMFILE); + for (int i = 0; i < count; i++) + raw_syscall1(__NR_close, fds[i]); + } + + /* clone3 with separate CLONE_PIDFD and CLONE_PARENT_SETTID outputs. */ TEST("clone3 CLONE_PIDFD"); { /* Allocate stack for child */ @@ -107,9 +152,11 @@ int main(void) } int32_t child_pidfd = -1; + int32_t parent_tid = -1; struct clone_args ca = {0}; - ca.flags = CLONE_PIDFD; + ca.flags = CLONE_PIDFD | CLONE_PARENT_SETTID; ca.pidfd = (uint64_t) &child_pidfd; + ca.parent_tid = (uint64_t) &parent_tid; ca.exit_signal = SIGCHLD; ca.stack = (uint64_t) stack; ca.stack_size = 65536; @@ -126,6 +173,8 @@ int main(void) } EXPECT_TRUE(child_pidfd >= 0, "pidfd not written"); + TEST("clone3 writes a separate parent TID"); + EXPECT_EQ(parent_tid, child, "parent TID not written"); /* Poll the pidfd for child exit (with 1s timeout) */ TEST("poll pidfd for exit"); diff --git a/tests/test-posix-timers.c b/tests/test-posix-timers.c new file mode 100644 index 00000000..e49ac69c --- /dev/null +++ b/tests/test-posix-timers.c @@ -0,0 +1,356 @@ +/* + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +static int passes, fails; + +static void require(int ok, const char *message) +{ + if (!ok) { + perror(message); + exit(2); + } +} + +static sigset_t signal_set(int signum) +{ + sigset_t set; + sigemptyset(&set); + sigaddset(&set, signum); + return set; +} + +static timer_t create_timer(int signum, int payload) +{ + struct sigevent event = { + .sigev_notify = SIGEV_SIGNAL, + .sigev_signo = signum, + .sigev_value.sival_int = payload, + }; + timer_t timer; + require(timer_create(CLOCK_MONOTONIC, &event, &timer) == 0, "timer_create"); + return timer; +} + +static void arm_timer(timer_t timer, int periodic) +{ + struct itimerspec value = { + .it_value.tv_nsec = 10000000, + .it_interval.tv_nsec = periodic ? 100000000 : 0, + }; + require(timer_settime(timer, 0, &value, NULL) == 0, "timer_settime"); +} + +static void expire_timers(void) +{ + struct timespec delay = {.tv_nsec = 350000000}; + require(nanosleep(&delay, NULL) == 0, "nanosleep"); +} + +static int take_signal(int signum, siginfo_t *info) +{ + sigset_t set = signal_set(signum); + struct timespec timeout = {.tv_nsec = 100000000}; + return sigtimedwait(&set, info, &timeout); +} + +static void test_create(void) +{ + /* Use the kernel ABI: libc can reject a bad signal before timer_create. */ + int signals[] = {0, -1, 65, INT_MAX, 1, 64}; + for (unsigned i = 0; i < sizeof(signals) / sizeof(signals[0]); i++) { + struct sigevent event = { + .sigev_notify = SIGEV_SIGNAL, + .sigev_signo = signals[i], + }; + int id = -1; + long rc = syscall(SYS_timer_create, CLOCK_MONOTONIC, &event, &id); + int error = errno; + if (rc == 0) + syscall(SYS_timer_delete, id); + TEST("timer signal range is 1..64"); + EXPECT_TRUE(signals[i] > 0 && signals[i] <= 64 + ? rc == 0 + : rc == -1 && error == EINVAL, + "wrong timer_create result"); + } + + struct sigevent event = {.sigev_notify = SIGEV_NONE}; + timer_t timer; + TEST("CLOCK_BOOTTIME is accepted"); + int rc = timer_create(CLOCK_BOOTTIME, &event, &timer); + EXPECT_EQ(rc, 0, "CLOCK_BOOTTIME rejected"); + if (rc == 0) + timer_delete(timer); + + require(timer_create(CLOCK_MONOTONIC, &event, &timer) == 0, + "create silent timer"); + arm_timer(timer, 1); + expire_timers(); + struct itimerspec value; + TEST("silent periodic timer advances"); + EXPECT_TRUE(timer_gettime(timer, &value) == 0 && + value.it_value.tv_sec == 0 && value.it_value.tv_nsec > 0 && + value.it_value.tv_nsec <= 100000000, + "silent timer did not advance"); + struct itimerspec stop = {.it_interval.tv_sec = 1}; + require(timer_settime(timer, 0, &stop, &value) == 0, "disarm timer"); + TEST("settime returns the old interval"); + EXPECT_EQ(value.it_interval.tv_nsec, 100000000, "old interval lost"); + TEST("disarm clears value and interval"); + EXPECT_TRUE(timer_gettime(timer, &value) == 0 && + value.it_value.tv_sec == 0 && value.it_value.tv_nsec == 0 && + value.it_interval.tv_sec == 0 && + value.it_interval.tv_nsec == 0, + "disarmed timer retains an interval"); + timer_delete(timer); + + timer = create_timer(SIGUSR1, 71); + struct itimerspec absolute = {0}; + require(clock_gettime(CLOCK_MONOTONIC, &absolute.it_value) == 0, + "clock_gettime"); + absolute.it_value.tv_nsec += 10000000; + if (absolute.it_value.tv_nsec >= 1000000000) { + absolute.it_value.tv_sec++; + absolute.it_value.tv_nsec -= 1000000000; + } + require(timer_settime(timer, TIMER_ABSTIME, &absolute, NULL) == 0, + "arm absolute timer"); + expire_timers(); + siginfo_t info; + TEST("absolute timer carries its payload"); + EXPECT_TRUE(take_signal(SIGUSR1, &info) == SIGUSR1 && + info.si_code == SI_TIMER && info.si_value.sival_int == 71, + "absolute timer notification missing"); + TEST("one-shot overrun is zero"); + EXPECT_EQ(timer_getoverrun(timer), 0, "unexpected overrun"); + timer_delete(timer); +} + +static void test_notifications(int signum, int use_fd, int periodic) +{ + int count = signum == SIGUSR1 ? 2 : 33; + sigset_t set = signal_set(signum); + int fd = use_fd ? signalfd(-1, &set, SFD_NONBLOCK) : -1; + require(!use_fd || fd >= 0, "signalfd"); + timer_t timers[33]; + for (int i = 0; i < count; i++) { + timers[i] = create_timer(signum, i + 1); + arm_timer(timers[i], periodic); + } + expire_timers(); + uint64_t seen = 0; + int received = 0, metadata_ok = 1; + while (received < count) { + int payload, overrun; + if (use_fd) { + struct signalfd_siginfo info; + ssize_t n = read(fd, &info, sizeof(info)); + if (n == -1 && errno == EAGAIN) + break; + require(n == sizeof(info), "read signalfd"); + metadata_ok &= + info.ssi_signo == signum && info.ssi_code == SI_TIMER; + payload = info.ssi_int; + overrun = info.ssi_overrun; + } else { + siginfo_t info; + if (take_signal(signum, &info) != signum) + break; + metadata_ok &= info.si_code == SI_TIMER; + payload = info.si_value.sival_int; + overrun = info.si_overrun; + } + require(payload > 0 && payload <= count, "timer payload"); + uint64_t bit = UINT64_C(1) << (payload - 1); + metadata_ok &= !(seen & bit); + seen |= bit; + metadata_ok &= periodic ? overrun > 0 : overrun == 0; + metadata_ok &= timer_getoverrun(timers[payload - 1]) == overrun; + received++; + } + printf(" signal=%d signalfd=%d periodic=%d\n", signum, use_fd, periodic); + TEST("each timer delivers once"); + EXPECT_EQ(received, count, "timer notification lost"); + TEST("payloads and overruns agree"); + EXPECT_TRUE(metadata_ok, "wrong payload or overrun"); + for (int i = 0; i < count; i++) + timer_delete(timers[i]); + if (fd >= 0) + close(fd); +} + +static void test_fairness(int signum) +{ + int count = signum == SIGUSR1 ? 3 : 33; + sigset_t set = signal_set(signum); + int fd = signalfd(-1, &set, SFD_NONBLOCK); + require(fd >= 0, "signalfd"); + timer_t timers[33]; + for (int i = 0; i < count; i++) { + timers[i] = create_timer(signum, i + 1); + struct itimerspec value = { + .it_value.tv_nsec = 1000000, + .it_interval.tv_nsec = i + 1 < count ? 1 : 0, + }; + require(timer_settime(timers[i], 0, &value, NULL) == 0, + "timer_settime"); + } + struct timespec delay = {.tv_nsec = 10000000}; + require(nanosleep(&delay, NULL) == 0, "nanosleep"); + int seen = 0; + for (int i = 0; i < 2 * count && !seen; i++) { + struct signalfd_siginfo info; + ssize_t n = read(fd, &info, sizeof(info)); + if (n == -1 && errno == EAGAIN) + break; + require(n == sizeof(info), "read signalfd"); + seen = info.ssi_code == SI_TIMER && info.ssi_int == count; + } + printf(" signal=%d competing_timers=%d\n", signum, count - 1); + TEST("fast periodic timers do not starve a one-shot"); + EXPECT_TRUE(seen, "one-shot notification starved"); + for (int i = 0; i < count; i++) + timer_delete(timers[i]); + siginfo_t info; + while (take_signal(signum, &info) == signum) + ; + close(fd); +} + +static void test_cleanup(int signum, int rearm) +{ + timer_t timer = create_timer(signum, 99); + union sigval first = {.sival_int = 10}, last = {.sival_int = 20}; + siginfo_t info; + if (signum >= SIGRTMIN) { + /* Exercise cleanup with the retained entries across the ring's end. */ + for (int i = 0; i < 32; i++) + require(sigqueue(getpid(), signum, first) == 0, + "queue first signal"); + for (int i = 0; i < 31; i++) + require(take_signal(signum, &info) == signum, + "advance signal queue"); + } + arm_timer(timer, 1); + expire_timers(); + if (signum >= SIGRTMIN) + require(sigqueue(getpid(), signum, last) == 0, "queue last signal"); + if (rearm) { + struct itimerspec value = {.it_value.tv_sec = 60}; + require(timer_settime(timer, 0, &value, NULL) == 0, "rearm timer"); + } else { + require(timer_delete(timer) == 0, "delete timer"); + } + if (signum < SIGRTMIN) + require(sigqueue(getpid(), signum, last) == 0, "queue last signal"); + if (signum >= SIGRTMIN) { + TEST("cleanup preserves earlier signal"); + EXPECT_TRUE(take_signal(signum, &info) == signum && + info.si_code == SI_QUEUE && + info.si_value.sival_int == 10, + "earlier signal lost or reordered"); + } + TEST(rearm ? "rearm discards stale notification" + : "delete discards stale notification"); + EXPECT_TRUE(take_signal(signum, &info) == signum && + info.si_code == SI_QUEUE && info.si_value.sival_int == 20, + "stale timer signal or lost user signal"); + if (rearm) { + arm_timer(timer, 0); + expire_timers(); + TEST("new arm starts with zero overruns"); + EXPECT_TRUE(take_signal(signum, &info) == signum && + info.si_code == SI_TIMER && info.si_overrun == 0 && + timer_getoverrun(timer) == 0, + "old arm changed new arm accounting"); + timer_delete(timer); + } + while (take_signal(signum, &info) == signum) + ; +} + +static void test_exec(const char *path) +{ + pid_t child = fork(); + require(child >= 0, "fork"); + if (child == 0) { + timer_t standard = create_timer(SIGUSR1, 1); + timer_t realtime = create_timer(SIGRTMIN, 2); + arm_timer(standard, 0); + arm_timer(realtime, 0); + expire_timers(); + union sigval value = {.sival_int = 42}; + require(sigqueue(getpid(), SIGRTMIN, value) == 0, "queue user signal"); + execl(path, path, "exec-child", NULL); + _exit(2); + } + int status; + require(waitpid(child, &status, 0) == child, "waitpid"); + TEST("exec drops timers, keeps user signal"); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0, + "exec retained a timer signal or lost the user signal"); +} + +int main(int argc, char **argv) +{ + setvbuf(stdout, NULL, _IONBF, 0); + sigset_t blocked = signal_set(SIGUSR1); + sigaddset(&blocked, SIGRTMIN); + require(sigprocmask(SIG_BLOCK, &blocked, NULL) == 0, "block signals"); + if (argc > 1 && strcmp(argv[1], "exec-child") == 0) { + siginfo_t info; + int ok = take_signal(SIGRTMIN, &info) == SIGRTMIN && + info.si_code == SI_QUEUE && info.si_value.sival_int == 42; + sigprocmask(SIG_UNBLOCK, &blocked, NULL); + return !ok; + } + if (argc > 1 && strcmp(argv[1], "elfuse") == 0) { + /* Older Linux kernels keep a queued signal after delete or rearm. */ + for (int rearm = 0; rearm < 2; rearm++) { + test_cleanup(SIGUSR1, rearm); + test_cleanup(SIGRTMIN, rearm); + } + struct sigevent event = { + .sigev_notify = SIGEV_THREAD_ID, + .sigev_signo = SIGUSR1, + }; +#ifdef __GLIBC__ + event._sigev_un._tid = getpid(); +#else + event.sigev_notify_thread_id = getpid(); +#endif + int id; + TEST("thread notification reports ENOSYS"); + EXPECT_ERRNO(syscall(SYS_timer_create, CLOCK_MONOTONIC, &event, &id), + ENOSYS, "unsupported notification misreported"); + } else { + test_create(); + for (int periodic = 0; periodic < 2; periodic++) { + for (int use_fd = 0; use_fd < 2; use_fd++) { + test_notifications(SIGUSR1, use_fd, periodic); + test_notifications(SIGRTMIN, use_fd, periodic); + } + } + test_fairness(SIGUSR1); + test_fairness(SIGRTMIN); + test_exec(argv[0]); + } + SUMMARY("test-posix-timers"); + return fails != 0; +}