From 73a0bc5eb4e8d25657f17b57b3ddc669d1ef68f6 Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 19:47:30 +0200 Subject: [PATCH 1/6] feat(libkernel): record exception handlers sceKernelInstallExceptionHandler and sceKernelRemoveExceptionHandler threw. They now keep one handler per allowed signal (SIGHUP, SIGILL, SIGFPE, SIGBUS, SIGSEGV, SIGUSR1, as in shadPS4): another signal or a null handler returns EINVAL, a second handler for a signal returns EAGAIN, and removing clears it. Delivery is not implemented and is recorded in the technical debt; sceKernelRaiseException still throws. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 9 +++++ .../prx/libkernel/System/src/Exception.cpp | 30 ++++++++++++++--- core/libs/tests/GuestExceptionHandler.cpp | 33 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 ++ 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestExceptionHandler.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 43949deb4..111cc2310 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -254,6 +254,15 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_exception_handler_tests ${LIBS_INCLUDE_DIR}/tests/GuestExceptionHandler.cpp) + target_include_directories(guest_exception_handler_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_exception_handler_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_exception_handler_tests) + add_test(NAME guest_exception_handler COMMAND guest_exception_handler_tests) + if(WIN32) + set_tests_properties(guest_exception_handler PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sync_on_address_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyncOnAddress.cpp) target_include_directories(guest_sync_on_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sync_on_address_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 8e59a6b68..ba53dbdba 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -2,19 +2,39 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include + +namespace { + +constexpr std::array AllowedSignals{1, 4, 8, 10, 11, 30}; + +std::mutex handlersLock; +std::array handlers{}; + +bool Allowed(int signum) { + for (const int allowed : AllowedSignals) + if (allowed == signum) return true; + return false; +} + +} extern "C" { int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler) { - (void)signum; - (void)handler; - NotImplemented_nid_no_patch(__func__); + if (!Allowed(signum) || handler == nullptr) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(handlersLock); + if (handlers[signum] != nullptr) return SCE_KERNEL_ERROR_EAGAIN; + handlers[signum] = handler; return 0; } int APS5_VABI sceKernelRemoveExceptionHandler(int signum) { - (void)signum; - NotImplemented_nid_no_patch(__func__); + if (!Allowed(signum)) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(handlersLock); + handlers[signum] = nullptr; return 0; } diff --git a/core/libs/tests/GuestExceptionHandler.cpp b/core/libs/tests/GuestExceptionHandler.cpp new file mode 100644 index 000000000..fbb5dfd8f --- /dev/null +++ b/core/libs/tests/GuestExceptionHandler.cpp @@ -0,0 +1,33 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler); +int APS5_VABI sceKernelRemoveExceptionHandler(int signum); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_EAGAIN = static_cast(0x80020023); + +static void APS5_VABI Handler(int, void*) {} +static void APS5_VABI Other(int, void*) {} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + auto* handler = reinterpret_cast(&Handler); + auto* other = reinterpret_cast(&Other); + for (const int rejected : {0, 2, 9, 31, 128, -1}) + Require(sceKernelInstallExceptionHandler(rejected, handler) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelInstallExceptionHandler(30, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelRemoveExceptionHandler(9) == SCE_KERNEL_ERROR_EINVAL); + for (const int signum : {1, 4, 8, 10, 11, 30}) { + Require(sceKernelInstallExceptionHandler(signum, handler) == 0); + Require(sceKernelInstallExceptionHandler(signum, other) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelRemoveExceptionHandler(signum) == 0); + Require(sceKernelInstallExceptionHandler(signum, other) == 0); + Require(sceKernelRemoveExceptionHandler(signum) == 0); + } + Require(sceKernelRemoveExceptionHandler(30) == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c457ddd14..324f1e79e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -97,6 +97,8 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [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` + - [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`. - [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. - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. From dd2890c1c5b7ef0776bf6f8cf5b4b53853364b38 Mon Sep 17 00:00:00 2001 From: Merijn Date: Mon, 5 Oct 2026 22:55:58 +0200 Subject: [PATCH 2/6] feat(libkernel): deliver sceKernelRaiseException on Windows 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 --- core/libs/prx/libkernel/CMakeLists.txt | 9 + .../prx/libkernel/Pthread/include/Pthread.hpp | 1 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 2 + .../prx/libkernel/System/src/Exception.cpp | 202 +++++++++++++++++- core/libs/prx/libkernel/Time/TimedWait.cpp | 36 +++- .../prx/libkernel/Time/include/TimedWait.hpp | 2 + core/libs/tests/GuestRaiseException.cpp | 118 ++++++++++ 7 files changed, 361 insertions(+), 9 deletions(-) create mode 100644 core/libs/tests/GuestRaiseException.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 111cc2310..b34080e13 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -263,6 +263,15 @@ if(BUILD_TESTING) set_tests_properties(guest_exception_handler PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(guest_raise_exception_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseException.cpp) + target_include_directories(guest_raise_exception_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_exception_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_exception_tests) + add_test(NAME guest_raise_exception COMMAND guest_raise_exception_tests) + set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sync_on_address_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyncOnAddress.cpp) target_include_directories(guest_sync_on_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sync_on_address_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index b95ca4f87..4533d3a2f 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -85,6 +85,7 @@ struct PthreadPrivate { std::atomic references{2}; void* stackAddress = nullptr; std::size_t stackSize = 0; + std::atomic* waitState = nullptr; std::atomic affinity{DEFAULT_THREAD_AFFINITY}; std::atomic priority{DEFAULT_THREAD_PRIORITY}; std::mutex nameLock; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 3dc69cbab..7fb038be0 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -96,6 +96,7 @@ static void RunThread(std::unique_ptr args) { const auto entry = args->entry; void* arg = args->arg; PthreadPrivate* self = args->self; + self->waitState = TimedWait::ThreadWaitState(); currentThread = self; args.reset(); finishThread(self, entry(arg)); @@ -327,6 +328,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(); currentThread = adopted.release(); } #else diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index ba53dbdba..2ee216927 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -3,11 +3,81 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include +#include #include +#include +#ifdef _WIN32 +#include +#endif + +extern "C" Pthread APS5_VABI scePthreadSelf(); namespace { +struct GuestMcontext { + std::uint64_t onstack; + std::uint64_t rdi; + std::uint64_t rsi; + std::uint64_t rdx; + std::uint64_t rcx; + std::uint64_t r8; + std::uint64_t r9; + std::uint64_t rax; + std::uint64_t rbx; + std::uint64_t rbp; + std::uint64_t r10; + std::uint64_t r11; + std::uint64_t r12; + std::uint64_t r13; + std::uint64_t r14; + std::uint64_t r15; + std::uint32_t trapno; + std::uint16_t fs; + std::uint16_t gs; + std::uint64_t addr; + std::uint32_t flags; + std::uint16_t es; + std::uint16_t ds; + std::uint64_t err; + std::uint64_t rip; + std::uint64_t cs; + std::uint64_t rflags; + std::uint64_t rsp; + std::uint64_t ss; + std::uint64_t len; + std::uint64_t fpformat; + std::uint64_t ownedfp; + std::uint64_t lbrfrom; + std::uint64_t lbrto; + std::uint64_t aux1; + std::uint64_t aux2; + std::uint64_t fpstate[104]; + std::uint64_t fsbase; + std::uint64_t gsbase; + std::uint64_t spare[6]; +}; + +struct GuestUcontext { + std::uint32_t sigmask[4]; + std::int32_t reserved[12]; + GuestMcontext mcontext; + GuestUcontext* link; + void* stackPointer; + std::uint64_t stackSize; + std::int32_t stackFlags; + std::int32_t stackAlign; + std::int32_t flags; + std::int32_t spare[4]; + std::int32_t tail[3]; +}; + +static_assert(offsetof(GuestUcontext, mcontext) == 0x40); +static_assert(offsetof(GuestUcontext, mcontext) + offsetof(GuestMcontext, rsp) == 0xf8); + +using GuestExceptionHandler = void (APS5_VABI *)(int, void*); + constexpr std::array AllowedSignals{1, 4, 8, 10, 11, 30}; std::mutex handlersLock; @@ -19,6 +89,127 @@ bool Allowed(int signum) { return false; } +GuestExceptionHandler Handler(int signum) { + std::lock_guard lock(handlersLock); + return reinterpret_cast(handlers[signum]); +} + +#ifdef _WIN32 +constexpr std::size_t RedZone = 128; + +struct Delivery { + GuestExceptionHandler handler; + int signum; + CONTEXT context; +}; + +void Deliver(GuestExceptionHandler handler, int signum, CONTEXT& context) { + GuestUcontext ucontext{}; + auto& m = ucontext.mcontext; + m.rdi = context.Rdi; + m.rsi = context.Rsi; + m.rdx = context.Rdx; + m.rcx = context.Rcx; + m.r8 = context.R8; + m.r9 = context.R9; + m.rax = context.Rax; + m.rbx = context.Rbx; + m.rbp = context.Rbp; + m.r10 = context.R10; + m.r11 = context.R11; + m.r12 = context.R12; + m.r13 = context.R13; + m.r14 = context.R14; + m.r15 = context.R15; + m.rip = context.Rip; + m.rsp = context.Rsp; + m.rflags = context.EFlags; + m.cs = context.SegCs; + m.ss = context.SegSs; + m.len = sizeof(GuestMcontext); + static_assert(sizeof(context.FltSave) <= sizeof(m.fpstate)); + std::memcpy(m.fpstate, &context.FltSave, sizeof(context.FltSave)); + handler(signum, &ucontext); + context.Rdi = m.rdi; + context.Rsi = m.rsi; + context.Rdx = m.rdx; + context.Rcx = m.rcx; + context.R8 = m.r8; + context.R9 = m.r9; + context.Rax = m.rax; + context.Rbx = m.rbx; + context.Rbp = m.rbp; + context.R10 = m.r10; + context.R11 = m.r11; + context.R12 = m.r12; + context.R13 = m.r13; + context.R14 = m.r14; + context.R15 = m.r15; + context.Rip = m.rip; + context.Rsp = m.rsp; + context.EFlags = static_cast(m.rflags); + std::memcpy(&context.FltSave, m.fpstate, sizeof(context.FltSave)); +} + +[[noreturn]] void RedirectedEntry(Delivery* delivery) { + CONTEXT context = delivery->context; + const auto handler = delivery->handler; + const int signum = delivery->signum; + delete delivery; + Deliver(handler, signum, context); + RtlRestoreContext(&context, nullptr); + std::abort(); +} + +void CALLBACK WaitingEntry(ULONG_PTR parameter) { + auto* delivery = reinterpret_cast(parameter); + const auto handler = delivery->handler; + const int signum = delivery->signum; + delete delivery; + CONTEXT context{}; + RtlCaptureContext(&context); + Deliver(handler, signum, context); +} + +void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { + if (thread == scePthreadSelf()) { + CONTEXT context{}; + RtlCaptureContext(&context); + Deliver(handler, signum, context); + return; + } + const auto native = static_cast(thread->nativeHandle); + if (SuspendThread(native) == static_cast(-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(delivery)) != 0; + ResumeThread(native); + if (!queued) { + delete delivery; + throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread"); + } + return; + } + auto* delivery = new Delivery{handler, signum, {}}; + delivery->context.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT; + if (!GetThreadContext(native, &delivery->context)) { + ResumeThread(native); + delete delivery; + throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context"); + } + CONTEXT redirected = delivery->context; + redirected.Rsp = ((delivery->context.Rsp - RedZone) & ~static_cast(15)) - 8; + redirected.Rip = reinterpret_cast(&RedirectedEntry); + redirected.Rcx = reinterpret_cast(delivery); + if (!SetThreadContext(native, &redirected)) { + ResumeThread(native); + delete delivery; + throw std::runtime_error("sceKernelRaiseException: cannot redirect the target thread"); + } + ResumeThread(native); +} +#endif + } extern "C" { @@ -39,10 +230,17 @@ int APS5_VABI sceKernelRemoveExceptionHandler(int signum) { } int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) { - (void)thread; - (void)signum; + if (signum != 30) return SCE_KERNEL_ERROR_EINVAL; + if (thread == nullptr) 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 + RaiseOn(thread, handler, signum); + return 0; +#else NotImplemented_nid_no_patch(__func__); return 0; +#endif } void APS5_VABI sceKernelDebugRaiseException(int c1, int c2) { diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 08c0b0d77..7fb2592c0 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -17,8 +17,26 @@ namespace { constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL; constexpr std::uint64_t LEAD_NANOS = 500000ULL; +thread_local std::atomic waitState{0}; + #ifdef _WIN32 +DWORD AlertableWait(DWORD count, const HANDLE* handles, DWORD milliseconds) { + 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); + return result; +} + +void AlertableSleep(DWORD milliseconds) { + waitState.fetch_add(1, std::memory_order_seq_cst); + SleepEx(milliseconds, TRUE); + waitState.fetch_sub(1, std::memory_order_seq_cst); +} + void WINAPI DestroyWaiter(void* value); DWORD WaiterSlotIndex() { @@ -36,7 +54,7 @@ bool TimerWait(HANDLE timer, std::uint64_t nanos) { due.QuadPart = -static_cast(nanos / 100ULL); if (due.QuadPart == 0) due.QuadPart = -1; if (!SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE)) return false; - return WaitForSingleObject(timer, INFINITE) == WAIT_OBJECT_0; + return AlertableWait(1, &timer, INFINITE) == WAIT_OBJECT_0; } DWORD WholeMilliseconds(std::uint64_t nanos) { @@ -51,7 +69,7 @@ void SleepNanosCoarse(std::uint64_t nanos) { } thread_local HANDLE timer = CreateHighResolutionTimer(); if (TimerWait(timer, nanos)) return; - Sleep(static_cast((nanos + 999999ULL) / 1000000ULL)); + AlertableSleep(static_cast((nanos + 999999ULL) / 1000000ULL)); } #endif @@ -90,7 +108,7 @@ Waiter* ThisThreadWaiter() { } bool EventSet(HANDLE event, DWORD milliseconds) { - return WaitForSingleObject(event, milliseconds) == WAIT_OBJECT_0; + return AlertableWait(1, &event, milliseconds) == WAIT_OBJECT_0; } bool WaitEventUntil(Waiter* waiter, std::uint64_t deadlineNanos) { @@ -104,7 +122,7 @@ bool WaitEventUntil(Waiter* waiter, std::uint64_t deadlineNanos) { due.QuadPart = -static_cast(bulk / 100ULL); if (waiter->timer && SetWaitableTimer(waiter->timer, &due, 0, nullptr, nullptr, FALSE)) { HANDLE handles[2] = {waiter->event, waiter->timer}; - if (WaitForMultipleObjects(2, handles, FALSE, INFINITE) == WAIT_OBJECT_0) return true; + if (AlertableWait(2, handles, INFINITE) == WAIT_OBJECT_0) return true; continue; } if (EventSet(waiter->event, WholeMilliseconds(bulk))) return true; @@ -140,7 +158,7 @@ void Condition::unlink(Waiter* waiter) { } void Condition::waitSignal(Waiter* waiter) { - WaitForSingleObject(waiter->event, INFINITE); + AlertableWait(1, &waiter->event, INFINITE); } bool Condition::waitSignalUntil(Waiter* waiter, std::uint64_t deadlineNanos) { @@ -190,6 +208,10 @@ void Condition::NotifyAll() { #endif +std::atomic* ThreadWaitState() { + return &waitState; +} + bool Coarse() { #ifdef _WIN32 static const bool coarse = std::getenv("APS5_COARSE_TIMED_WAITS") != nullptr; @@ -233,7 +255,7 @@ void SleepUntil(std::uint64_t deadlineNanos) { std::uint64_t now = NowNanos(); if (deadlineNanos > now + LEAD_NANOS) { thread_local HANDLE timer = CreateHighResolutionTimer(); - if (!TimerWait(timer, deadlineNanos - now - LEAD_NANOS)) Sleep(WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); + if (!TimerWait(timer, deadlineNanos - now - LEAD_NANOS)) AlertableSleep(WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); now = NowNanos(); } while (now < deadlineNanos) { @@ -251,7 +273,7 @@ void PollSleepUntil(std::uint64_t deadlineNanos) { const std::uint64_t now = NowNanos(); if (deadlineNanos <= now) return; thread_local HANDLE timer = CreateHighResolutionTimer(); - if (!TimerWait(timer, deadlineNanos - now)) Sleep(WholeMilliseconds(deadlineNanos - now)); + if (!TimerWait(timer, deadlineNanos - now)) AlertableSleep(WholeMilliseconds(deadlineNanos - now)); #else SleepUntil(deadlineNanos); #endif diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 774c62dc6..fa83866dc 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_TIMEDWAIT_HPP #define CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_TIMEDWAIT_HPP +#include #include #include #include @@ -9,6 +10,7 @@ namespace TimedWait { bool Coarse(); +std::atomic* ThreadWaitState(); std::uint64_t NowNanos(); std::uint64_t DeadlineNanos(std::uint64_t microseconds); std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp new file mode 100644 index 000000000..f07d17217 --- /dev/null +++ b/core/libs/tests/GuestRaiseException.cpp @@ -0,0 +1,118 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler); +int APS5_VABI sceKernelRemoveExceptionHandler(int signum); +int APS5_VABI sceKernelRaiseException(Pthread thread, int signum); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t attr, int init, int max, void* opt); +int APS5_VABI sceKernelDeleteSema(KernelSema sem); +int APS5_VABI sceKernelSignalSema(KernelSema sem, int count); +int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SIGUSR1 = 30; +static constexpr int Repeats = 100; + +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic calls{0}; +static std::atomic handlerThread; +static std::atomic handlerRsp{0}; +static std::atomic handlerFrame{0}; + +static void APS5_VABI Handler(int signum, void* context) { + Require(signum == SIGUSR1); + std::uint64_t rsp = 0; + std::memcpy(&rsp, static_cast(context) + 0xf8, sizeof(rsp)); + int local = 0; + handlerFrame.store(reinterpret_cast(&local)); + handlerRsp.store(rsp); + handlerThread.store(std::this_thread::get_id()); + calls.fetch_add(1); +} + +struct Worker { + std::atomic started{false}; + std::atomic stop{false}; + std::thread::id id; + KernelSema sem = nullptr; + int waitResult = -1; +}; + +static void* APS5_VABI Busy(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + volatile std::uint64_t spins = 0; + while (!worker.stop.load()) spins = spins + 1; + return nullptr; +} + +static void* APS5_VABI Waiting(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + worker.waitResult = sceKernelWaitSema(worker.sem, 1, nullptr); + 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); + Require(handlerThread.load() == thread); + Require(handlerRsp.load() != 0 && handlerFrame.load() < handlerRsp.load()); +} + +int main() { + Require(sceKernelRaiseException(scePthreadSelf(), 11) == SCE_KERNEL_ERROR_EINVAL); + bool rejected = false; + try { + sceKernelRaiseException(scePthreadSelf(), SIGUSR1); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); + Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&Handler)) == 0); + + Require(sceKernelRaiseException(scePthreadSelf(), SIGUSR1) == 0); + Require(calls.load() == 1 && handlerThread.load() == std::this_thread::get_id()); + + Worker busy; + Pthread busyThread = nullptr; + Require(scePthreadCreate(&busyThread, nullptr, Busy, &busy, "busy") == 0); + while (!busy.started.load()) std::this_thread::yield(); + for (int raised = 0; raised < Repeats; ++raised) { + Require(sceKernelRaiseException(busyThread, SIGUSR1) == 0); + ExpectDelivery(1 + raised, busy.id); + } + busy.stop.store(true); + Require(scePthreadJoin(busyThread, nullptr) == 0); + + Worker waiting; + Require(sceKernelCreateSema(&waiting.sem, "raise", 0, 0, 1, nullptr) == 0); + Pthread waitingThread = nullptr; + Require(scePthreadCreate(&waitingThread, nullptr, Waiting, &waiting, "waiting") == 0); + while (!waiting.started.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + for (int raised = 0; raised < Repeats; ++raised) { + Require(sceKernelRaiseException(waitingThread, SIGUSR1) == 0); + ExpectDelivery(1 + Repeats + raised, waiting.id); + } + Require(sceKernelSignalSema(waiting.sem, 1) == 0); + Require(scePthreadJoin(waitingThread, nullptr) == 0); + Require(waiting.waitResult == 0); + Require(sceKernelDeleteSema(waiting.sem) == 0); + + Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); +} From b550545b4f71c7088bd824e6b495f29c8c110a90 Mon Sep 17 00:00:00 2001 From: Merijn Date: Tue, 6 Oct 2026 09:59:16 +0200 Subject: [PATCH 3/6] fix(libkernel): keep redirected delivery off the red zone and the heap The redirect entry's Windows x64 home area started inside the guest's 128-byte red zone, and the interrupted thread freed the delivery on the heap, which it may have been suspended inside. The delivery is now copied onto the target stack below the red zone, with the home area below it, and the copy is refused when those pages are not committed. AI-assisted: yes Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/System/src/Exception.cpp | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 2ee216927..cfaf2a850 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -96,6 +96,7 @@ GuestExceptionHandler Handler(int signum) { #ifdef _WIN32 constexpr std::size_t RedZone = 128; +constexpr std::size_t HomeArea = 32; struct Delivery { GuestExceptionHandler handler; @@ -153,14 +154,21 @@ void Deliver(GuestExceptionHandler handler, int signum, CONTEXT& context) { [[noreturn]] void RedirectedEntry(Delivery* delivery) { CONTEXT context = delivery->context; - const auto handler = delivery->handler; - const int signum = delivery->signum; - delete delivery; - Deliver(handler, signum, context); + Deliver(delivery->handler, delivery->signum, context); RtlRestoreContext(&context, nullptr); std::abort(); } +bool StackWritable(DWORD64 low, DWORD64 high) { + for (DWORD64 address = low; address < high;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(address), &info, sizeof(info)) != sizeof(info)) return false; + if (info.State != MEM_COMMIT || (info.Protect & PAGE_GUARD) != 0 || (info.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE)) == 0) return false; + address = reinterpret_cast(info.BaseAddress) + info.RegionSize; + } + return true; +} + void CALLBACK WaitingEntry(ULONG_PTR parameter) { auto* delivery = reinterpret_cast(parameter); const auto handler = delivery->handler; @@ -190,20 +198,24 @@ void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { } return; } - auto* delivery = new Delivery{handler, signum, {}}; - delivery->context.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT; - if (!GetThreadContext(native, &delivery->context)) { + alignas(16) Delivery delivery{handler, signum, {}}; + delivery.context.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT; + if (!GetThreadContext(native, &delivery.context)) { ResumeThread(native); - delete delivery; throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context"); } - CONTEXT redirected = delivery->context; - redirected.Rsp = ((delivery->context.Rsp - RedZone) & ~static_cast(15)) - 8; + const DWORD64 slot = (delivery.context.Rsp - RedZone - sizeof(Delivery)) & ~static_cast(15); + if (!StackWritable(slot - HomeArea - 8, delivery.context.Rsp - RedZone)) { + ResumeThread(native); + throw std::runtime_error("sceKernelRaiseException: the target thread stack below its red zone is not committed"); + } + std::memcpy(reinterpret_cast(slot), &delivery, sizeof(Delivery)); + CONTEXT redirected = delivery.context; + redirected.Rsp = slot - HomeArea - 8; redirected.Rip = reinterpret_cast(&RedirectedEntry); - redirected.Rcx = reinterpret_cast(delivery); + redirected.Rcx = slot; if (!SetThreadContext(native, &redirected)) { ResumeThread(native); - delete delivery; throw std::runtime_error("sceKernelRaiseException: cannot redirect the target thread"); } ResumeThread(native); From 3cd2ff91402b6c6f14565ae71a5d1db4a6442ab2 Mon Sep 17 00:00:00 2001 From: Merijn Date: Tue, 6 Oct 2026 15:41:25 +0200 Subject: [PATCH 4/6] fix(libkernel): take a redirected delivery from rsp and the live registers 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 --- .../prx/libkernel/System/src/Exception.cpp | 35 +++++++++++++++++-- core/libs/tests/GuestRaiseException.cpp | 34 ++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index cfaf2a850..aef934025 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -13,6 +13,9 @@ #endif extern "C" Pthread APS5_VABI scePthreadSelf(); +#ifdef _WIN32 +extern "C" void Aps5RedirectedEntryStub(); +#endif namespace { @@ -179,6 +182,9 @@ void CALLBACK WaitingEntry(ULONG_PTR parameter) { Deliver(handler, signum, context); } +static_assert(HomeArea + 8 == 40, "Aps5RedirectedEntryStub finds the delivery 40 bytes above its stack pointer"); +static_assert(offsetof(Delivery, context) == 16 && offsetof(CONTEXT, Rax) == 0x78 && offsetof(CONTEXT, Rbp) == 0xa0 && offsetof(CONTEXT, R15) == 0xf0, "Aps5RedirectedEntryStub stores the live registers into the delivery's context"); + void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (thread == scePthreadSelf()) { CONTEXT context{}; @@ -212,8 +218,7 @@ void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { std::memcpy(reinterpret_cast(slot), &delivery, sizeof(Delivery)); CONTEXT redirected = delivery.context; redirected.Rsp = slot - HomeArea - 8; - redirected.Rip = reinterpret_cast(&RedirectedEntry); - redirected.Rcx = slot; + redirected.Rip = reinterpret_cast(&Aps5RedirectedEntryStub); if (!SetThreadContext(native, &redirected)) { ResumeThread(native); throw std::runtime_error("sceKernelRaiseException: cannot redirect the target thread"); @@ -224,6 +229,32 @@ void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { } +#ifdef _WIN32 +extern "C" [[noreturn]] void Aps5RedirectedEntry(void* delivery) { + RedirectedEntry(static_cast(delivery)); +} +asm(".text\n" + ".globl Aps5RedirectedEntryStub\n" + "Aps5RedirectedEntryStub:\n" + " movq %rax, 176(%rsp)\n" + " movq %rcx, 184(%rsp)\n" + " movq %rdx, 192(%rsp)\n" + " movq %rbx, 200(%rsp)\n" + " movq %rbp, 216(%rsp)\n" + " movq %rsi, 224(%rsp)\n" + " movq %rdi, 232(%rsp)\n" + " movq %r8, 240(%rsp)\n" + " movq %r9, 248(%rsp)\n" + " movq %r10, 256(%rsp)\n" + " movq %r11, 264(%rsp)\n" + " movq %r12, 272(%rsp)\n" + " movq %r13, 280(%rsp)\n" + " movq %r14, 288(%rsp)\n" + " movq %r15, 296(%rsp)\n" + " leaq 40(%rsp), %rcx\n" + " jmp Aps5RedirectedEntry\n"); +#endif + extern "C" { int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler) { diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index f07d17217..c6cfd629a 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -67,6 +68,22 @@ static void* APS5_VABI Waiting(void* arg) { return nullptr; } +static std::mutex hostLock; + +static constexpr int HostRounds = 20; +static std::atomic hostRound{0}; + +static void* APS5_VABI HostBlocked(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + for (int round = 0; round < HostRounds; ++round) { + while (hostRound.load() != round) std::this_thread::yield(); + worker.started.store(true); + std::lock_guard lock(hostLock); + } + 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); @@ -114,5 +131,22 @@ int main() { Require(waiting.waitResult == 0); Require(sceKernelDeleteSema(waiting.sem) == 0); + Worker blocked; + Pthread blockedThread = nullptr; + hostLock.lock(); + Require(scePthreadCreate(&blockedThread, nullptr, HostBlocked, &blocked, "host blocked") == 0); + for (int round = 0; round < HostRounds; ++round) { + while (!blocked.started.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceKernelRaiseException(blockedThread, SIGUSR1) == 0); + hostLock.unlock(); + ExpectDelivery(1 + 2 * Repeats + round, blocked.id); + hostLock.lock(); + blocked.started.store(false); + hostRound.store(round + 1); + } + hostLock.unlock(); + Require(scePthreadJoin(blockedThread, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); } From 51b6079b5b6659eb83e4406bfcffe313adefebdf Mon Sep 17 00:00:00 2001 From: Merijn Date: Wed, 7 Oct 2026 21:10:25 +0200 Subject: [PATCH 5/6] 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 --- .../prx/libkernel/Pthread/include/Pthread.hpp | 2 +- core/libs/prx/libkernel/Pthread/src/Thread.cpp | 4 ++-- core/libs/prx/libkernel/System/src/Exception.cpp | 15 +++++++-------- core/libs/prx/libkernel/Time/TimedWait.cpp | 15 ++++++++------- .../libs/prx/libkernel/Time/include/TimedWait.hpp | 2 +- core/libs/tests/GuestRaiseException.cpp | 15 +++++++++++++++ docs/dev/TechnicalDebt.md | 7 ++++++- 7 files changed, 40 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 3a1c831fb..2f3fb16eb 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -85,7 +85,7 @@ struct PthreadPrivate { std::atomic references{2}; void* stackAddress = nullptr; std::size_t stackSize = 0; - std::atomic* waitState = nullptr; + std::atomic waitCount{0}; std::atomic affinity{DEFAULT_THREAD_AFFINITY}; std::atomic priority{DEFAULT_THREAD_PRIORITY}; std::mutex nameLock; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index c1aa91883..b3c368d80 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -174,7 +174,7 @@ static void RunThread(std::unique_ptr 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(); } diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index aef934025..958bf534f 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -6,6 +6,7 @@ #include "prx/libkernel/Pthread/include/Pthread.hpp" #include #include +#include #include #include #ifdef _WIN32 @@ -193,15 +194,13 @@ void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { return; } const auto native = static_cast(thread->nativeHandle); + auto queued = std::make_unique(Delivery{handler, signum, {}}); if (SuspendThread(native) == static_cast(-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(delivery)) != 0; + if (thread->waitCount.load(std::memory_order_seq_cst) > 0) { + const bool accepted = QueueUserAPC(WaitingEntry, native, reinterpret_cast(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 diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 7fb2592c0..90fd466ad 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -17,24 +17,25 @@ namespace { constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL; constexpr std::uint64_t LEAD_NANOS = 500000ULL; -thread_local std::atomic waitState{0}; +thread_local std::atomic ownWaitState{0}; +thread_local std::atomic* 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* ThreadWaitState() { - return &waitState; +void BindThreadWaitState(std::atomic* state) { + waitState = state != nullptr ? state : &ownWaitState; } bool Coarse() { diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index fa83866dc..bddd4189e 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -10,7 +10,7 @@ namespace TimedWait { bool Coarse(); -std::atomic* ThreadWaitState(); +void BindThreadWaitState(std::atomic* state); std::uint64_t NowNanos(); std::uint64_t DeadlineNanos(std::uint64_t microseconds); std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index c6cfd629a..3a8913473 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -22,6 +22,7 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time); } static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(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 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); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 07ab6b6ad..ca20fa42c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -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. From 2654e3a4ca9249d0875356a102f42b20344dc209 Mon Sep 17 00:00:00 2001 From: Merijn Date: Wed, 7 Oct 2026 22:11:29 +0200 Subject: [PATCH 6/6] fix(kernel): drain raised exceptions when leaving an alertable wait 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 --- .../prx/libkernel/System/src/Exception.cpp | 23 +++++++++---- core/libs/prx/libkernel/Time/TimedWait.cpp | 12 ++++++- core/libs/tests/GuestRaiseException.cpp | 32 +++++++++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 958bf534f..e0ac735c3 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -186,22 +186,33 @@ void CALLBACK WaitingEntry(ULONG_PTR parameter) { static_assert(HomeArea + 8 == 40, "Aps5RedirectedEntryStub finds the delivery 40 bytes above its stack pointer"); static_assert(offsetof(Delivery, context) == 16 && offsetof(CONTEXT, Rax) == 0x78 && offsetof(CONTEXT, Rbp) == 0xa0 && offsetof(CONTEXT, R15) == 0xf0, "Aps5RedirectedEntryStub stores the live registers into the delivery's context"); -void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { +bool Exited(HANDLE native) { + return WaitForSingleObject(native, 0) == WAIT_OBJECT_0; +} + +bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (thread == scePthreadSelf()) { CONTEXT context{}; RtlCaptureContext(&context); Deliver(handler, signum, context); - return; + return true; } const auto native = static_cast(thread->nativeHandle); auto queued = std::make_unique(Delivery{handler, signum, {}}); - if (SuspendThread(native) == static_cast(-1)) throw std::runtime_error("sceKernelRaiseException: cannot suspend the target thread"); + if (SuspendThread(native) == static_cast(-1)) { + if (Exited(native)) return false; + throw std::runtime_error("sceKernelRaiseException: cannot suspend the target thread"); + } + if (Exited(native)) { + ResumeThread(native); + return false; + } if (thread->waitCount.load(std::memory_order_seq_cst) > 0) { const bool accepted = QueueUserAPC(WaitingEntry, native, reinterpret_cast(queued.get())) != 0; ResumeThread(native); if (!accepted) throw std::runtime_error("sceKernelRaiseException: cannot queue delivery to the waiting thread"); queued.release(); - return; + return true; } alignas(16) Delivery delivery{handler, signum, {}}; delivery.context.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT; @@ -223,6 +234,7 @@ void RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { throw std::runtime_error("sceKernelRaiseException: cannot redirect the target thread"); } ResumeThread(native); + return true; } #endif @@ -277,8 +289,7 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) { const auto handler = Handler(signum); if (handler == nullptr) throw std::runtime_error("sceKernelRaiseException: no handler installed for the signal"); #ifdef _WIN32 - RaiseOn(thread, handler, signum); - return 0; + return RaiseOn(thread, handler, signum) ? 0 : SCE_KERNEL_ERROR_ESRCH; #else NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 90fd466ad..fd8813901 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -22,6 +22,14 @@ thread_local std::atomic* waitState = &ownWaitState; #ifdef _WIN32 +using NtTestAlertFunction = LONG(NTAPI*)(); + +void DrainApcs() { + static const auto testAlert = reinterpret_cast(reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtTestAlert"))); + if (testAlert) testAlert(); + else SleepEx(0, TRUE); +} + DWORD AlertableWait(DWORD count, const HANDLE* handles, DWORD milliseconds) { waitState->fetch_add(1, std::memory_order_seq_cst); DWORD result; @@ -29,6 +37,7 @@ DWORD AlertableWait(DWORD count, const HANDLE* handles, DWORD milliseconds) { result = WaitForMultipleObjectsEx(count, handles, FALSE, milliseconds, TRUE); } while (result == WAIT_IO_COMPLETION); waitState->fetch_sub(1, std::memory_order_seq_cst); + DrainApcs(); return result; } @@ -36,6 +45,7 @@ void AlertableSleep(DWORD milliseconds) { waitState->fetch_add(1, std::memory_order_seq_cst); SleepEx(milliseconds, TRUE); waitState->fetch_sub(1, std::memory_order_seq_cst); + DrainApcs(); } void WINAPI DestroyWaiter(void* value); @@ -169,7 +179,7 @@ bool Condition::waitSignalUntil(Waiter* waiter, std::uint64_t deadlineNanos) { unlink(waiter); return false; } - EventSet(waiter->event, 0); + WaitForSingleObject(waiter->event, 0); return true; } diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index 3a8913473..4d17541a8 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -85,6 +85,21 @@ static void* APS5_VABI HostBlocked(void* arg) { return nullptr; } +static constexpr int LeavingRounds = 200; +static std::atomic leavingRound{0}; + +static void* APS5_VABI Leaving(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + for (int round = 0; round < LeavingRounds; ++round) { + worker.started.store(true); + Require(sceKernelWaitSema(worker.sem, 1, nullptr) == 0); + volatile std::uint64_t spins = 0; + while (leavingRound.load() == round) spins = spins + 1; + } + return nullptr; +} + static std::atomic finishedReturned{false}; static void* APS5_VABI Finished(void*) { @@ -156,6 +171,23 @@ int main() { hostLock.unlock(); Require(scePthreadJoin(blockedThread, nullptr) == 0); + Worker leaving; + Require(sceKernelCreateSema(&leaving.sem, "leaving", 0, 0, 1, nullptr) == 0); + Pthread leavingThread = nullptr; + Require(scePthreadCreate(&leavingThread, nullptr, Leaving, &leaving, "leaving") == 0); + for (int round = 0; round < LeavingRounds; ++round) { + while (!leaving.started.load()) std::this_thread::yield(); + leaving.started.store(false); + std::this_thread::sleep_for(std::chrono::microseconds(200)); + Require(sceKernelSignalSema(leaving.sem, 1) == 0); + for (int spin = 0; spin < round % 16 * 64; ++spin) std::this_thread::yield(); + Require(sceKernelRaiseException(leavingThread, SIGUSR1) == 0); + ExpectDelivery(1 + 2 * Repeats + HostRounds + round, leaving.id); + leavingRound.store(round + 1); + } + Require(scePthreadJoin(leavingThread, nullptr) == 0); + Require(sceKernelDeleteSema(leaving.sem) == 0); + Pthread finishedThread = nullptr; Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0); while (!finishedReturned.load()) std::this_thread::yield();