mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
iOS: fix Legacy-mode SIGBUS on cross-window JIT code patches
Boot crash on iOS 18 under LiveContainer (Legacy W^X mode): CPU thread, EXC_BAD_ACCESS KERN_PROTECTION_FAILURE in Arm64BaseBlocks::New -- a str of a branch encoding into an r-x page. In Legacy mode armStartBlock flips only the current block's 1 MiB window to RW, but New()'s link-repoint loop, Remove()'s entry stubs and the backedge patch write into EARLIER blocks, whose windows are execute-protected by then. PatchWord trusted its callers to hold a Begin/EndCodeWrite scope; no caller on those paths did. The toggle modes (macOS, Simulator) masked it because the compile thread's write-protect is off arena-wide during recRecompile. Give the patch primitive its own scope instead: armPatchCodeWord stores through the dual-map alias and, when the target page is outside the open emit window, wraps the store in a page-granular Begin/EndCodeWriteRange. The page check matters -- the bump allocator routinely puts the previous block's link site on the same page as the current block's start, and RX-flipping that page mid-emit would kill the compile in a new way. The whole-arena Begin/EndCodeWrite scopes that only existed to cover those patches are gone with it. They were their own crash: Legacy range windows bypass the refcount, so recClear's paired EndCodeWrite RX-flipped the open emit window whenever the stale-overlap walk cleared blocks mid-compile, and recClearIOP paid a whole-arena mprotect pair per covered IOP store. The fastmem backpatch and the cold-island patches route through armPatchCodeWord for the same reason, and microVU's code-cache scope narrows to its own buffer span so an MTVU close can no longer RX-flip an EE emit window open on the other thread.
This commit is contained in:
@@ -191,6 +191,43 @@ u8* armEndBlock()
|
||||
return armAsmPtr;
|
||||
}
|
||||
|
||||
// Patch one instruction word at `site`, handling W^X itself instead of
|
||||
// trusting the caller to hold a write scope — link sites in earlier blocks
|
||||
// sit in windows armStartBlock never opened, which is a SIGBUS on iOS
|
||||
// Legacy mode. Pages inside the open emit window are stored to directly
|
||||
// (the previous block's link site often shares a page with the current
|
||||
// block's start, and RX-flipping that mid-emit would kill the compile);
|
||||
// anything else gets its own page-granular Begin/EndCodeWriteRange.
|
||||
// Aligned 4-byte stores are atomic on AArch64, and mprotect is fine from
|
||||
// the Mach exception-handler thread, so the fastmem-fault path can use it.
|
||||
void armPatchCodeWord(void* site, u32 instr)
|
||||
{
|
||||
u8* const rx_site = static_cast<u8*>(site);
|
||||
|
||||
bool in_open_window = false;
|
||||
if (s_arm_block_start)
|
||||
{
|
||||
static const uintptr_t page_mask = []() {
|
||||
size_t page_size = HostSys::GetRuntimePageSize();
|
||||
if (page_size == 0)
|
||||
page_size = 4096;
|
||||
return ~(static_cast<uintptr_t>(page_size) - 1);
|
||||
}();
|
||||
|
||||
const uintptr_t site_page = reinterpret_cast<uintptr_t>(rx_site) & page_mask;
|
||||
const uintptr_t first_page = reinterpret_cast<uintptr_t>(s_arm_block_start) & page_mask;
|
||||
const uintptr_t last_page =
|
||||
(reinterpret_cast<uintptr_t>(s_arm_block_start) + s_arm_block_write_size - 1) & page_mask;
|
||||
in_open_window = (site_page >= first_page && site_page <= last_page);
|
||||
}
|
||||
|
||||
if (!in_open_window)
|
||||
HostSys::BeginCodeWriteRange(rx_site, sizeof(u32));
|
||||
*reinterpret_cast<volatile u32*>(armGetWritableCodePtr(rx_site)) = instr;
|
||||
if (!in_open_window)
|
||||
HostSys::EndCodeWriteRange(rx_site, sizeof(u32));
|
||||
}
|
||||
|
||||
void armDisassembleAndDumpCode(const void* ptr, size_t size)
|
||||
{
|
||||
#ifdef INCLUDE_DISASSEMBLER
|
||||
@@ -254,13 +291,7 @@ void armEmitJmpPtr(void* code_address, const void* target, bool flush_icache)
|
||||
pxAssertRel((off & 3) == 0, "armEmitJmpPtr: branch offset not 4-byte aligned");
|
||||
const intptr_t imm26 = off >> 2;
|
||||
pxAssertRel(imm26 >= -(1 << 25) && imm26 < (1 << 25), "armEmitJmpPtr: branch offset out of B imm26 range");
|
||||
// code_address is the RX alias; under iOS dual-mapping the store must go
|
||||
// through the RW mirror. Begin/EndCodeWrite covers the toggle modes
|
||||
// (refcounted, so it nests inside an open emit scope).
|
||||
HostSys::BeginCodeWrite();
|
||||
*reinterpret_cast<volatile u32*>(armGetWritableCodePtr(static_cast<u8*>(code_address))) =
|
||||
0x14000000u | (static_cast<u32>(imm26) & 0x03FFFFFFu);
|
||||
HostSys::EndCodeWrite();
|
||||
armPatchCodeWord(code_address, 0x14000000u | (static_cast<u32>(imm26) & 0x03FFFFFFu));
|
||||
if (flush_icache)
|
||||
HostSys::FlushInstructionCache(code_address, 4);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,11 @@ u8* armEndBlock();
|
||||
void armDisassembleAndDumpCode(const void* ptr, size_t size);
|
||||
void armEmitJmp(const void* ptr, bool force_inline = false);
|
||||
void armEmitCall(const void* ptr, bool force_inline = false);
|
||||
// Store one instruction word into code memory, opening its own W^X scope when
|
||||
// the target lies outside the open emit window. Use for anything that patches
|
||||
// code outside the block being emitted — link sites, entry stubs, fastmem
|
||||
// backpatch. No cache maintenance: callers own the flush policy.
|
||||
void armPatchCodeWord(void* site, u32 instr);
|
||||
// In-place patch: overwrite the 4-byte B at `code_address` with a branch to
|
||||
// `target`. Used by EE block chaining to rewrite a link site (not tied to the
|
||||
// current emit cursor). `code_address` must already hold a single B instruction.
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
#include <map>
|
||||
|
||||
#include "common/HostSys.h"
|
||||
#include "arm64/AsmHelpers.h" // armGetWritableCodePtr (iOS dual-map W^X)
|
||||
#include "arm64/AsmHelpers.h" // armPatchCodeWord (self-scoped W^X code patching)
|
||||
#include "x86/BaseblockEx.h" // BASEBLOCK, BASEBLOCKEX, BaseBlockArray, recLUT_SetPage
|
||||
|
||||
class Arm64BaseBlocks
|
||||
@@ -123,20 +123,18 @@ protected:
|
||||
return (call ? 0x94000000u : 0x14000000u) | (static_cast<u32>(imm26) & 0x03FFFFFFu);
|
||||
}
|
||||
|
||||
// Store only, no cache maintenance. Valid ONLY for a site inside the
|
||||
// block currently being emitted: that buffer has not been executed yet,
|
||||
// and armEndBlock() issues one whole-range flush over it before it can
|
||||
// be. (AetherSX2 does the same — its Link() performs no flush at all,
|
||||
// leaving it to the single range flush in armEndBlock.)
|
||||
// Never use this on code that is already live; use PatchAtomic.
|
||||
// Store only, no cache maintenance. Fine for a site inside the block
|
||||
// currently being emitted (that buffer has not been executed yet, and
|
||||
// armEndBlock() issues one whole-range flush over it), and for the
|
||||
// New() repoint loop, whose callers flush via FlushPatchedSites.
|
||||
// Never use this on code that is already live without a flush; use
|
||||
// PatchAtomic.
|
||||
static void PatchWord(uptr site, u32 instr)
|
||||
{
|
||||
// 4-byte aligned word stores are atomic on AArch64. `site` is the RX
|
||||
// address; under iOS dual-map W^X the store goes through the RW alias
|
||||
// (identity elsewhere). Callers hold an open Begin/EndCodeWrite scope
|
||||
// for the toggle modes; on Darwin the "signal handler" caller is
|
||||
// really the Mach exception-handler thread, so that scope is safe.
|
||||
*reinterpret_cast<volatile u32*>(armGetWritableCodePtr(reinterpret_cast<u8*>(site))) = instr;
|
||||
// armPatchCodeWord owns the W^X handling — sites outside the open
|
||||
// emit window get their own page-granular write scope. Nothing on
|
||||
// the New()/Remove() paths holds one.
|
||||
armPatchCodeWord(reinterpret_cast<u8*>(site), instr);
|
||||
}
|
||||
|
||||
// (HostSys::FlushInstructionCache, not the raw builtin: on Darwin the
|
||||
|
||||
@@ -360,14 +360,13 @@ void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc,
|
||||
pxAssertRel(branch_imm26 >= -0x2000000 && branch_imm26 <= 0x1FFFFFF,
|
||||
"Backpatch thunk too far from faulting instruction for B instruction");
|
||||
|
||||
HostSys::BeginCodeWrite();
|
||||
// iOS dual-map W^X: the faulting instruction lives at an RX address; the
|
||||
// store goes through the RW alias (identity elsewhere). The branch
|
||||
// displacement above was computed against the RX address, which is what
|
||||
// the CPU executes.
|
||||
u32* patch_ptr = reinterpret_cast<u32*>(armGetWritableCodePtr(reinterpret_cast<u8*>(code_address)));
|
||||
*patch_ptr = 0x14000000u | (static_cast<u32>(branch_imm26) & 0x03FFFFFFu);
|
||||
HostSys::EndCodeWrite();
|
||||
// armPatchCodeWord handles W^X (page-granular scope on iOS Legacy mode,
|
||||
// RW alias under dual-mapping) — an arena-wide Begin/EndCodeWrite here
|
||||
// would RX-flip any emit window open on another thread on its way out.
|
||||
// The branch displacement above was computed against the RX address,
|
||||
// which is what the CPU executes.
|
||||
armPatchCodeWord(reinterpret_cast<u8*>(code_address),
|
||||
0x14000000u | (static_cast<u32>(branch_imm26) & 0x03FFFFFFu));
|
||||
|
||||
// Flush icache at the patch point too.
|
||||
HostSys::FlushInstructionCache(reinterpret_cast<void*>(code_address), 4);
|
||||
|
||||
@@ -1247,20 +1247,18 @@ static __fi u32 psxRecClearMem(u32 pc)
|
||||
|
||||
static void recClearIOP(u32 Addr, u32 Size)
|
||||
{
|
||||
// On macOS/Apple Silicon the IOP code cache is MAP_JIT (per-thread W^X).
|
||||
// recClearIOP is IOP SMC detection: every qualifying store in IopMem.cpp (plus
|
||||
// DMA/SIF/BIOS-HLE) funnels here via psxCpu->Clear while the IOP thread is
|
||||
// *executing* recompiled code, i.e. execute-protected with no emit scope open.
|
||||
// psxRecClearMem's recBlocks.Remove() patches compiled block entry points
|
||||
// (Arm64BaseBlocks::Remove -> PatchAtomic) inside that cache, so we must flip
|
||||
// to write mode first or the patch faults (ESR 0x9200004f, byte-write
|
||||
// permission). Refcounted (nests safely), no-op on Linux. Mirrors the EE
|
||||
// recClear fix (b54fbcdad).
|
||||
HostSys::BeginCodeWrite();
|
||||
// IOP SMC detection: every qualifying store funnels here via psxCpu->Clear
|
||||
// (IopMem.cpp, DMA/SIF/BIOS-HLE, and the out-of-line store stubs'
|
||||
// iopStoreClearHit). The only code writes underneath are
|
||||
// Arm64BaseBlocks::Remove()'s entry stubs, and those open their own
|
||||
// per-site W^X scope (armPatchCodeWord) — SetFnptr targets are heap data.
|
||||
// The arena-wide Begin/EndCodeWrite this used to hold was both wasteful
|
||||
// (a whole-arena mprotect pair per covered store in iOS Legacy mode) and
|
||||
// hazardous: Legacy range windows bypass the refcount, so the paired
|
||||
// EndCodeWrite RX-flipped any emit window open on another thread.
|
||||
u32 end = Addr + Size * 4;
|
||||
for (u32 i = Addr; i < end; i += PSXREC_CLEARM(i))
|
||||
;
|
||||
HostSys::EndCodeWrite();
|
||||
}
|
||||
|
||||
// Called from the JIT RAM-store fast-path stubs when the stored-to granule
|
||||
|
||||
@@ -1793,8 +1793,10 @@ static void recPatchIslandB(u8* site, const u8* target)
|
||||
{
|
||||
const intptr_t imm26 = (reinterpret_cast<intptr_t>(target) - reinterpret_cast<intptr_t>(site)) >> 2;
|
||||
pxAssertRel(imm26 >= -(1 << 25) && imm26 < (1 << 25), "Cold-exit island out of B imm26 range");
|
||||
// iOS dual-map W^X: store via the RW alias; displacement/flush stay on RX.
|
||||
*reinterpret_cast<volatile u32*>(armGetWritableCodePtr(site)) = 0x14000000u | (static_cast<u32>(imm26) & 0x03FFFFFFu);
|
||||
// armPatchCodeWord opens its own W^X scope: the islands live in the hot
|
||||
// block, whose window armEndBlock() already closed by the time the cold
|
||||
// session patches them.
|
||||
armPatchCodeWord(site, 0x14000000u | (static_cast<u32>(imm26) & 0x03FFFFFFu));
|
||||
HostSys::FlushInstructionCache(site, 4);
|
||||
}
|
||||
|
||||
@@ -1831,10 +1833,11 @@ static void recEmitColdSideExits()
|
||||
pxAssert(armGetCurrentCodePointer() < SysMemory::GetEERecEnd());
|
||||
s_coldPtr = armEndBlock();
|
||||
|
||||
HostSys::BeginCodeWrite();
|
||||
// Each island patch opens its own write scope (armPatchCodeWord); an
|
||||
// arena-wide Begin/EndCodeWrite here would RX-flip a concurrently open
|
||||
// MTVU emit window in Legacy mode on its way out.
|
||||
for (int k = 0; k < n; k++)
|
||||
recPatchIslandB(s_sideExitIslands[k], coldStart[k]);
|
||||
HostSys::EndCodeWrite();
|
||||
|
||||
g_branch = 1;
|
||||
}
|
||||
@@ -2675,14 +2678,14 @@ static void recClear(u32 addr, u32 size)
|
||||
if (blockidx == -1)
|
||||
return;
|
||||
|
||||
// macOS/Apple Silicon (no-op elsewhere): recClear runs from the EE
|
||||
// page-fault handler (fastmem backpatch + SMC via mmap_ClearCpuBlock) and
|
||||
// from runtime SMC on the executing CPU thread — both enter with the
|
||||
// MAP_JIT code cache in execute-protected (W^X) mode. The SetFnptr writes
|
||||
// and Arm64BaseBlocks::Remove() stub patches below target that region, so
|
||||
// a write faults (SIGBUS) unless we flip the thread to write mode first.
|
||||
// Refcounted, so it nests harmlessly when reached from an open emit scope.
|
||||
HostSys::BeginCodeWrite();
|
||||
// No write scope here: the only code writes below are Remove()'s entry
|
||||
// stubs, and those open their own per-site scope (armPatchCodeWord).
|
||||
// SetFnptr/recLUT targets are ordinary heap data. The arena-wide
|
||||
// Begin/EndCodeWrite this used to hold was worse than useless on iOS
|
||||
// Legacy mode — the range-window emit scope bypasses the refcount, so
|
||||
// when recClear runs mid-compile (the stale-overlap walk in
|
||||
// recRecompile) the paired EndCodeWrite RX-flipped the open emit window
|
||||
// and the next emitted instruction faulted.
|
||||
|
||||
// Track the EE-address span of all blocks we touch so the post-walk
|
||||
// tail can reset interior BLOCKs across the *full* extent of the
|
||||
@@ -2795,8 +2798,6 @@ static void recClear(u32 addr, u32 size)
|
||||
// into the middle of a freshly-recompiled block.
|
||||
if (upperextent > lowerextent)
|
||||
iopClearRecLUT(GETBLOCK(lowerextent), upperextent - lowerextent);
|
||||
|
||||
HostSys::EndCodeWrite();
|
||||
}
|
||||
|
||||
static void iopClearRecLUT(BASEBLOCK* base, int count)
|
||||
|
||||
@@ -457,6 +457,18 @@ void normJumpCompile(mV, microFlagCycles& mFC, bool isEvilJump)
|
||||
// constructed once per mVUreset over the whole post-dispatcher cache range.
|
||||
static thread_local ptrdiff_t s_mVUblockStartOffset = 0;
|
||||
|
||||
// The write scope covers exactly the persistent MA's buffer span
|
||||
// [x86start, physical region end]: every byte a session emits lands in
|
||||
// there. Range form, not the whole-arena BeginCodeWrite: in iOS Legacy
|
||||
// mode the whole-arena EndCodeWrite RX-flips the entire arena on close,
|
||||
// which killed any EE emit window open on the other thread (Legacy range
|
||||
// windows bypass the refcount). Toggle modes fall through to the same
|
||||
// refcounted whole-region toggle as before; dual-mapping is a no-op.
|
||||
static size_t mVUcodeCacheWriteSpan(microVU& mVU)
|
||||
{
|
||||
return static_cast<size_t>(mVU.prog.x86end - mVU.prog.x86start) + (mVUcacheSafeZone * _1mb);
|
||||
}
|
||||
|
||||
static void mVUopenCodeCache(microVU& mVU)
|
||||
{
|
||||
// Nested call (same thread re-enters before its outer close): no-op.
|
||||
@@ -465,7 +477,7 @@ static void mVUopenCodeCache(microVU& mVU)
|
||||
|
||||
pxAssert(mVU.jitAsm); // mVUreset must have built it.
|
||||
|
||||
HostSys::BeginCodeWrite();
|
||||
HostSys::BeginCodeWriteRange(mVU.prog.x86start, mVUcodeCacheWriteSpan(mVU));
|
||||
// armAsmPtr MUST equal the MA's buffer base (= mVU.prog.x86start) for
|
||||
// armGetCurrentCodePointer() to return the real write position:
|
||||
// armGetCurrentCodePointer() = armAsmPtr + armAsm->GetCursorOffset()
|
||||
@@ -519,7 +531,7 @@ static void mVUcloseCodeCache(microVU& mVU)
|
||||
mVUPersist::EndEpisode(mVU, codeStart + codeSize);
|
||||
|
||||
armAsm = nullptr; // unbind; do not delete (persistent)
|
||||
HostSys::EndCodeWrite();
|
||||
HostSys::EndCodeWriteRange(mVU.prog.x86start, mVUcodeCacheWriteSpan(mVU));
|
||||
if (codeSize > 0)
|
||||
{
|
||||
HostSys::FlushInstructionCache(codeStart, codeSize);
|
||||
|
||||
Reference in New Issue
Block a user