Make a guest call survivable when the callee misbehaves

A faulting call was stopping the whole emulator, because exception()
honours the first-chance mask and that ends in g_system->pause(true).
For a harness deliberately feeding a routine broken input, the first bad
input killed the run - measured against a real BIOS boot, where the shell
was simply never reached. The fault is a result, not an incident: park
the mask for the duration and report the code, EPC and BadVAddr back.

Also refuse to nest. The dangerous caller is one of the Lua memory hooks,
which fire from Memory::read/write inside the callee and can call straight
back in; re-entering execBlock on top of a live one shreds the delayed
load ping-pong underneath it. Park and restore that bookkeeping too, since
even a clean boundary can have a branch pending the callee must not
inherit.
This commit is contained in:
Rix
2026-10-11 01:18:32 -07:00
parent 9bc8e442b9
commit 00a60960eb
3 changed files with 47 additions and 0 deletions
+8
View File
@@ -314,6 +314,14 @@ void PCSX::LuaFFI::open_pcsx(Lua L) {
regs.pc = pc;
auto outcome = g_emulator->m_cpu->RunUntil(ra, cycles);
if (outcome == R3000Acpu::RunUntilResult::Reentered) {
regs.GPR = savedGPR;
regs.pc = savedPC;
return L.error(
"callGuest can't be nested: this one was called from inside another guest call, most likely from "
"a breakpoint invoker that fired during it. An ExecutionFlow event listener is a fine place to "
"call from; the middle of an instruction is not.");
}
if (outcome == R3000Acpu::RunUntilResult::Unsupported) {
regs.GPR = savedGPR;
regs.pc = savedPC;
+37
View File
@@ -1653,6 +1653,36 @@ void InterpretedCPU::Execute() {
PCSX::R3000Acpu::RunUntilResult InterpretedCPU::RunUntil(uint32_t stopPC, uint64_t cycleBudget) {
ZoneScoped;
/* Nesting would re-enter execBlock on top of a live one and shred the delayed-load
ping-pong underneath it. The dangerous caller is a breakpoint invoker, which runs from
Debug::process in the middle of execBlock's loop body; an ExecutionFlow event listener
is fine, since those fire from hasToRun() between blocks, which is the same point
UI::shellReached does its own wholesale register surgery from. */
if (m_inRunUntil) return RunUntilResult::Reentered;
m_inRunUntil = true;
/* Even at a clean boundary a branch may have left a delay slot pending, and the callee
must not inherit it. Park the bookkeeping and hand it back untouched. */
const bool savedNextIsDelaySlot = m_nextIsDelaySlot;
const bool savedInDelaySlot = m_inDelaySlot;
const unsigned savedCurrentDelayedLoad = m_currentDelayedLoad;
const auto savedDelayedLoad0 = m_delayedLoadInfo[0];
const auto savedDelayedLoad1 = m_delayedLoadInfo[1];
m_nextIsDelaySlot = false;
m_inDelaySlot = false;
m_delayedLoadInfo[0].active = false;
m_delayedLoadInfo[1].active = false;
m_delayedLoadInfo[0].pcActive = false;
m_delayedLoadInfo[1].pcActive = false;
/* exception() honours the first-chance mask by printing a line and calling
g_system->pause(true), which stops the WHOLE emulator - including a -run that some
other part of the harness is relying on. A call that faults is a result here, not an
incident: we report the code, EPC and BadVAddr in the return value and leave the
machine alone. Park the mask for the duration. */
auto &debugSettings = PCSX::g_emulator->settings.get<PCSX::Emulator::SettingDebugSettings>();
const uint32_t savedFirstChance = debugSettings.get<PCSX::Emulator::DebugSettings::FirstChanceException>();
debugSettings.get<PCSX::Emulator::DebugSettings::FirstChanceException>() = 0;
const uint64_t deadline = m_regs.cycle + cycleBudget;
/* m_inISR is how we hear about an exception: exception() sets it unconditionally, and
it covers the cop0 vectors too, which enumerating the four vector addresses wouldn't.
@@ -1672,7 +1702,14 @@ PCSX::R3000Acpu::RunUntilResult InterpretedCPU::RunUntil(uint32_t stopPC, uint64
break;
}
}
debugSettings.get<PCSX::Emulator::DebugSettings::FirstChanceException>() = savedFirstChance;
m_inISR = wasInISR;
m_nextIsDelaySlot = savedNextIsDelaySlot;
m_inDelaySlot = savedInDelaySlot;
m_currentDelayedLoad = savedCurrentDelayedLoad;
m_delayedLoadInfo[0] = savedDelayedLoad0;
m_delayedLoadInfo[1] = savedDelayedLoad1;
m_inRunUntil = false;
return result;
}
+2
View File
@@ -263,6 +263,7 @@ class R3000Acpu {
OutOfCycles, /* the budget ran out first */
Exception, /* the guest took an exception */
Unsupported, /* this cpu can't do it */
Reentered, /* called from inside another RunUntil */
};
/* Runs until pc reaches stopPC, the cycle budget is spent, or an exception is taken.
Unlike Execute(), this doesn't run branchTest(), so counters, scheduled interrupts
@@ -391,6 +392,7 @@ class R3000Acpu {
m_regs.scheduleMask = 0;
}
bool m_inISR = false;
bool m_inRunUntil = false;
bool m_nextIsDelaySlot = false;
bool m_inDelaySlot = false;
struct {