A thread had one TimedWait waiter for every wait. When
sceKernelRaiseException reached a thread blocked in a TimedWait
condition, its handler ran as an APC inside that wait, and a wait in
the handler queued the same waiter on a second condition while it was
still linked into the first. The links of the first queue were
overwritten, so waiters behind it were never woken, and a wake of the
outer wait set the shared event and was consumed by the inner wait.
A wait now takes the thread's first waiter that is not in use, and
creates a deeper one for a wait nested inside another.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
In the host-blocked phase of guest_raise_exception, the redirected handler
runs as soon as the worker wakes inside pthread_mutex_lock, before it owns
hostLock. ExpectDelivery then returns and main relocks hostLock right away.
If main wins that race, the worker stays blocked in the previous round's
lock_guard, so it never sets started for the next round and main spins
forever. The run hangs until the 30 s ctest timeout, with only three
RunThread lines in the log.
The worker now publishes the round after it acquires the mutex, and main
waits for that before relocking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A raise that suspends the thread after its wait returned but before the
wait count drops queued an APC that only ran at the next alertable wait.
Pending APCs are now run right after the count drops. The 0 ms poll in
Condition::waitSignalUntil runs under queueLock and is no longer
alertable, and a target that exits during the raise returns ESRCH.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The APC delivery was allocated after SuspendThread, so a target holding
the allocator lock deadlocked the raising thread; it is now allocated
before the suspend. The wait count a raise checks lived in the target's
thread_local storage and dangled once the thread exited; it is now a
member of PthreadPrivate, which TimedWait counts into through a
thread-local pointer bound when the thread starts, and a raise on a
thread that already finished returns ESRCH. TechnicalDebt gets one line
per unverified choice: Windows only, the FreeBSD ucontext layout, the
signal mask, the YMM upper halves, and delivery to threads outside
TimedWait.
AI-assisted: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thread suspended inside a system call returns through sysret, which
overwrites rcx and r11, so the delivery address passed in rcx was
garbage when the redirected thread resumed. PPSA12544 crashed that way
on its SaveDataProcessQueue thread, blocked in a host wait when Unity's
GC raised SIGUSR1. The redirected rip is now an entry stub that takes
the delivery from its fixed place above rsp. It also stores the live
general registers into the delivery's context first: the snapshot taken
while the thread was in the system call still holds the call number in
rax instead of its result, which the restore would have handed back to
the wait.
The guest test raises at a thread blocked on a host mutex in 20 rounds
and expects each delivery once the wait returns; it fails without this
change.
AI-assisted: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run the SIGUSR1 handler recorded by sceKernelInstallExceptionHandler on the
target thread with a PS5 ucontext (uc_mcontext at 0x40, FreeBSD amd64
layout) of its interrupted state:
- on the calling thread, directly;
- on a thread in a libkernel wait, from an APC: the TimedWait waits are
alertable and count themselves in a per-thread wait state, so the
handler runs inside the wait, which then continues;
- on a running thread, by suspending it and redirecting it to an entry
that calls the handler and restores the possibly modified context.
Other signals return EINVAL, as in shadPS4; raising without a handler
throws. Linux still throws.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>