mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 11:26:56 +02:00
Keep signal APC frames out of the guest red zone on Windows
sceKernelRaiseException delivers SIGUSR1 as a Windows special user APC. Windows builds the APC frame directly below the interrupted RSP, which overwrites live red-zone locals of SysV leaf functions. PPSA10595 (UE5) signals worker threads while they run a zstd Huffman decoder whose tableLog slot sits below RSP; the slot came back as zero and the decoder faulted on a wild table index. Suspend the target before queuing the APC and, when it is in guest code, move RSP below the red zone with RIP parked on a "ret 0x80" stub that restores it in one instruction. The APC handler unparks the context first, so the guest handler sees and resumes at the real RIP/RSP. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+78
-1
@@ -1032,7 +1032,74 @@ static NtQueueApcThreadExFunc GetNtQueueApcThreadEx() {
|
||||
return nt_queue_apc_thread_ex;
|
||||
}
|
||||
|
||||
// Windows builds a user APC frame directly below the interrupted RSP, but SysV guest code may keep
|
||||
// live locals in the 128-byte red zone there. Before a signal APC is queued at guest code, the
|
||||
// target's RSP is moved below the red zone and RIP parked on "ret 0x80", which undoes the move in
|
||||
// one instruction. The APC handler undoes it itself when the APC arrives first.
|
||||
constexpr uint64_t SIGNAL_RED_ZONE_SKIP = 128 + sizeof(uint64_t);
|
||||
|
||||
static uint64_t GetSignalRedZoneStub() {
|
||||
static const uint64_t stub = []() -> uint64_t {
|
||||
constexpr SIZE_T size = 0x1000;
|
||||
auto* code = static_cast<uint8_t*>(
|
||||
VirtualAlloc(nullptr, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
if (code == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
code[0] = 0xc2; // ret 0x80
|
||||
code[1] = 0x80;
|
||||
code[2] = 0x00;
|
||||
DWORD old = 0;
|
||||
if (VirtualProtect(code, size, PAGE_EXECUTE_READ, &old) == FALSE) {
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), code, size);
|
||||
return reinterpret_cast<uint64_t>(code);
|
||||
}();
|
||||
return stub;
|
||||
}
|
||||
|
||||
// The target must be suspended, so nothing here may take a lock it could hold (the stub is
|
||||
// created beforehand). Returns true when its guest red zone is out of the APC's way.
|
||||
static bool ParkThreadBelowGuestRedZone(HANDLE thread, uint64_t stub) {
|
||||
if (stub == 0) {
|
||||
return false;
|
||||
}
|
||||
CONTEXT context {};
|
||||
context.ContextFlags = CONTEXT_CONTROL;
|
||||
if (GetThreadContext(thread, &context) == FALSE) {
|
||||
return false;
|
||||
}
|
||||
if (context.Rip == stub) {
|
||||
return true; // An earlier signal already parked it.
|
||||
}
|
||||
if (!IsGuestCodeAddress(context.Rip)) {
|
||||
return false; // Host code follows the Windows ABI and has no red zone.
|
||||
}
|
||||
const uint64_t parked_rsp = context.Rsp - SIGNAL_RED_ZONE_SKIP;
|
||||
const uint64_t resume_rip = context.Rip;
|
||||
if (WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(parked_rsp), &resume_rip,
|
||||
sizeof(resume_rip), nullptr) == FALSE) {
|
||||
return false;
|
||||
}
|
||||
context.Rsp = parked_rsp;
|
||||
context.Rip = stub;
|
||||
return SetThreadContext(thread, &context) != FALSE;
|
||||
}
|
||||
|
||||
static void UnparkSignalContext(PCONTEXT context) {
|
||||
const uint64_t stub = GetSignalRedZoneStub();
|
||||
if (context != nullptr && stub != 0 && context->Rip == stub) {
|
||||
context->Rip = *reinterpret_cast<const uint64_t*>(context->Rsp);
|
||||
context->Rsp += SIGNAL_RED_ZONE_SKIP;
|
||||
}
|
||||
}
|
||||
|
||||
static void SignalApcHandler(void* arg1, void* arg2, void* /*arg3*/, PCONTEXT context) {
|
||||
// Resume at the interrupted guest instruction, not the parking stub, and show the handler the
|
||||
// real guest context.
|
||||
UnparkSignalContext(context);
|
||||
|
||||
auto* thread = static_cast<Pthread>(arg1);
|
||||
const auto signum = static_cast<int>(reinterpret_cast<intptr_t>(arg2));
|
||||
if (!PthreadTakePendingSignal(thread, signum)) {
|
||||
@@ -1143,7 +1210,8 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
|
||||
}
|
||||
|
||||
HANDLE target_thread =
|
||||
OpenThread(THREAD_SET_CONTEXT, FALSE, static_cast<DWORD>(target_thread_id));
|
||||
OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME, FALSE,
|
||||
static_cast<DWORD>(target_thread_id));
|
||||
if (target_thread == nullptr) {
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
@@ -1152,9 +1220,18 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
|
||||
KytyUserApcOption option {};
|
||||
option.UserApcFlags = KytyQueueUserApcFlagsSpecialUserApc;
|
||||
|
||||
// Queue while suspended so the APC is delivered at the parked context on resume.
|
||||
const uint64_t red_zone_stub = GetSignalRedZoneStub();
|
||||
const bool suspended = SuspendThread(target_thread) != static_cast<DWORD>(-1);
|
||||
if (suspended) {
|
||||
ParkThreadBelowGuestRedZone(target_thread, red_zone_stub);
|
||||
}
|
||||
const auto status =
|
||||
nt_queue_apc_thread_ex(target_thread, option, SignalApcHandler, thread,
|
||||
reinterpret_cast<void*>(static_cast<intptr_t>(signum)), nullptr);
|
||||
if (suspended) {
|
||||
ResumeThread(target_thread);
|
||||
}
|
||||
|
||||
if (status != 0) {
|
||||
PthreadTakePendingSignal(thread, signum);
|
||||
|
||||
Reference in New Issue
Block a user