mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user