Executable-Only (XO) memory protections

Besides W^X, Apple also explored Executable-Only memory protection: pages that are executable but not readable (even by the owning process). The goal of XO memory is to prevent attackers from easily dumping or disassembling JIT-compiled code to find gadgets or vulnerabilities.

On ARM architectures, it’s possible to have pages that permit instruction fetch but not data reads. In practice, macOS’s JIT implementation doesn’t fully hide JIT pages from the owning process; JIT pages are left readable (R-X) when protected, so the JIT compiler can inspect or patch its code if needed. However, Apple did implement a form of execute-only routine as a mitigation in the past: WebKit’s 2016 “Bulletproof JIT” strategy used a stub function mapped as --x (execute but not readable). That stub knew the secret location of a writable alias of the JIT region and would copy data into it, without disclosing the address to other code. This made it harder for exploits to find and write to JIT memory. Bulletproof JIT was later augmented by hardware features (APRR: Access Protection ReRouting Registers) and pointer authentication, but it illustrates the execute-only concept: even if attackers gained read/write primitives, they couldn’t easily read the JIT copying function or locate the writable mapping.

On Apple silicon Macs, the primary executable-only protection is at the kernel/system level rather than for user JIT code. macOS uses a Page Protection Layer (PPL) in iOS/tvOS that makes pages containing code execute-only to userland (and even to a compromised kernel), ensuring they can’t be modified after signature verification. On macOS, full PPL is not enabled for user space (macOS is designed to allow unsigned code for developer flexibility). Instead, macOS relies on the MAP_JIT entitlement and W^X toggling to control JIT code execution. The result is that JIT pages are not globally execute-only–the process’s own code can read them as data if it needs to–but they are protected from other processes and are never concurrently writable/executable.

It’s a clASsIC trade-off between security and the practicality of JIT debugging and patching. Apple continues to research stronger protections (for example, ARMv8.5’s Branch Target Identification (BTI) and Pointer Authentication also make jumping to injected code or gadgets more difficult).In short, macOS’s JIT implementation stops short of true execute-only memory for JIT pages, but it enforces that any JIT code in execution state is non-writable, and any time it is writable it’s non-executable, significantly constraining the ability to misuse JIT memory.