From 5031a4d2baff15fe3d2d662549cbe2c9dafe5052 Mon Sep 17 00:00:00 2001 From: J1coding Date: Sat, 25 Jul 2026 16:19:33 +0200 Subject: [PATCH] iOS: gate the JIT keepalive canary on the VM being parked The canary flip from the keepalive fix drops execute on the arena's first page, but the didBecomeActive prewarm re-runs it on every app switch while the CPU thread is executing the dispatcher on that exact page -> Instruction Abort. The old 'every caller runs parked' claim was only a comment; now ValidateJITAlive asks the scene layer for real VM/worker state and skips the probe (canary=skipped-vm-active) while anything JIT is running or still initializing. A live VM is its own proof the grant works. --- common/Darwin/DarwinMisc.cpp | 38 ++++++++++++------- common/Darwin/DarwinMisc.h | 5 +++ .../ios/app/src/main/cpp/IOS/SceneDelegate.mm | 24 +++++++++++- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/common/Darwin/DarwinMisc.cpp b/common/Darwin/DarwinMisc.cpp index e680a51c37..504576298e 100644 --- a/common/Darwin/DarwinMisc.cpp +++ b/common/Darwin/DarwinMisc.cpp @@ -749,6 +749,15 @@ bool DarwinMisc::IsJITAvailable() #endif } +// Set by the platform layer at scene connect, before the worker that allocates +// the arena exists, so the canary below can tell whether anything JIT is live. +static bool (*s_jit_activity_query)() = nullptr; + +void DarwinMisc::SetJITActivityQuery(bool (*query)()) +{ + s_jit_activity_query = query; +} + bool DarwinMisc::ValidateJITAlive() { #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR @@ -764,21 +773,24 @@ bool DarwinMisc::ValidateJITAlive() } // Check 2: JIT code memory still writable? Write a canary, read it back. - // - // Under a dual-mapping (g_code_rw_offset != 0) g_code_rw_base is the RW - // alias — writable by construction, and a dead alias is exactly what this - // probe detects. Under an identity mapping (offset == 0) the base IS the - // live RX code page (the EE dispatcher sits at arena offset 0 once the VM - // has prewarmed), so the store needs a real write scope: a bare store - // faults with KERN_PROTECTION_FAILURE on the main thread (boot-gate crash, - // 2026-07-25). In Legacy mode flip just the first page RW and back via - // mprotect, treating a failed flip as "grant died" (alive=0) instead of - // letting the write SIGBUS; in the MAP_JIT toggle mode use the per-thread - // Begin/EndCodeWrite. Every caller runs while the VM is parked (the - // keepalive timer skips when the VM thread is active), so briefly dropping - // execute on that page cannot race JIT execution. + // Under a dual-mapping the base is the RW alias and a dead alias is exactly + // what this detects. Under an identity mapping it is the live RX dispatcher + // page, so Legacy flips just that page RW and back via mprotect, treating a + // failed flip as "grant died" (alive=0) instead of letting the store SIGBUS; + // the MAP_JIT toggle mode uses the per-thread Begin/EndCodeWrite. if (g_code_rw_base != 0 && g_code_rw_size > 0) { + // Never touch a page a JIT thread might be executing: the Legacy flip + // drops execute on the dispatcher page, and the store rewrites its + // first instruction in every mode. A running VM is proof enough that + // the grant works, so skip the probe and say so in the log. + if (s_jit_activity_query && s_jit_activity_query()) + { + std::fprintf(stderr, "@@JIT_KEEPALIVE@@ alive=1 cs_debugged=1 canary=skipped-vm-active\n"); + std::fflush(stderr); + return true; + } + volatile u8* canary = reinterpret_cast(g_code_rw_base); #ifdef ARCH_ARM64 const bool identity = (g_code_rw_offset == 0); diff --git a/common/Darwin/DarwinMisc.h b/common/Darwin/DarwinMisc.h index ba162f0544..8084ec6eb5 100644 --- a/common/Darwin/DarwinMisc.h +++ b/common/Darwin/DarwinMisc.h @@ -112,6 +112,11 @@ struct CPUClass { /// Returns false if iOS has revoked the JIT grant since boot. bool ValidateJITAlive(); + /// Registered by the platform layer. Returns true while any thread may be + /// executing or emitting JIT code; ValidateJITAlive skips its arena canary + /// while that holds, since the probe touches the live dispatcher page. + void SetJITActivityQuery(bool (*query)()); + // [P43] iOS 26 Dual-Mapping JIT enum class JitMode { Simulator, // MAP_JIT + pthread_jit_write_protect_np diff --git a/platforms/ios/app/src/main/cpp/IOS/SceneDelegate.mm b/platforms/ios/app/src/main/cpp/IOS/SceneDelegate.mm index 0d867dff47..d2eed86a8e 100644 --- a/platforms/ios/app/src/main/cpp/IOS/SceneDelegate.mm +++ b/platforms/ios/app/src/main/cpp/IOS/SceneDelegate.mm @@ -59,12 +59,19 @@ #import "IOS/ARMSX2GameView.h" #import "IOS/PCSX2SceneDelegate.h" +// Defined below, next to the VM worker state it reads. +static bool ARMSX2JITWorkerBusy(); + @implementation PCSX2SceneDelegate #pragma mark - Scene connection & bootstrap - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { if (![scene isKindOfClass:[UIWindowScene class]]) return; - + + // Let ValidateJITAlive see VM/worker state before any of its callers can + // run (they are all scene-driven, so this is always first). + DarwinMisc::SetJITActivityQuery(&ARMSX2JITWorkerBusy); + UIWindowScene *windowScene = (UIWindowScene *)scene; // --- SDL Initialization --- @@ -622,6 +629,21 @@ static std::atomic s_vmInitComplete{false}; static std::atomic s_vmThreadShouldExit{false}; static std::atomic s_idleVMPrewarmResolved{false}; +// True while some thread may be executing or emitting JIT code: the VM is +// running, or the worker is still inside CPUThreadInitialize. ValidateJITAlive +// skips its arena canary while this holds — the didBecomeActive prewarm re-runs +// the keepalive on every app switch, and flipping the dispatcher page under a +// live CPU thread is an instant Instruction Abort. +static bool ARMSX2JITWorkerBusy() +{ + if (s_vmThreadActive.load(std::memory_order_relaxed)) + return true; + // Before the worker exists the arena doesn't either (the canary is + // already skipped on g_code_rw_base==0), so "init not complete" only + // bites while CPUThreadInitialize is actually in flight. + return !s_vmInitComplete.load(std::memory_order_relaxed); +} + static void ARMSX2ResolveIdleVMPrewarm() { bool expected = false;