GS: write JIT code through RW alias on iOS dual-map

The GS software renderer's ARM64 code generators (DrawScanline and
SetupPrim) handed the raw RX pointer to their vixl MacroAssembler. On
iOS 26+ devices using the LuckTXM JIT mode, executable memory is
dual-mapped: a read-execute alias for the CPU and a separate read-write
alias for the emitter, offset by g_code_rw_offset. Writing to the RX
page faults instantly (KERN_PROTECTION_FAILURE) -- the GS-thread SIGBUS
crash seen on iPhone 16 / iOS 27 beta.

The EE and VU recompilers already handle this via armGetWritableCodePtr
(pcsx2/arm64/AsmHelpers.cpp). Mirror that pattern in the GS path: a
file-local gsGetWritableCodePtr() helper adds the offset on real iOS
devices and is an identity no-op everywhere else (macOS, iOS Simulator,
Android, Legacy-iOS). The original RX pointer is kept as m_code_rx so
GetCode() still returns the executable entry point the rasterizer calls
into -- vixl's GetStartAddress echoes the constructor argument, so
without this override GetCode() would return the non-executable RW
pointer.

Cross-platform safety: the guard (#if __APPLE__ && TARGET_OS_IPHONE &&
!TARGET_OS_SIMULATOR) is byte-identical to the already-shipped EE/VU
helper. On every non-iOS path the helper returns the pointer unchanged,
and g_code_rw_offset is 0, so behaviour is identical to before. The x86
GS path (.all.cpp, selected by CMakeLists.txt:480-490 under ARCH_X86)
shares no code with these arm64 files and is not compiled on any arm64
target.
This commit is contained in:
Jeen
2026-07-13 17:19:50 +02:00
parent 917bc67865
commit 4bf3be1f69
4 changed files with 38 additions and 4 deletions
@@ -11,6 +11,18 @@
#include <cstdint>
// On iOS dual-map JIT, write through the RW alias (rx + g_code_rw_offset).
// Identity no-op elsewhere. Mirrors armGetWritableCodePtr in pcsx2/arm64/AsmHelpers.cpp.
#if defined(__APPLE__) && TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
#include "common/Darwin/DarwinMisc.h"
static void* gsGetWritableCodePtr(void* rx_ptr)
{
return static_cast<u8*>(rx_ptr) + DarwinMisc::g_code_rw_offset;
}
#else
static void* gsGetWritableCodePtr(void* rx_ptr) { return rx_ptr; }
#endif
// warning : offset of on non-standard-layout type 'GSScanlineGlobalData' [-Winvalid-offsetof]
#ifdef __clang__
#pragma clang diagnostic push
@@ -84,8 +96,9 @@ static const auto& _fd = v2;
#define armAsm (&m_emitter)
GSDrawScanlineCodeGenerator::GSDrawScanlineCodeGenerator(u64 key, void* code, size_t maxsize)
: m_emitter(static_cast<vixl::byte*>(code), maxsize, vixl::aarch64::PositionDependentCode)
: m_emitter(static_cast<vixl::byte*>(gsGetWritableCodePtr(code)), maxsize, vixl::aarch64::PositionDependentCode)
, m_sel(key)
, m_code_rx(static_cast<const u8*>(code))
{
// hopefully no constants which need to be moved to register first..
m_emitter.GetScratchRegisterList()->Remove(_xscratch.GetCode());
@@ -15,7 +15,8 @@ public:
void Generate();
size_t GetSize() const { return m_emitter.GetSizeOfCodeGenerated(); }
const u8* GetCode() const { return m_emitter.GetBuffer().GetStartAddress<const u8*>(); }
// Return RX entry pointer, not the RW write pointer.
const u8* GetCode() const { return m_code_rx; }
private:
void Init();
@@ -79,4 +80,7 @@ private:
GSScanlineSelector m_sel;
vixl::aarch64::Label m_step_label;
// RX entry pointer; GetCode() must return RX even when the emitter writes through the RW alias.
const u8* m_code_rx;
};
@@ -9,6 +9,18 @@
#include <cstdint>
// On iOS dual-map JIT, write through the RW alias (rx + g_code_rw_offset).
// Identity no-op elsewhere. Mirrors armGetWritableCodePtr in pcsx2/arm64/AsmHelpers.cpp.
#if defined(__APPLE__) && TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
#include "common/Darwin/DarwinMisc.h"
static void* gsGetWritableCodePtr(void* rx_ptr)
{
return static_cast<u8*>(rx_ptr) + DarwinMisc::g_code_rw_offset;
}
#else
static void* gsGetWritableCodePtr(void* rx_ptr) { return rx_ptr; }
#endif
MULTI_ISA_UNSHARED_IMPL;
using namespace vixl::aarch64;
@@ -28,8 +40,9 @@ static constexpr const GSScanlineConstantData128B& g_const = g_const_128b;
#define armAsm (&m_emitter)
GSSetupPrimCodeGenerator::GSSetupPrimCodeGenerator(u64 key, void* code, size_t maxsize)
: m_emitter(static_cast<vixl::byte*>(code), maxsize, vixl::aarch64::PositionDependentCode)
: m_emitter(static_cast<vixl::byte*>(gsGetWritableCodePtr(code)), maxsize, vixl::aarch64::PositionDependentCode)
, m_sel(key)
, m_code_rx(static_cast<const u8*>(code))
{
m_en.z = m_sel.zb ? 1 : 0;
m_en.f = m_sel.fb && m_sel.fge ? 1 : 0;
@@ -15,7 +15,8 @@ public:
void Generate();
size_t GetSize() const { return m_emitter.GetSizeOfCodeGenerated(); }
const u8* GetCode() const { return m_emitter.GetBuffer().GetStartAddress<const u8*>(); }
// Return RX entry pointer, not the RW write pointer.
const u8* GetCode() const { return m_code_rx; }
private:
void Depth();
@@ -30,4 +31,7 @@ private:
{
u32 z : 1, f : 1, t : 1, c : 1;
} m_en;
// RX entry pointer; GetCode() must return RX even when the emitter writes through the RW alias.
const u8* m_code_rx;
};