fix(libkernel): keep ymm upper halves across a redirected raise

A thread that sceKernelRaiseException redirects resumes through
RtlRestoreContext with a CONTEXT that has no XSTATE, so the upper halves
of ymm0-ymm15 came back as the handler and the delivery path left them.
A handler or host routine that runs vzeroupper or any VEX instruction
(Zen 2 guest code, the CRT's memcpy) cleared the upper lanes of a thread
interrupted in the middle of AVX code. The entry stub now stores
ymm0-ymm15 next to the delivery when the CPU and the OS enable AVX, and
the exit loads them back right before RtlRestoreContext, which restores
only the low halves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
This commit is contained in:
clem-gb 2026-10-08 09:04:17 +02:00
parent a5df1a874d
commit 15c1962e82
3 changed files with 130 additions and 6 deletions

View File

@ -16,6 +16,7 @@
extern "C" Pthread APS5_VABI scePthreadSelf();
#ifdef _WIN32
extern "C" void Aps5RedirectedEntryStub();
extern "C" [[noreturn]] void Aps5RedirectedExit(CONTEXT* context, const void* vectors, void (*restore)(CONTEXT*, EXCEPTION_RECORD*));
#endif
namespace {
@ -101,13 +102,21 @@ GuestExceptionHandler Handler(int signum) {
#ifdef _WIN32
constexpr std::size_t RedZone = 128;
constexpr std::size_t HomeArea = 32;
constexpr std::size_t VectorBytes = 16 * 32;
struct Delivery {
GuestExceptionHandler handler;
int signum;
CONTEXT context;
std::uint8_t vectors[VectorBytes];
std::uint64_t saveVectors;
};
const bool SaveVectors = [] {
__builtin_cpu_init();
return __builtin_cpu_supports("avx") != 0;
}();
void Deliver(GuestExceptionHandler handler, int signum, CONTEXT& context) {
GuestUcontext ucontext{};
auto& m = ucontext.mcontext;
@ -159,8 +168,7 @@ void Deliver(GuestExceptionHandler handler, int signum, CONTEXT& context) {
[[noreturn]] void RedirectedEntry(Delivery* delivery) {
CONTEXT context = delivery->context;
Deliver(delivery->handler, delivery->signum, context);
RtlRestoreContext(&context, nullptr);
std::abort();
Aps5RedirectedExit(&context, delivery->saveVectors != 0 ? delivery->vectors : nullptr, RtlRestoreContext);
}
bool StackWritable(DWORD64 low, DWORD64 high) {
@ -185,6 +193,7 @@ 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");
static_assert(offsetof(Delivery, vectors) == 1248 && offsetof(Delivery, saveVectors) == 1760, "Aps5RedirectedEntryStub stores ymm0-ymm15 into the delivery when saveVectors is set");
bool Exited(HANDLE native) {
return WaitForSingleObject(native, 0) == WAIT_OBJECT_0;
@ -198,7 +207,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) {
return true;
}
const auto native = static_cast<HANDLE>(thread->nativeHandle);
auto queued = std::make_unique<Delivery>(Delivery{handler, signum, {}});
auto queued = std::make_unique<Delivery>(Delivery{handler, signum, {}, {}, 0});
if (SuspendThread(native) == static_cast<DWORD>(-1)) {
if (Exited(native)) return false;
throw std::runtime_error("sceKernelRaiseException: cannot suspend the target thread");
@ -214,7 +223,7 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) {
queued.release();
return true;
}
alignas(16) Delivery delivery{handler, signum, {}};
alignas(16) Delivery delivery{handler, signum, {}, {}, SaveVectors ? 1u : 0u};
delivery.context.ContextFlags = CONTEXT_FULL | CONTEXT_FLOATING_POINT;
if (!GetThreadContext(native, &delivery.context)) {
ResumeThread(native);
@ -262,8 +271,50 @@ asm(".text\n"
" movq %r13, 280(%rsp)\n"
" movq %r14, 288(%rsp)\n"
" movq %r15, 296(%rsp)\n"
" cmpq $0, 1800(%rsp)\n"
" je 1f\n"
" vmovdqu %ymm0, 1288(%rsp)\n"
" vmovdqu %ymm1, 1320(%rsp)\n"
" vmovdqu %ymm2, 1352(%rsp)\n"
" vmovdqu %ymm3, 1384(%rsp)\n"
" vmovdqu %ymm4, 1416(%rsp)\n"
" vmovdqu %ymm5, 1448(%rsp)\n"
" vmovdqu %ymm6, 1480(%rsp)\n"
" vmovdqu %ymm7, 1512(%rsp)\n"
" vmovdqu %ymm8, 1544(%rsp)\n"
" vmovdqu %ymm9, 1576(%rsp)\n"
" vmovdqu %ymm10, 1608(%rsp)\n"
" vmovdqu %ymm11, 1640(%rsp)\n"
" vmovdqu %ymm12, 1672(%rsp)\n"
" vmovdqu %ymm13, 1704(%rsp)\n"
" vmovdqu %ymm14, 1736(%rsp)\n"
" vmovdqu %ymm15, 1768(%rsp)\n"
"1:\n"
" leaq 40(%rsp), %rcx\n"
" jmp Aps5RedirectedEntry\n");
" jmp Aps5RedirectedEntry\n"
".globl Aps5RedirectedExit\n"
"Aps5RedirectedExit:\n"
" testq %rdx, %rdx\n"
" je 1f\n"
" vmovdqu 0(%rdx), %ymm0\n"
" vmovdqu 32(%rdx), %ymm1\n"
" vmovdqu 64(%rdx), %ymm2\n"
" vmovdqu 96(%rdx), %ymm3\n"
" vmovdqu 128(%rdx), %ymm4\n"
" vmovdqu 160(%rdx), %ymm5\n"
" vmovdqu 192(%rdx), %ymm6\n"
" vmovdqu 224(%rdx), %ymm7\n"
" vmovdqu 256(%rdx), %ymm8\n"
" vmovdqu 288(%rdx), %ymm9\n"
" vmovdqu 320(%rdx), %ymm10\n"
" vmovdqu 352(%rdx), %ymm11\n"
" vmovdqu 384(%rdx), %ymm12\n"
" vmovdqu 416(%rdx), %ymm13\n"
" vmovdqu 448(%rdx), %ymm14\n"
" vmovdqu 480(%rdx), %ymm15\n"
"1:\n"
" xorl %edx, %edx\n"
" jmp *%r8\n");
#endif
extern "C" {

View File

@ -32,6 +32,7 @@ static std::atomic<int> calls{0};
static std::atomic<std::thread::id> handlerThread;
static std::atomic<std::uint64_t> handlerRsp{0};
static std::atomic<std::uintptr_t> handlerFrame{0};
static std::atomic<bool> clobberVectors{false};
static void APS5_VABI Handler(int signum, void* context) {
Require(signum == SIGUSR1);
@ -41,6 +42,7 @@ static void APS5_VABI Handler(int signum, void* context) {
handlerFrame.store(reinterpret_cast<std::uintptr_t>(&local));
handlerRsp.store(rsp);
handlerThread.store(std::this_thread::get_id());
if (clobberVectors.load()) asm volatile("vzeroall" ::: "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15");
calls.fetch_add(1);
}
@ -102,6 +104,60 @@ static void* APS5_VABI Leaving(void* arg) {
return nullptr;
}
static constexpr int VectorRounds = 100;
static constexpr std::size_t VectorBytes = 16 * 32;
alignas(32) static std::uint8_t vectorPattern[VectorBytes];
alignas(32) static std::uint8_t vectorResult[VectorBytes];
static volatile std::uint8_t vectorStop = 0;
static void* APS5_VABI Vectors(void* arg) {
auto& worker = *static_cast<Worker*>(arg);
worker.id = std::this_thread::get_id();
worker.started.store(true);
asm volatile(
"vmovdqu 0(%[pattern]), %%ymm0\n"
"vmovdqu 32(%[pattern]), %%ymm1\n"
"vmovdqu 64(%[pattern]), %%ymm2\n"
"vmovdqu 96(%[pattern]), %%ymm3\n"
"vmovdqu 128(%[pattern]), %%ymm4\n"
"vmovdqu 160(%[pattern]), %%ymm5\n"
"vmovdqu 192(%[pattern]), %%ymm6\n"
"vmovdqu 224(%[pattern]), %%ymm7\n"
"vmovdqu 256(%[pattern]), %%ymm8\n"
"vmovdqu 288(%[pattern]), %%ymm9\n"
"vmovdqu 320(%[pattern]), %%ymm10\n"
"vmovdqu 352(%[pattern]), %%ymm11\n"
"vmovdqu 384(%[pattern]), %%ymm12\n"
"vmovdqu 416(%[pattern]), %%ymm13\n"
"vmovdqu 448(%[pattern]), %%ymm14\n"
"vmovdqu 480(%[pattern]), %%ymm15\n"
"1:\n"
"pause\n"
"cmpb $0, (%[stop])\n"
"je 1b\n"
"vmovdqu %%ymm0, 0(%[result])\n"
"vmovdqu %%ymm1, 32(%[result])\n"
"vmovdqu %%ymm2, 64(%[result])\n"
"vmovdqu %%ymm3, 96(%[result])\n"
"vmovdqu %%ymm4, 128(%[result])\n"
"vmovdqu %%ymm5, 160(%[result])\n"
"vmovdqu %%ymm6, 192(%[result])\n"
"vmovdqu %%ymm7, 224(%[result])\n"
"vmovdqu %%ymm8, 256(%[result])\n"
"vmovdqu %%ymm9, 288(%[result])\n"
"vmovdqu %%ymm10, 320(%[result])\n"
"vmovdqu %%ymm11, 352(%[result])\n"
"vmovdqu %%ymm12, 384(%[result])\n"
"vmovdqu %%ymm13, 416(%[result])\n"
"vmovdqu %%ymm14, 448(%[result])\n"
"vmovdqu %%ymm15, 480(%[result])\n"
"vzeroupper\n"
:
: [pattern] "r"(vectorPattern), [result] "r"(vectorResult), [stop] "r"(&vectorStop)
: "memory", "cc", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15");
return nullptr;
}
static std::atomic<bool> finishedReturned{false};
static void* APS5_VABI Finished(void*) {
@ -191,6 +247,24 @@ int main() {
Require(scePthreadJoin(leavingThread, nullptr) == 0);
Require(sceKernelDeleteSema(leaving.sem) == 0);
if (__builtin_cpu_supports("avx")) {
for (std::size_t i = 0; i < VectorBytes; ++i) vectorPattern[i] = static_cast<std::uint8_t>(i * 7 + 1);
clobberVectors.store(true);
Worker vectors;
Pthread vectorsThread = nullptr;
Require(scePthreadCreate(&vectorsThread, nullptr, Vectors, &vectors, "vectors") == 0);
while (!vectors.started.load()) std::this_thread::yield();
std::this_thread::sleep_for(std::chrono::milliseconds(20));
for (int raised = 0; raised < VectorRounds; ++raised) {
Require(sceKernelRaiseException(vectorsThread, SIGUSR1) == 0);
ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + raised, vectors.id);
}
clobberVectors.store(false);
vectorStop = 1;
Require(scePthreadJoin(vectorsThread, nullptr) == 0);
Require(std::memcmp(vectorPattern, vectorResult, VectorBytes) == 0);
}
Pthread finishedThread = nullptr;
Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0);
while (!finishedReturned.load()) std::this_thread::yield();

View File

@ -237,7 +237,6 @@ Throughout the project, every function at every stage either **does exactly what
- [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