iOS: fix JIT keepalive timer running during gameplay

The keepalive timer was stopped before VMManager::Initialize and skipped
validation while s_vmThreadActive was true. This was wrong: iOS can revoke
CS_DEBUGGED at any time, including mid-frame during active gameplay. When
revocation happened during gameplay, the protection on code and data pages
was silently flipped, crashing the CPU thread (instruction abort), GS thread
(data abort write fault on shared memory), and MTVU thread (translation
fault at null) simultaneously.

Fix: keep the timer running continuously at its 12-second interval during
all app states, including active gameplay. The validation cost is trivial
(one csops syscall plus one byte canary write). When revocation is detected
during gameplay, the timer posts a JITExpired notification and stops — the
next boot attempt will fall back to interpreter mode.

Root cause traced from crash log analysis on iPad Pro M1 (iPad13,8, iOS
26.5): God of War II crashed at frame 71, ~12 seconds after JIT acquisition,
with SIGBUS KERN_PROTECTION_FAILURE on shared memory writes from the GS
thread. Three threads faulted simultaneously, confirming a grant revocation
rather than a single-thread bug.
This commit is contained in:
Jeen
2026-07-12 18:06:15 +02:00
parent d57633224d
commit b8e94ea84f
@@ -635,10 +635,11 @@ static void ARMSX2StartJITKeepalive()
dispatch_time(DISPATCH_TIME_NOW, 12 * NSEC_PER_SEC),
12 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(s_jitKeepaliveTimer, ^{
// Skip while VM is running — the recompiler constantly writes code,
// so JIT cannot expire during active gameplay.
if (s_vmThreadActive.load(std::memory_order_relaxed))
return;
// Check JIT on every interval, even during active gameplay.
// iOS can revoke CS_DEBUGGED at any time — including mid-frame —
// which flips protection on code and data pages and crashes the
// CPU, GS, and MTVU threads simultaneously. The canary write is
// cheap (one byte) and the csops check is a single syscall.
if (!DarwinMisc::ValidateJITAlive())
{
s_jitExpired.store(true);
@@ -1031,8 +1032,9 @@ static void ARMSX2StartJITKeepalive()
ARMSX2ApplyJITScriptProtocol("pre-vm-initialize");
// --- Initialize & Execute VM ---
// VM about to run — JIT is in active use, keepalive not needed.
ARMSX2StopJITKeepalive();
// Keep the keepalive timer running during gameplay. iOS can
// revoke CS_DEBUGGED mid-frame, and the timer detects it before
// a protection fault crashes the CPU/GS/MTVU threads.
Error bootError;
const VMBootResult bootResult = VMManager::Initialize(boot_params, &bootError);
const std::string bootErrorText = bootError.GetDescription();