From 6850e2e5a8524180fb7cb014ff3703ed2bb7b689 Mon Sep 17 00:00:00 2001 From: AurisDSP <235008503+AurisDSP@users.noreply.github.com> Date: Sat, 9 May 2026 07:27:01 -0400 Subject: [PATCH] Apple Silicon: consolidate JIT W^X handling with RAII guards Address review feedback on #18701 (cc @elad335): combine the SPU worker fix from #18701, the SPRX Loader fix from #18703, and three additional similar W^X leaks discovered while auditing the codebase for the same pattern. Use Allman-style braces to match RPCS3 coding style. Background: On AArch64 Apple Silicon, MAP_JIT pages enforce W^X per-thread. pthread_jit_write_protect_np(false) enables write mode and pthread_jit_write_protect_np(true) restores execute mode. When code takes an early return or throws between these calls, the thread is left in write mode, which can cause segfaults on subsequent code fetches or inconsistent state at thread teardown. Fixes applied (all gated on __APPLE__): 1. Emu/Cell/SPUCommonRecompiler.cpp - SPU cache worker thread Add RAII guard so execute mode is restored on worker exit. 2. Emu/System.cpp - SPRX Loader thread Enter write mode (was missing entirely) so ppu_initialize() and ppu_precompile() can write to MAP_JIT pages, and pair with an RAII guard. Reproducer: Red Dead Redemption (BLUS30418) crashes ~12s into boot at 0x300010000 without this fix. 3. Emu/Cell/SPULLVMRecompiler.cpp - SPU LLVM compile path The compile function enters write mode, then has an early "return nullptr" path on rebuild_ubertrampoline failure that skipped the explicit restore. Add RAII guard so execute mode is restored on every exit path. The existing explicit restore before the cache-flush asm directives is preserved. 4. Emu/Cell/PPUThread.cpp - PPU LLVM worker thread (operator()) Worker entered write mode but never restored it on operator() return. Add RAII guard. 5. Emu/Cell/PPUThread.cpp - ppu_initialize() main path This scope alternates write/execute mode and contains an early "return compiled_new" at the empty-jits check plus a final return that both leak write mode. Add RAII guard so execute mode is always restored on exit. Intermediate explicit transitions for the symbol-resolver invocation are preserved. No behavioral change on x86_64 or non-Apple ARM64 (all changes are inside #ifdef __APPLE__ / #if defined(__APPLE__)). Supersedes #18703. --- rpcs3/Emu/Cell/PPUThread.cpp | 24 ++++++++++++++++++++++++ rpcs3/Emu/Cell/SPUCommonRecompiler.cpp | 13 +++++++++++++ rpcs3/Emu/Cell/SPULLVMRecompiler.cpp | 12 ++++++++++++ rpcs3/Emu/System.cpp | 18 ++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/rpcs3/Emu/Cell/PPUThread.cpp b/rpcs3/Emu/Cell/PPUThread.cpp index f5d91cc519..c43ac5d73c 100644 --- a/rpcs3/Emu/Cell/PPUThread.cpp +++ b/rpcs3/Emu/Cell/PPUThread.cpp @@ -5289,7 +5289,19 @@ bool ppu_initialize(const ppu_module& info, bool check_only, u64 file_s thread_ctrl::scoped_priority low_prio(-1); #ifdef __APPLE__ + // Apple Silicon W^X: PPU LLVM worker enables write mode for + // JIT memory. Pair it with an RAII guard so execute mode + // is restored on every exit path (return, exception, etc.) + // to keep per-thread state consistent at teardown. pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; #endif for (u32 i = work_cv++; i < workload.size(); i = work_cv++, g_progr_pdone++) { @@ -5421,7 +5433,19 @@ bool ppu_initialize(const ppu_module& info, bool check_only, u64 file_s // Jit can be null if the loop doesn't ever enter. #ifdef __APPLE__ + // Apple Silicon W^X: this scope toggles write/execute mode multiple + // times below. Use an RAII guard so execute mode is always restored + // on every exit path, including the early "return compiled_new" at + // the empty-jits check and the normal return at function end. pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; #endif // Try to patch all single and unregistered BLRs with the same function (TODO: Maybe generalize it into PIC code detection and patching) ppu_intrp_func_t BLR_func = nullptr; diff --git a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp index 8b4bd15e26..dffca21cae 100644 --- a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp @@ -857,7 +857,20 @@ void spu_cache::initialize(bool build_existing_cache) named_thread_group workers("SPU Worker ", worker_count, [&]() -> uint { #ifdef __APPLE__ + // Apple Silicon W^X: enable JIT write mode for this worker and + // pair it with an RAII guard so execute mode is restored on + // every exit path (return, exception, etc.). Leaving a worker + // in write mode at teardown can leave per-thread state + // inconsistent on AArch64. pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; #endif // Set low priority thread_ctrl::scoped_priority low_prio(-1); diff --git a/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp b/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp index 5b63ca80cc..927d7ac187 100644 --- a/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp @@ -3463,7 +3463,19 @@ public: } #if defined(__APPLE__) + // Apple Silicon W^X: enter write mode for JIT memory and pair + // it with an RAII guard so execute mode is restored on every + // exit path (the early "return nullptr" below would otherwise + // leave the thread in write mode permanently). pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; #endif if (g_cfg.core.spu_debug) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 199aad38ee..29016a408a 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1835,6 +1835,24 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, g_fxo->init("SPRX Loader"sv, [this, dir_queue, is_fast = m_precompilation_option.is_fast]() mutable { +#ifdef __APPLE__ + // Apple Silicon W^X: this thread invokes ppu_initialize() + // and ppu_precompile(), which write into MAP_JIT pages. + // Without enabling write mode here, these writes segfault + // before the game can boot (reproducible: RDR BLUS30418 + // crashes ~12s into boot at 0x300010000). Pair the enable + // with an RAII guard so execute mode is restored on every + // exit path (return, exception, etc.). + pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; +#endif std::vector*> mod_list; if (auto& _main = *ensure(g_fxo->try_get>()); !_main.path.empty())