Security implications and mitigations

The design of JIT memory on macOS is a balancing act between performance and security. The security implications are significant: by restricting JIT memory in these ways, Apple greatly reduces the risk of JIT being an easy path to executing arbitrary code. Some key points and mitigations include:

No implicit RWX

An attacker who gains a write primitive in a process cannot simply write malicious code into a JIT page and execute it. If they try to write into a JIT region while it’s in execute (R-X) mode, they will trigger a memory protection fault. They would first need to somehow disable the write protection on that thread, but calling pthread_jit_write_protect_np(0) is not trivial for an attacker, especially if the app has enabled the allowlist mode (in which case that call is not even permitted except from specific callbacks). This dramatically raises the bar for exploits. The bypass resistance is evident: earlier exploits relied on jumping straight to shellcode; now they must chain multiple logic flaws or info leaks to even get the shellcode in place and authorized to run.

Pointer authentication (PAC)

PAC further ensures that even if an attacker can write and mark code as executable, they might not be able to call it. On Apple CPUs with PAC (arm64e mode), code pointers (like function returns and indirect call targets) carry cryptographic signatures. The OS and compilers use PAC to sign return addresses and certain function pointers with keys that are unique per process. In context of JIT, this means the control-flow from C++ into JIT code and back can be guarded. For example, Safari’s JIT uses a PAC signature when jumping into JIT code. If an attacker tries to redirect execution to a JIT payload via an illegitimate pointer, the PAC check will fail and the app will crash rather than execute untrusted code. PAC isn’t bulletproof (researchers have found clever PAC bypasses using side-channels or by corrupting data before it’s signed), but it’s a strong mitigation against straightforward code pointer manipulation.

Sandboxing and entitlement restrictions

The requirement for a special entitlement (allow-jit) means that most software cannot generate new executable code at runtime. This is a security win: malware can’t simply JIT itself to evade static analysis. Only specifically signed apps (or system processes) that Apple has allowed can use JIT, and those are typically well-audited (browsers, language runtimes, etc.). Even within those, the sandbox policies can limit what a compromised JIT-enabled process can do. For instance, a malicious web page might exploit Safari’s JIT, but it’s still trapped in the browser’s sandbox (no unrestricted file system access, can’t elevate privileges directly, etc.). The design assumes that JIT will be a high-value target and layers Defense-in-Depth accordingly.

PAC and JIT interaction

One challenge for a JIT engine is managing pointers that cross the boundary between JIT code and C++ (host) code. macOS’s pointer authentication enforces that return addresses and function pointers are signed. JIT code that calls back into C/C++ (or vice versa) must use the correct PAC sequence. Apple provides guidelines and even specialized instructions for this. For example, if JIT compiled code wants to call an Objective-C method or a C function pointer, the JIT must be aware of PAC or use calls through trampolines that handle authentication. Apple’s documentation on “protecting code compiled just-in-time” indicates that variables accessed between toggling memory permissions should reside in PAC-protected memory or registers, highlighting that the engine should not leave sensitive pointers in unprotected memory where they could be corrupted during JIT write phases. Moreover, starting with A12, return addresses are PAC-signed (with IB key) by the hardware on function call; JIT code that leverages function calls/returns inherits that protection automatically (the CPU will sign and check the link register on calls/returns). The bottom line: on macOS/ARM64e, JIT doesn’t exist in a vacuum; it works within the pointer-authenticated world, and an internals engineer needs to ensure any handwritten assembly or code trampolines respect the PAC requirements to avoid crashes.

Bypass history

Attackers have still found ways to exploit JIT engines, but each time a new mitigation has made it harder. As noted, WebKit’s earlier “Bulletproof JIT” was bypassed by ROP because it lacked Control-Flow Integrity (CFI). Apple responded with hardware CFI (BTI) and PAC in newer chips. APRR could be bypassed by corrupting the code before it was copied into RX memory; Apple then inlined the copy function and used PAC to protect the intermediate buffer. There’s a cat-and-mouse dynamic: for each bypass technique, mitigations like JIT hardening, CFI, signing, and isolation are deployed to close the gap. The current state (as of macOS 12/13) is that a successful JIT exploit likely requires chaining multiple vulnerabilities (e.g., an info leak, a bug to invoke a JIT copy routine out of sequence, and a sandbox escape), which is a significantly higher hurdle than in the past.

In conclusion, macOS’s JIT compilation support is built on a robust architectural foundation: special memory mappings (MAP_JIT) governed by the kernel code-signing enforcement, with hardware-assisted W^X (per-thread memory permission registers) and increasingly granular controls (like callback allowlists). Apple’s JavaScriptCore in Safari is a canonical example of these mechanisms in action, combining software techniques and hardware features (APRR, PAC, BTI) to make JIT both performant and as secure as possible. A new macOS internals engineer delving into this should focus on the relationships between the user APIs (mmap with MAP_JIT, pthread_jit_write_protect_np, etc.), the kernel enforcement (codesign checks, VM map handling), and the hardware behavior (how the CPU handles execute permissions and pointer authentication). The primary takeaway is that modern macOS JIT is no longer “just allocate RWX and go” – it’s a tightly managed dance between the compiler and the OS to maintain system security while still permitting dynamic code execution.

Futher readings

  • https://blog.svenpeter.dev/posts/m1_sprr_gxf/
  • https://googleprojectzero.blogspot.com/2020/09/jitsploitation-three.html
  • https://github.com/zherczeg/sljit/issues/99
  • https://support.apple.com/guide/security/operating-system-integrity-sec8b776536b/web