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.
This commit is contained in:
J1coding
2026-07-26 13:48:45 +02:00
committed by Jeen
parent 54191be600
commit 5031a4d2ba
3 changed files with 53 additions and 14 deletions
+25 -13
View File
@@ -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<volatile u8*>(g_code_rw_base);
#ifdef ARCH_ARM64
const bool identity = (g_code_rw_offset == 0);
+5
View File
@@ -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
@@ -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<bool> s_vmInitComplete{false};
static std::atomic<bool> s_vmThreadShouldExit{false};
static std::atomic<bool> 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;