fix(libkernel): no allocation while the target is suspended; own the wait count

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>
This commit is contained in:
Merijn 2026-10-07 21:10:25 +02:00
parent b92894c628
commit 51b6079b5b
7 changed files with 40 additions and 20 deletions

View File

@ -85,7 +85,7 @@ struct PthreadPrivate {
std::atomic<unsigned> references{2};
void* stackAddress = nullptr;
std::size_t stackSize = 0;
std::atomic<int>* waitState = nullptr;
std::atomic<int> waitCount{0};
std::atomic<KernelCpumask> affinity{DEFAULT_THREAD_AFFINITY};
std::atomic<int> priority{DEFAULT_THREAD_PRIORITY};
std::mutex nameLock;

View File

@ -174,7 +174,7 @@ static void RunThread(std::unique_ptr<ThreadArgs> args) {
const auto entry = args->entry;
void* arg = args->arg;
PthreadPrivate* self = args->self;
self->waitState = TimedWait::ThreadWaitState();
TimedWait::BindThreadWaitState(&self->waitCount);
if (!self->stackAddress) SetStackFromHost(self);
currentThread = self;
RegisterStack(self);
@ -408,7 +408,7 @@ Pthread APS5_VABI scePthreadSelf() {
adopted->threadId = std::this_thread::get_id();
adopted->_detached = true;
adopted->references.store(1, std::memory_order_relaxed);
adopted->waitState = TimedWait::ThreadWaitState();
TimedWait::BindThreadWaitState(&adopted->waitCount);
SetStackFromHost(adopted.get());
currentThread = adopted.release();
}

View File

@ -6,6 +6,7 @@
#include "prx/libkernel/Pthread/include/Pthread.hpp"
#include <array>
#include <cstring>
#include <memory>
#include <mutex>
#include <stdexcept>
#ifdef _WIN32
@ -193,15 +194,13 @@ void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) {
return;
}
const auto native = static_cast<HANDLE>(thread->nativeHandle);
auto queued = std::make_unique<Delivery>(Delivery{handler, signum, {}});
if (SuspendThread(native) == static_cast<DWORD>(-1)) throw std::runtime_error("sceKernelRaiseException: cannot suspend the target thread");
if (thread->waitState != nullptr && thread->waitState->load(std::memory_order_seq_cst) > 0) {
auto* delivery = new Delivery{handler, signum, {}};
const bool queued = QueueUserAPC(WaitingEntry, native, reinterpret_cast<ULONG_PTR>(delivery)) != 0;
if (thread->waitCount.load(std::memory_order_seq_cst) > 0) {
const bool accepted = QueueUserAPC(WaitingEntry, native, reinterpret_cast<ULONG_PTR>(queued.get())) != 0;
ResumeThread(native);
if (!queued) {
delete delivery;
throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread");
}
if (!accepted) throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread");
queued.release();
return;
}
alignas(16) Delivery delivery{handler, signum, {}};
@ -274,7 +273,7 @@ int APS5_VABI sceKernelRemoveExceptionHandler(int signum) {
int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) {
if (signum != 30) return SCE_KERNEL_ERROR_EINVAL;
if (thread == nullptr) return SCE_KERNEL_ERROR_ESRCH;
if (thread == nullptr || thread->_finished.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_ESRCH;
const auto handler = Handler(signum);
if (handler == nullptr) throw std::runtime_error("sceKernelRaiseException: no handler installed for the signal");
#ifdef _WIN32

View File

@ -17,24 +17,25 @@ namespace {
constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL;
constexpr std::uint64_t LEAD_NANOS = 500000ULL;
thread_local std::atomic<int> waitState{0};
thread_local std::atomic<int> ownWaitState{0};
thread_local std::atomic<int>* waitState = &ownWaitState;
#ifdef _WIN32
DWORD AlertableWait(DWORD count, const HANDLE* handles, DWORD milliseconds) {
waitState.fetch_add(1, std::memory_order_seq_cst);
waitState->fetch_add(1, std::memory_order_seq_cst);
DWORD result;
do {
result = WaitForMultipleObjectsEx(count, handles, FALSE, milliseconds, TRUE);
} while (result == WAIT_IO_COMPLETION);
waitState.fetch_sub(1, std::memory_order_seq_cst);
waitState->fetch_sub(1, std::memory_order_seq_cst);
return result;
}
void AlertableSleep(DWORD milliseconds) {
waitState.fetch_add(1, std::memory_order_seq_cst);
waitState->fetch_add(1, std::memory_order_seq_cst);
SleepEx(milliseconds, TRUE);
waitState.fetch_sub(1, std::memory_order_seq_cst);
waitState->fetch_sub(1, std::memory_order_seq_cst);
}
void WINAPI DestroyWaiter(void* value);
@ -208,8 +209,8 @@ void Condition::NotifyAll() {
#endif
std::atomic<int>* ThreadWaitState() {
return &waitState;
void BindThreadWaitState(std::atomic<int>* state) {
waitState = state != nullptr ? state : &ownWaitState;
}
bool Coarse() {

View File

@ -10,7 +10,7 @@
namespace TimedWait {
bool Coarse();
std::atomic<int>* ThreadWaitState();
void BindThreadWaitState(std::atomic<int>* state);
std::uint64_t NowNanos();
std::uint64_t DeadlineNanos(std::uint64_t microseconds);
std::uint64_t RemainingMicros(std::uint64_t deadlineNanos);

View File

@ -22,6 +22,7 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time);
}
static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016);
static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast<int>(0x80020003);
static constexpr int SIGUSR1 = 30;
static constexpr int Repeats = 100;
@ -84,6 +85,13 @@ static void* APS5_VABI HostBlocked(void* arg) {
return nullptr;
}
static std::atomic<bool> finishedReturned{false};
static void* APS5_VABI Finished(void*) {
finishedReturned.store(true);
return nullptr;
}
static void ExpectDelivery(int before, std::thread::id thread) {
for (int attempt = 0; attempt < 5000 && calls.load() == before; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(1));
Require(calls.load() == before + 1);
@ -148,5 +156,12 @@ int main() {
hostLock.unlock();
Require(scePthreadJoin(blockedThread, nullptr) == 0);
Pthread finishedThread = nullptr;
Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0);
while (!finishedReturned.load()) std::this_thread::yield();
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Require(sceKernelRaiseException(finishedThread, SIGUSR1) == SCE_KERNEL_ERROR_ESRCH);
Require(scePthreadJoin(finishedThread, nullptr) == 0);
Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0);
}

View File

@ -181,7 +181,12 @@ Throughout the project, every function at every stage either **does exactly what
- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported
- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out
- [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler but nothing delivers to it: a host fault still ends the process, and `sceKernelRaiseException` is not implemented. PPSA12544's PS5Util module (Unity) installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection, so delivery must run the handler on the target thread with a FreeBSD `ucontext`
- [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) delivers on Windows only and throws on Linux
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) saves and restores only `CONTEXT_FULL | CONTEXT_FLOATING_POINT` on Windows, so the upper halves of the YMM registers are not preserved if the handler or the entry uses AVX
- [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns
- [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`.
- [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not modelled: `scePthreadCancel` throws, so no cancel request is ever pending and `scePthreadTestcancel` and `pthread_testcancel` return without acting; the cancel state and type set by `scePthreadSetcancelstate` and `scePthreadSetcanceltype` are stored and not used
- [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws.