Skip to content

feat(libkernel): wait for pending guest signals in the signal waits - #4138

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
VitzS7:fix/libkernel-signal-waits
Oct 12, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
VitzS7:fix/libkernel-signal-waits

Conversation

@VitzS7

@VitzS7 VitzS7 commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

What

pause, sigwait, sigwaitinfo and sigtimedwait no longer call NotImplemented_nid_no_patch, and pthread_kill to another thread no longer throws. They wait on a process-wide set of pending guest signals that covers every number from 1 to 128:

  • a signal raised while it is blocked stays pending and is delivered when _sigprocmask unblocks it, taken by sigwait, sigwaitinfo or sigtimedwait, or delivered by pause, which runs its handler on the calling thread and returns -1 with EINTR;
  • pthread_kill to another thread leaves the signal pending for whichever thread waits for or unblocks it, and answers ESRCH for a handle that is not a live guest thread; it threw for every thread other than the caller before;
  • raise and kill accept every number from 1 to 128: an unblocked one whose handler is registered runs its handler on the calling thread, which is what the signals with no host equivalent (SIGIO, SIGALRM, SIGBUS, SIGPIPE) need to be usable at all. An unblocked signal with no host equivalent and no handler still fails with EINVAL as before, so no path that used to continue now runs a default action.

sigtimedwait follows FreeBSD's sigtimedwait(2): it returns the selected signal number, EAGAIN when the timeout expires with nothing pending in the set, EINVAL for a tv_nsec outside 0 to 1000000000 while nothing is pending, and a null timeout blocks indefinitely. sigwait returns 0 with the number through its pointer, as POSIX specifies. The guest siginfo_t that sigwaitinfo and sigtimedwait write follows FreeBSD 11's sys/sys/siginfo.h (80 bytes, si_signo at 0, si_code at 8, si_pid at 12, si_uid at 16, si_addr at 24, si_value at 32, si_band at 40, asserted in the source); only si_signo, si_code (SI_USER) and si_pid are written, and si_uid stays 0 because no uid is modelled.

The limits that remain are recorded in technical debt: the set is process-wide like the mask, the waits are not cancellation points, an unblocked caught signal does not interrupt them with EINTR, and a pending signal whose disposition is the default action and that has no host equivalent stays pending instead of running that action, which on FreeBSD terminates the process for most signals.

guest_sigtimedwait replaces its NotImplemented checks with the behaviour above, including a signal sent from another thread and pause woken by one. One line of guest_signals changed: pthread_kill with a handle that is not a live guest thread threw and now returns ESRCH, as FreeBSD's does.

Overlap: #3986 changes the same file with a per-thread mask and pending set. No open pull request implements these four waits; if #3986 lands first this rebases onto it, keeps its per-thread masks and drops the duplicate part of the pending handling here.

Tested

Linux (Fedora 44 container, GCC 16.2.1, Release, BUILD_TESTING=ON, bundled dependencies), on this branch rebased onto main bca7a29d2:

  • guest_signals, guest_sigtimedwait, guest_signal_sets, guest_kill, guest_ioctl_getsid, guest_getitimer, guest_thread_cpu_clock, guest_memory and every guest_pthread_* test pass; guest_sigtimedwait was run five times in a row.
  • ctest -E "agc_|video_out|lds_|msaa" --timeout 90 -j6: 482 of 482 passed in 7.7 s. The excluded tests need a GPU, and this machine only has lavapipe.
  • python3 tools/check_conventions.py --base origin/main, git diff --check and python3 tools/tests/test_progress.py pass.
  • Not tested: Windows and macOS, and no title or homebrew was run. The change uses only std::mutex, std::condition_variable, std::atomic and std::chrono, but the Windows build is unchecked by me.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes (Hermes Agent)

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Which title stops at pause, sigwait, sigwaitinfo, sigtimedwait or pthread_kill to another thread on main? With the title ID and the log line this can go in, rebased on #3986 once that lands (it replaces the process-wide mask and pending set this builds on); otherwise I'll close it until one does.

@VitzS7

VitzS7 commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

The title IDs, with the NIDs they stop on:

Stop NID Titles
pause EDyhWelj7UY Ruffle Flash PPSA68091, PS5MC PPSA99420, RetroArch PPSA99169
sigwait mrbHXqK8wkg XPSemu PPSA97358, PS5MC PPSA99420
sigwaitinfo Uq5BGthgbl4 PS5MC PPSA99420
sigtimedwait D2-dIoJ0ZtE VLC PPSA85300

Where each of those comes from, all in the catalog sweep on #1578: pause is the next stop of Ruffle once #1702 lands and of RetroArch once #3598 lands (the orchestration of that round records both), PS5MC and XPSemu appear as stops on main 0694c579 in the third-run table, and sigtimedwait is reported in the feat/libc-rand48 run ("VLC then stops at sigtimedwait (D2-dIoJ0ZtE)"). #3552 has the same set with call sites: Ruffle pause at 0x8F7266; PS5MC pause 0x3BD80F6/0x3C9E220, sigwait 0x3C9E4C7, sigwaitinfo 0x3C9E588; XPSemu's sigwait_compat thread at 0x1101294/0x1101368.

To be exact about what is not verified: no run has shown the process dying inside the wait itself. What the sweep records is the NID at the import/next-stop level, which is why #3552 says "not yet observed in a run", and #4561 now tracks the same four calls throwing on main. The pthread_kill to another thread in this PR is a fix to that same pending model rather than a title-observed stop.

I will rebase on #3986 as soon as it lands and keep the pending set consistent with the per-thread mask that replaces the process-wide one.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Progress report (955f4fb → 01874b4)

📈 System libraries: 87.54% (+0.15%, +5 implemented functions)

✅ 5 implemented
Library Name
libkernel pause_nid_postfix
libkernel pthread_kill_nid_postfix
libkernel sigtimedwait_nid_postfix
libkernel sigwait_nid_postfix
libkernel sigwaitinfo_nid_postfix

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the title list. Three things before this can land:

  • Signals.cpp:93-97: MarkPending does fetch_or and notify_all without holding pending.lock, while WaitForPendingInSet (:209-223) and pause (:476-484) check the set under that lock and then wait. A signal raised between the check and the wait is lost and an infinite wait (sigwait, sigwaitinfo, null-timeout sigtimedwait, pause) hangs forever. Take the lock (or at least lock/unlock it) between the fetch_or and the notify_all. The 50 ms sleep in GuestSigtimedwait.cpp hides this; a test that raises with no delay, repeated many times, would show it.
  • This builds on the process-wide mask and pending set that #3986 replaces, as @oneandonlydean said. Please rebase on #3986 once it lands and keep pending signals per thread, so pthread_kill to another thread (:396) marks the target's set, not a process-wide one.
  • It also conflicts with main in docs/dev/TechnicalDebt.md.

Before: raise, kill and pthread_kill delivered a signal at once or failed
with EINVAL, nothing stayed pending, and pause, sigwait, sigwaitinfo and
sigtimedwait called NotImplemented_nid_no_patch.

Now: a process-wide pending set holds signals 1 to 128. A signal raised
while it is blocked stays pending and is delivered when _sigprocmask
unblocks it, taken by sigwait, sigwaitinfo or sigtimedwait, or delivered by
pause, which runs its handler on the calling thread and returns -1 with
EINTR. pthread_kill to another thread leaves the signal pending for
whichever thread waits for it and answers ESRCH for a handle that is not a
live guest thread. sigtimedwait follows FreeBSD's sigtimedwait(2): EAGAIN
on timeout, EINVAL for a tv_nsec outside 0 to 1000000000 while nothing is
pending, and an indefinite wait with a null timeout.
Before: MarkPending set the bit and notified while the waits checked the set
under pending.lock, so a signal raised between the check and the wait was lost
and sigwait, sigwaitinfo, a null-timeout sigtimedwait and pause hung forever.
Now: the bit is set while holding that lock, so a wait either sees it or is
notified after it has blocked. GuestSigtimedwait raises with no delay 500 times
without the first sleep the other senders use; on the previous code that hung in
2 of 10 runs, and it passes on the new one.
@VitzS7
VitzS7 force-pushed the fix/libkernel-signal-waits branch from 404fe40 to 01874b4 Compare October 12, 2026 20:57
@VitzS7

VitzS7 commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — all three:

1. The lost wakeup. You were right. MarkPending set the bit and notified without pending.lock, so a raise landing between the check and the wait was lost and the wait never returned. The bit is now set while holding that lock and the notification is sent after releasing it, so a wait either sees the bit or is notified after it has blocked. GuestSigtimedwait now raises the signal from another thread with no delay, 500 times, instead of relying on the 50 ms sleep the other senders use. On the previous code the test hung: 2 of 10 runs ended at the 60 s timeout (exit 124); on this one 5 of 5 runs pass, and the full suite is 526/526 with the GPU targets excluded.

2. The conflict. Rebased onto main c952c2b0e. TechnicalDebt.md is the hub now, so the entries went to docs/dev/debt/libkernel.md: sigtimedwait, pause, sigwait, sigwaitinfo, sigaction and pthread_kill are updated there. The pull request touches no other documentation file.

3. Per-thread pending. Agreed, and I would rather not guess it here: this still keeps one process-wide pending set, as you said it builds on the mask #3986 replaces. As soon as #3986 lands I will rebase on it and keep the set per thread, so pthread_kill to another thread marks the target's set and every wait reads the calling thread's set.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 12, 2026
@github-actions

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#4578 docs/dev/debt/libkernel.md L146-149
#4582 docs/dev/debt/libkernel.md L145-147
#4671 docs/dev/debt/libkernel.md L149-150

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Checked the lost-wakeup fix (bit set under pending.lock, notify after release): guest_sigtimedwait with 500 undelayed raises passes 30 runs in a row, and both guest_signals and guest_sigtimedwait fail without the change. Scoped build and ctest (141 tests) pass on the PR merged onto main. The process-wide pending set stays until #3986 lands, as recorded in docs/dev/debt/libkernel.md; whichever of the two merges second rebases. Merging.

@adriadam10
adriadam10 merged commit 2a9068b into boykopovar:main Oct 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants