From 3cd2ff91402b6c6f14565ae71a5d1db4a6442ab2 Mon Sep 17 00:00:00 2001 From: Merijn Date: Tue, 6 Oct 2026 15:41:25 +0200 Subject: [PATCH] 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); }