JavaScriptCore JIT in Safari WebKit

JavaScriptCore (JSC), the engine in Safari/WebKit, is a prime consumer of JIT on macOS. Over the years, it has become a JIT that closely tracks Apple’s OS capabilities. JSC employs multi-tier JIT compilers:

all of which emit native code for JavaScript. On macOS (and iOS), JSC’s JIT works within the single MAP_JIT region allowed for the process; essentially a large executable heap. The system architecture described above was largely driven by the needs of WebKit: Apple’s desire to secure Safari’s JIT influenced the design of MAP_JIT and APRR.

Historically, JSC implemented the Bulletproof JIT around iOS 10, which used twin mappings for code: one RX and one hidden RW, with an XOR “constant blinding” technique to hide the address. With the advent of APRR (Armv8.3) on the A12 chip and later, JSC transitioned to using the per-thread permission model. In WebKit’s source, the JIT write toggle is invoked via functions like restrictJITMemoryToRX() and restrictJITMemoryToRW(), which correspond to calling the thread-specific APRR controls. WebKit inlines the memcpy into JIT pages to avoid having a standalone function that could be repurposed by attackers.

Apple also integrated Pointer Authentication into JSC. For example, pointers to JIT compiled functions are signed with PAC (using a special key) before being used. The PAC is verified by hardware on jump. According to one security presentation, WebKit signs JIT function pointers with the B-key (PACIB). This means even if an attacker manages to write malicious code in the JIT region, they cannot simply set a function pointer to that address and call it; the pointer authentication will fail unless they also somehow forge the correct signature (which is cryptographically non-trivial without the key). Additionally, JSC uses PAC to protect data structures that could corrupt JIT code generation. For instance, on iOS JSC had a mechanism called PAC-ized cage for the JavaScript heap pointers to mitigate fake object attacks.

JSC is also subject to the sandbox rules. Safari’s renderer (WebContent) processes are sandboxed such that even if an attacker breaks out of JavaScript via a JIT exploit, they have limited abilities. The JIT region’s address is randomized (ASLR) and not directly exposed, making it harder to locate. Moreover, as of macOS 11+, JSC’s JIT region on Apple Silicon is always r-x for other threads, so an exploit can’t race the JIT compiler thread to inject code — it would have to hijack the JIT thread itself or trick it into copying malicious code. The security impact is that exploits have become much more complex. Google Project Zero noted that pre-2018, a single bug giving read/write in WebKit could directly write shellcode into JIT memory and execute it. Now, multiple mitigations (W^X, APRR, PAC, ASLR, CFI/BTI, etc.) need bypassing. In 2020, a Project Zero exploit chain (targeting iOS 13) used a series of techniques to bypass JSC’s JIT hardening, including corrupting the temporary buffer that JIT code was assembled in before it was copied into RX memory. Apple responded by adding integrity checks and using PAC to guard those buffers too.

In summary, JavaScriptCore’s implementation exemplifies the state of JIT on macOS: it uses MAP_JIT for its code allocation, it leverages the pthread_jit_write_protect_np API to toggle writability, and it layers on pointer authentication and CFI for jump/call protection. All this happens behind the scenes, so from an internals engineer’s perspective, the key is to follow the proper APIs (use mmap(MAP_JIT), use the thread protection toggles, sign pointers if needed on arm64e) and let the OS/hardware enforce the rules.