Skip to content

Honor futex_waitv clockid and wake without polling#9

Merged
jserv merged 1 commit into
mainfrom
futex-waitv
May 5, 2026
Merged

Honor futex_waitv clockid and wake without polling#9
jserv merged 1 commit into
mainfrom
futex-waitv

Conversation

@jserv

@jserv jserv commented May 5, 2026

Copy link
Copy Markdown
Contributor

futex_waitv (SYS 449) is SYSCALL_DEFINE5 in the kernel: (waiters, nr_futexes, flags, timeout, clockid). The dispatch wrapper sc_futex_waitv was forwarding only x0..x3, dropping x4. The implementation hardcoded a CLOCK_MONOTONIC->CLOCK_REALTIME deadline conversion, so a guest asking for CLOCK_REALTIME got monotonic semantics regardless of what it passed. Wire x4 through and give sys_futex_waitv a clockid parameter. Validate against CLOCK_REALTIME or CLOCK_MONOTONIC when timeout is non-NULL (Linux returns EINVAL otherwise) and branch the absolute-deadline conversion on clockid.

The pre-existing waitv loop slept on a private cond that no wake site ever signalled; forward progress relied on a 50ms poll. Add optional group_lock / group_cond pointers to futex_waiter_t plus a futex_waiter_notify_group helper. Each wake site (futex_wake, futex_requeue, futex_wake_op both passes, futex_unlock_pi) calls it after marking woken=1 under the bucket lock. waitv blocks on shared.cond directly; the loop's bounded sleep is now 500ms, kept only so exit_group and timeout edges are still observed when no signal arrives.

Lock ordering is bucket -> group_lock; futex_waiter_notify_group is only called by wake sites that already hold the bucket lock, and the waitv thread never holds shared.lock while taking a bucket lock. Stack lifetime of shared.lock and shared.cond is protected by the bucket-lock pairing in waitv_unlink, which synchronizes with every wake's notify_group before the destroys run.

Locked in by tests/test-futex-waitv.c (19 cases): single-element wake returning index 0, multi-element wake returning the woken index, EAGAIN on stale val, eight EINVAL paths covering nr=0, nr>128, top-level flags, reserved!=0, element flags reserved bits, size!=U32, malformed CLOCK_REALTIME / CLOCK_MONOTONIC nsec, unaligned uaddr, NULL waiters_gva, bad clockid with timeout; two EFAULT paths via PROT_NONE pages; the inclusive nr_futexes==128 boundary; and ETIMEDOUT under both CLOCK_MONOTONIC and CLOCK_REALTIME deadlines. The CLOCK_REALTIME case exists specifically to catch a regression of the dropped-clockid bug. Verified against Linux ground truth via tests/qemu-runner.sh; all 19 cases match.


Summary by cubic

Fixes futex_waitv to honor the clockid argument and switches waitv to signal-driven wakeups, removing the 50ms poll and matching Linux behavior. This improves correctness (timeouts) and reduces wake latency.

  • Bug Fixes

    • Forward clockid to sys_futex_waitv; support CLOCK_REALTIME and CLOCK_MONOTONIC, return EINVAL for others when a timeout is provided.
    • Convert absolute deadlines based on the selected clock; validate timespec (EINVAL) and faulting pointers (EFAULT) to mirror Linux.
    • Validate per-element fields (reserved bits, size U32, 4-byte alignment), reject NULL waiters, and enforce 1..128 nr_futexes.
  • Refactors

    • Replace polling with group-condition signaling from all wake sites to unblock waitv; keep a 500ms bounded sleep to observe exit/timeouts.
    • Strengthen waiter unlinking to handle requeue races and enforce lock order (bucket -> group lock).
    • Add tests/test-futex-waitv (19 cases) with a Makefile rule using -lpthread, and register it in tests/manifest.txt.

Written for commit 1423b4a. Summary will update on new commits.

futex_waitv (SYS 449) is SYSCALL_DEFINE5 in the kernel: (waiters,
nr_futexes, flags, timeout, clockid). The dispatch wrapper sc_futex_waitv
was forwarding only x0..x3, dropping x4. The implementation hardcoded
a CLOCK_MONOTONIC->CLOCK_REALTIME deadline conversion, so a guest asking
for CLOCK_REALTIME got monotonic semantics regardless of what it passed.
Wire x4 through and give sys_futex_waitv a clockid parameter. Validate
against CLOCK_REALTIME or CLOCK_MONOTONIC when timeout is non-NULL
(Linux returns EINVAL otherwise) and branch the absolute-deadline
conversion on clockid.

The pre-existing waitv loop slept on a private cond that no wake site
ever signalled; forward progress relied on a 50ms poll. Add optional
group_lock / group_cond pointers to futex_waiter_t plus a
futex_waiter_notify_group helper. Each wake site (futex_wake,
futex_requeue, futex_wake_op both passes, futex_unlock_pi) calls it
after marking woken=1 under the bucket lock. waitv blocks on
shared.cond directly; the loop's bounded sleep is now 500ms, kept only
so exit_group and timeout edges are still observed when no signal
arrives.

Lock ordering is bucket -> group_lock; futex_waiter_notify_group is
only called by wake sites that already hold the bucket lock, and the
waitv thread never holds shared.lock while taking a bucket lock.
Stack lifetime of shared.lock and shared.cond is protected by the
bucket-lock pairing in waitv_unlink, which synchronizes with every
wake's notify_group before the destroys run.

Locked in by tests/test-futex-waitv.c (19 cases): single-element
wake returning index 0, multi-element wake returning the woken
index, EAGAIN on stale val, eight EINVAL paths covering nr=0,
nr>128, top-level flags, reserved!=0, element flags reserved bits,
size!=U32, malformed CLOCK_REALTIME / CLOCK_MONOTONIC nsec,
unaligned uaddr, NULL waiters_gva, bad clockid with timeout; two
EFAULT paths via PROT_NONE pages; the inclusive nr_futexes==128
boundary; and ETIMEDOUT under both CLOCK_MONOTONIC and
CLOCK_REALTIME deadlines. The CLOCK_REALTIME case exists
specifically to catch a regression of the dropped-clockid bug.
Verified against Linux ground truth via tests/qemu-runner.sh; all
19 cases match.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 6 files

@jserv
jserv merged commit 34c741f into main May 5, 2026
5 checks passed
@jserv
jserv deleted the futex-waitv branch May 5, 2026 13:31
Max042004 added a commit to Max042004/elfuse that referenced this pull request Jul 9, 2026
The HVC sysprog21#9 W^X path can fall back to SIGSEGV when the fault
is not a legitimate permission toggle. signal_deliver_fault() installs
the SA_SIGINFO handler registers and marks X8=2, but the shim always ran
the W^X retry epilogue and restored the saved fault frame over them.

Teach both HVC sysprog21#9 return sites to honor the existing frame-drop marker,
and clear X8 after successful W^X toggles so stale guest state cannot
select that path.
henrybear327 pushed a commit to henrybear327/elfuse that referenced this pull request Jul 9, 2026
Honor futex_waitv clockid and wake without polling
henrybear327 pushed a commit to henrybear327/elfuse that referenced this pull request Jul 9, 2026
Replace the blanket TLBI VMALLE1IS that ran after every page-table-
modifying syscall with a per-VA TLBI VAE1IS path bounded by 16 pages,
upgrading to broadcast for larger ranges. Common cases (RELRO mprotect,
small munmap, MAP_FIXED PROT_NONE invalidation) now keep unrelated TLB
entries alive across the syscall return.

Stage requests on a per-vCPU TLS slot (cpu_tlbi_req in core/guest.h)
rather than a guest-global accumulator. A global slot let one vCPU's
syscall epilogue drain another vCPU's pending request before the second
vCPU eret'd back to EL0, leaving stale translations live until the
broadcast TLBI from the first vCPU caught up. With per-vCPU TLS each
thread strictly owns its own request and no concurrent vCPU can read,
clear, or partially observe it. The slot is C11 _Thread_local, so
fork-child and CLONE_THREAD workers start with TLBI_NONE for free.

Extend the X8 wire protocol after HVC sysprog21#5: 0 skips the flush, 1 keeps
the broadcast meaning, 2 stays reserved for the execve drop-frame
marker the shim handles separately, and 3 selects the new selective
path with X9 carrying the page-aligned start VA and X10 the page count.
The shim's tlbi_selective branch issues TLBI VAE1IS in a loop with a
defensive cbz x10 guard against a stray zero-count request, and tails
with DSB ISH + IC IALLU + DSB + ISB so callers like file-backed mmap
of executable pages still see the same I-cache invalidation as the
broadcast path.

Switch the W^X HVC sysprog21#9 fault handler in shim.S to single-page TLBI
VAE1IS using FAR_EL1. Per ARM ARM B2.2.5.6, TLBI VAE1IS for any VA
invalidates every cached entry containing that VA, so the per-page
TLBI also retires any 2 MiB block entry the prior split_l2_block left
behind. guest_split_block therefore no longer requests a separate
TLBI: every caller follows it with guest_invalidate_ptes or
guest_update_perms on the actually-changing range, and that subsequent
per-page TLBI is sufficient.

guest_update_perms now tracks the smallest sub-range whose L3
descriptor actually changed and only requests TLBI for that sub-range,
eliminating the broadcast-on-no-op false positive previously emitted
by adjacent same-perm mprotect storms (the common shape of dynamic-
linker RELRO).

Clear the per-vCPU slot at the end of guest_bootstrap_create_vcpu:
guest_build_page_tables and the boot-time guest_invalidate_ptes calls
(stack guard, null page) accumulate TLBI requests on the main thread's
TLS, but the shim's _start does its own TLBI VMALLE1IS before enabling
the MMU, so the first guest syscall must not redundantly broadcast on
top.
jserv added a commit that referenced this pull request Jul 10, 2026
henrybear327 pushed a commit to henrybear327/elfuse that referenced this pull request Jul 17, 2026
henrybear327 pushed a commit to henrybear327/elfuse that referenced this pull request Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant