ARM64e ABI and Pointer Authentication (PAC)

Pointer Authentication (PAC)

Pointer Authentication is the “big” feature of ARMv8.3-A; it dramatically helped defend against code-reuse and memory corruption attacks. It repurposes the unused high-order bits of 64-bit pointers to store a cryptographic Pointer Authentication Code (PAC), effectively a signature covering the pointer’s value and a context.

pac1.png

Dedicated instructions are used to sign pointers when storing them and to authenticate (verify and strip the PAC) when loading them. If a pointer has been modified in memory (i.e., the PAC no longer matches), the authentication fails and the CPU will invalidate the pointer, causing a fault on use. This provides a probabilistic guarantee that a forged or corrupted pointer won’t be treated as valid, thereby thwarting control-flow hijacking via return addresses, function pointers, vtable pointers, etc.

pac5.png

ARMv8.3-A defines five 128-bit secret keys for PAC:

  • two for instruction (code) pointers,
  • two for data pointers, and
  • one general key.

In hardware, these keys reside in special registers inaccessible to user-mode (EL0) software. Signing a pointer (PAC instruction) uses one of the keys plus a 64-bit “modifier” (context value) to produce the PAC bits, which replace some of the pointer’s upper bits. Authentication (AUT instruction) recomputes and checks the PAC, setting a faulting invalid address if it doesn’t match.

ARMv8.3 introduced variants like: PACIA/PACDA (pointer auth with key A for code or data, using a supplied modifier register), PACIZA (using zero as modifier), and matching AUTIA/AUTDA instructions for verification. Specialized combined forms also exist (added in v8.3 or later): e.g., BLRAA (authenticate pointer and branch), RETAB (authenticate link register and return). These allow minimal performance overhead by merging PAC checks with normal control-flow operations.

Apple’s ARM64e ABI

Apple introduced a new ABI slice called arm64e to utilize Pointer Authentication in software. On arm64e iOS and macOS binaries, the compiler/assembler automatically inserts PAC signing and authentication instructions at appropriate places (function prologues/epilogues, pointer assignments, etc.).

For example, return addresses (link register) are signed on function entry (using PACIASP with the stack pointer as context) and authenticated on return (AUTIASP paired with RET). Indirect function calls are hardened by signing function pointers when stored and using authenticating branch instructions (BLRAA/BLRAB) when calling them.

Many code pointers in structures get signed as well. This is largely transparent to developers; as Apple’s documentation notes, “the addition of pointer authentication is transparent to most apps because the compiler manages the process.” Apps simply build for the arm64e architecture (supported from Xcode 10.1 onward) to opt-in and gain PAC protection. Notably, devices with Apple A12 Bionic or later (including all M-series Macs) support arm64e binaries.

Kernel arm64e compilation

Apple’s XNU kernel is also compiled as arm64e, so the kernel itself benefits from PAC. In kernel (EL1) and hypervisor (EL2) modes (TODO LINK!!), Apple uses an implementation-defined PAC algorithm (a variant of ARM’s QARMA cipher with Apple’s customizations).


QARMA Cipher

QARMA is a lightweight, tweakable block-cipher family designed for efficient hardware implementation of Pointer Authentication. It operates on 64-bit blocks (the size of an ARM pointer) using a small ARX (Add-Rotate-XOR) round function coupled with a “tweak” value that changes every invocation. This tweak is fed with context (e.g., stack pointer or process-specific salt) to bind the signature to its use site. Apple’s PAC variant uses the standard QARMA structure (10-12 rounds of ARX operations per block) but replaces ARM’s reference constants with Apple-chosen round keys and injects additional per-boot and per-domain diversification values. The result is a hardware cipher that signs pointers in just a handful of cycles, producing a 16- to 24-bit PAC embedded in the high bits of each 64-bit pointer, with no way for user code to read or extract the key material.


Apple added hardware diversification: each boot generates a fresh random tweak to the PAC keys (so PACs differ each boot even with same inputs), and a special KERN-Key* diversifier bit is mixed into PAC computations in kernel mode.


* A KERN-Key diversifier is a per-boot, domain-specific tweak value that’s mixed into the QARMA-based PAC key derivation at EL1, ensuring kernel-mode pointer signatures use a distinct key from user-space. Think of the KERN-Key diversifier like a unique, per-boot “ink stamp” added to every kernel pointer signature: just as a tamper-evident wax seal uses a fresh design each time, it ensures you can’t reuse or forge yesterday’s seal today. This is pretty new to me.


In effect, the hardware uses separate effective keys for kernel vs user pointers even if the base key registers are the same, providing isolation between kernel and user space. Similarly, when running virtual machines, Apple Silicon uses a separate diversifier for host vs guest (via a VKEY_EL2 register) to ensure a guest’s pointer signatures cannot be valid in the host context. These measures mitigate “cross-domain” attacks (an attacker in userspace cannot forge a kernel PAC, and a malicious guest VM cannot forge host pointers).

PAC in XNU kernel

Apple integrated PAC into macOS’s XNU kernel for control-flow and data protection. Examples include:

(1) Return address protection

XNU signs any saved LR values (e.g. when an exception traps to EL1, the trapped LR is PAC-signed before storing to the kernel stack). On returning from exceptions or interrupts, the kernel authenticates the saved LR (using RETAB which checks the B-key PAC) to catch any tampering. This defends against return-oriented programming in kernel context.

(2) Function pointer protection

Many internal kernel function pointers or callback vectors are stored as signed pointers. For instance, during early boot XNU takes pointers in its __thread_starts table (the entry points for new threads) and re-bases and signs them with PAC so that they cannot be overwritten to arbitrary values. Any attempt to corrupt such a pointer (e.g. via a memory write vulnerability) will result in a pointer that fails authentication when used, halting execution.

(3) Shared region pointers

macOS uses a shared cache of common frameworks mapped into processes. Embedded pointers in this shared region are signed with a special shared key so that they are valid across multiple processes that share the region. The kernel manages generating a random shared region A-key and uses it to PAC those pointers when populating the shared cache pages. This way, even cross-process shared function pointers are validated.

(4) Data structure pointers

XNU (and associated frameworks like libobjc) use Clang’s __ptrauth annotations to mark sensitive struct fields to be signed. Apple added tests in the kernel to ensure all expected data pointers are indeed signed and validated. Examples likely include Mach ports or task pointers, which, if corrupted, could lead to privilege escalation - PAC prevents such pointers from being exploited unless the attacker can also forge the correct PAC.

To maintain performance, Apple avoids excessive context switching of PAC keys. User-space keys (the A and B keys for each process) are set up per process and usually only changed on context switch, not on every user/kernel transition. Earlier SoCs required flushing or disabling keys on each syscall/trap when switching to a non-PAC process, by toggling bits in SCTLR_EL1 (which make PAC instructions act as NOPs for legacy processes). Newer Apple chips (A13 and later) added hardware controls to retain PAC state across exceptions, so the kernel only needs to (re)configure keys when a thread from a non-arm64e (no PAC) process is scheduled or vice-versa. This significantly reduced the overhead of PAC in the system without sacrificing backward compatibility for older apps.

Further information

  • https://gist.github.com/networkextension/24bac6ffb8cb875e1d8b4f8672cdba3b
  • https://blog.ret2.io/2021/06/16/intro-to-pac-arm64/
  • https://oliviagallucci.com/control-flow-integrity-cfi-user-vs-kernel-land/
  • https://googleprojectzero.blogspot.com/2019/02/examining-pointer-authentication-on.html
  • https://appsecuritymapping.com/wp-content/uploads/2023/02/App_Code-Hardening_v2.pdf

  • 2019 LLVM Developers’ Meeting: A. Bougacha & J. McCall “arm64e: An ABI for Pointer Authentication”