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.
This commit is contained in:
AurisDSP 2026-05-09 07:27:01 -04:00 committed by Elad
parent 3c2815e89c
commit 6850e2e5a8
4 changed files with 67 additions and 0 deletions

View File

@ -5289,7 +5289,19 @@ bool ppu_initialize(const ppu_module<lv2_obj>& 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<lv2_obj>& 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;

View File

@ -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);

View File

@ -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)

View File

@ -1835,6 +1835,24 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch,
g_fxo->init<named_thread>("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<ppu_module<lv2_obj>*> mod_list;
if (auto& _main = *ensure(g_fxo->try_get<main_ppu_module<lv2_obj>>()); !_main.path.empty())