From 4b6aea51e7a337e0f9601ddfbadf4eb6eb216d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 21 Oct 2026 14:35:00 +0200 Subject: [PATCH 1/2] IR interpreter: Halt with a memory exception on a bad guest access HandleFault only handled faults in JIT code, so a game's bad load or store under the IR interpreter crashed the app. Now IRInterpret marks itself (a thread-local, cleared around HLE calls, replacements and the MIPS interpreter fallback) and the handler steps over the faulting instruction, reports the exception and stops the core, or just ignores the access when IgnoreBadMemAccess is on. If the instruction can't be decoded it still crashes, as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/MIPS/IR/IRInterpreter.cpp | 24 ++++++++++++++++++++---- Core/MemFault.cpp | 33 +++++++++++++++++++++++++++++---- Core/MemFault.h | 17 +++++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/Core/MIPS/IR/IRInterpreter.cpp b/Core/MIPS/IR/IRInterpreter.cpp index ef01fc05f5..b399fe4a2a 100644 --- a/Core/MIPS/IR/IRInterpreter.cpp +++ b/Core/MIPS/IR/IRInterpreter.cpp @@ -16,6 +16,7 @@ #include "Core/HLE/HLE.h" #include "Core/HLE/ReplaceTables.h" #include "Core/HW/MMIO.h" +#include "Core/MemFault.h" #include "Core/MemMap.h" #include "Core/MIPS/MIPS.h" #include "Core/MIPS/MIPSTables.h" @@ -178,6 +179,8 @@ void IRRunKernelStore(MIPSState *mips, u32 addr, u32 value, u32 op) { u32 IRInterpret(MIPSState *mips, const IRInst *inst, const IRInst *chainArena) { const Memory::AddressSpace &space = *mips->space; + // Lets a bad guest access halt the core. Calls out to HLE and the like leave this scope. + Memory::IRInterpreterFaultScope faultScope(true); #if IR_THREADED_DISPATCH static const void *dispatch[256]; static bool dispatchReady = false; @@ -1448,7 +1451,10 @@ u32 IRInterpret(MIPSState *mips, const IRInst *inst, const IRInst *chainArena) { MIPSOpcode op(inst->constant); // HLE runs in the host's normal mode, see ApplyHostRoundingMode. RestoreHostRoundingMode(); - CallSyscall(mips, op); + { + Memory::IRInterpreterFaultScope outside(false); + CallSyscall(mips, op); + } ApplyHostRoundingMode(mips); if (coreState != CORE_RUNNING_CPU) { CoreTiming::ForceCheck(mips); @@ -1461,7 +1467,10 @@ u32 IRInterpret(MIPSState *mips, const IRInst *inst, const IRInst *chainArena) { // If we get here, the syscall is invalid. u32 pc = inst->constant; RestoreHostRoundingMode(); - CallSyscallUnresolvedAtPC(mips, pc); + { + Memory::IRInterpreterFaultScope outside(false); + CallSyscallUnresolvedAtPC(mips, pc); + } ApplyHostRoundingMode(mips); if (coreState != CORE_RUNNING_CPU) { // hm, what's this for? @@ -1476,7 +1485,10 @@ u32 IRInterpret(MIPSState *mips, const IRInst *inst, const IRInst *chainArena) { IR_CASE(Interpret) // SLOW fallback. Can be made faster. Ideally should be removed but may be useful for debugging. { MIPSOpcode op(inst->constant); - MIPSInterpret(mips, op); + { + Memory::IRInterpreterFaultScope outside(false); + MIPSInterpret(mips, op); + } IR_NEXT; } @@ -1485,7 +1497,11 @@ u32 IRInterpret(MIPSState *mips, const IRInst *inst, const IRInst *chainArena) { int funcIndex = inst->constant; const ReplacementTableEntry *f = GetReplacementFunc(funcIndex); RestoreHostRoundingMode(); - int cycles = f->replaceFunc(mips); + int cycles; + { + Memory::IRInterpreterFaultScope outside(false); + cycles = f->replaceFunc(mips); + } ApplyHostRoundingMode(mips); mips->r[inst->dest] = cycles < 0 ? -1 : 0; mips->downcount -= cycles < 0 ? -cycles : cycles; diff --git a/Core/MemFault.cpp b/Core/MemFault.cpp index 3fdf3adeb0..e164c7b875 100644 --- a/Core/MemFault.cpp +++ b/Core/MemFault.cpp @@ -58,6 +58,7 @@ static int64_t g_numReportedBadAccesses = 0; const uint8_t *g_lastCrashAddress; MemoryExceptionType g_lastMemoryExceptionType; static bool inCrashHandler = false; +thread_local bool g_inIRInterpreter = false; std::unordered_set g_ignoredAddresses; @@ -126,10 +127,11 @@ bool HandleFault(uintptr_t hostAddress, void *ctx) { // TODO: Check that codePtr is within the current JIT space. bool inJitSpace = mipsr4k.jit && mipsr4k.jit->CodeInRange(codePtr); - if (!inJitSpace) { - // This is a crash in non-jitted code. Not something we want to handle here, ignore. - // Actually, we could handle crashes from the IR interpreter here, although recovering the call stack - // might be tricky... + // The IR interpreter's own accesses to PSP memory. We can't redirect those to a crash handler, but we can + // step over the faulting instruction and stop the core. + const bool inIRInterpreter = !inJitSpace && g_inIRInterpreter; + if (!inJitSpace && !inIRInterpreter) { + // This is a crash in other non-jitted code. Not something we want to handle here, ignore. inCrashHandler = false; return false; } @@ -239,6 +241,18 @@ bool HandleFault(uintptr_t hostAddress, void *ctx) { type = MemoryExceptionType::UNKNOWN; } + if (inIRInterpreter && !success) { + // Without the instruction's size we can't step over it. + inCrashHandler = false; + return false; + } + if (inIRInterpreter && coreState == CORE_RUNTIME_ERROR) { + // Already reported, the interpreter just hasn't reached the end of the block yet. + context->CTX_PC += info.instructionSize; + inCrashHandler = false; + return true; + } + g_lastMemoryExceptionType = type; bool handled = true; @@ -276,6 +290,17 @@ bool HandleFault(uintptr_t hostAddress, void *ctx) { // just need to clean it up. Core_MemoryException(guestAddress, info.OperandSizeInBytes(), approximatePC, type, infoString, true); + if (inIRInterpreter) { + // Step over the access and stop the core, like the JITs' crash handlers do. The interpreter + // leaves at the next block exit, which checks the downcount. Resuming isn't possible, the guest + // has already run on past the access. + context->CTX_PC += info.instructionSize; + coreState = CORE_RUNTIME_ERROR; + currentMIPS->downcount = -1; + inCrashHandler = false; + return true; + } + // There's a small chance we can resume from this type of crash. g_lastCrashAddress = codePtr; diff --git a/Core/MemFault.h b/Core/MemFault.h index 546064dddd..1a2a770224 100644 --- a/Core/MemFault.h +++ b/Core/MemFault.h @@ -20,6 +20,23 @@ void MemFault_IgnoreLastCrash(); // just leave it as-is. bool HandleFault(uintptr_t hostAddress, void *context); +// Set on the thread running IRInterpret, while it runs its own loads and stores. A fault in those can +// be stepped over, which the IR interpreter does to halt cleanly instead of crashing. +extern thread_local bool g_inIRInterpreter; + +class IRInterpreterFaultScope { +public: + explicit IRInterpreterFaultScope(bool inInterpreter) : prev_(g_inIRInterpreter) { + g_inIRInterpreter = inInterpreter; + } + ~IRInterpreterFaultScope() { + g_inIRInterpreter = prev_; + } + +private: + bool prev_; +}; + } // Stack walk utility function, walks from the current state. Useful in the debugger and crash report screens etc. From c366da70d1371f1a59719841168505baa23c5fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 21 Oct 2026 14:40:45 +0200 Subject: [PATCH 2/2] IR interpreter: Make the fault flag a plain volatile global Only the CPU thread runs IRInterpret, so it doesn't need to be thread-local. Volatile keeps the compiler from dropping or sinking the stores, which nothing but the fault handler reads. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/MemFault.cpp | 2 +- Core/MemFault.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/MemFault.cpp b/Core/MemFault.cpp index e164c7b875..01d3e100de 100644 --- a/Core/MemFault.cpp +++ b/Core/MemFault.cpp @@ -58,7 +58,7 @@ static int64_t g_numReportedBadAccesses = 0; const uint8_t *g_lastCrashAddress; MemoryExceptionType g_lastMemoryExceptionType; static bool inCrashHandler = false; -thread_local bool g_inIRInterpreter = false; +volatile bool g_inIRInterpreter = false; std::unordered_set g_ignoredAddresses; diff --git a/Core/MemFault.h b/Core/MemFault.h index 1a2a770224..0c90ccdc41 100644 --- a/Core/MemFault.h +++ b/Core/MemFault.h @@ -20,9 +20,9 @@ void MemFault_IgnoreLastCrash(); // just leave it as-is. bool HandleFault(uintptr_t hostAddress, void *context); -// Set on the thread running IRInterpret, while it runs its own loads and stores. A fault in those can -// be stepped over, which the IR interpreter does to halt cleanly instead of crashing. -extern thread_local bool g_inIRInterpreter; +// Set while IRInterpret runs its own loads and stores. A fault in those can be stepped over, which the +// IR interpreter does to halt cleanly instead of crashing. Only the CPU thread runs it. +extern volatile bool g_inIRInterpreter; class IRInterpreterFaultScope { public: