ARM Pointer Authentication (PAC): Signing Pointers with a Cryptographic Hash

2026-06-06

On ARMv8.3-A and later (every Apple Silicon chip, every recent Android phone), the top bits of a 64-bit virtual address aren't address bits — they're a Pointer Authentication Code, a small cryptographic signature embedded in the pointer itself. This is ARM's answer to ROP/JOP attacks, and it works fundamentally differently than Intel's CET shadow stack.

The mechanism: virtual addresses on AArch64 are effectively 48 or 52 bits. The unused upper bits — typically bits 55:48 (or wider with TBI off) — get filled with a truncated QARMA block cipher output computed from the pointer value, a 128-bit key in a system register, and a 64-bit "context" modifier (often the stack pointer). New instructions do the work:

Five keys exist: IA, IB (instruction pointers A/B), DA, DB (data pointers), and GA (generic, for arbitrary 64-bit values). The kernel loads per-process keys on context switch, so a PAC signed in process X is meaningless in process Y.

Real-world example: In macOS arm64e, a function prologue starts with pacibsp (sign LR using key B + SP) and ends with retab (authenticate-and-return using key B). If an attacker overflows a buffer and overwrites the saved LR on the stack with a gadget address, the PAC bits don't match. The retab corrupts the address, the CPU jumps to a non-canonical address, and the process dies with SIGSEGV instead of executing the gadget. The attacker would need to forge a valid signature — which requires knowing the per-process key.

Rule of thumb for PAC strength: with N PAC bits, a blind forgery attempt succeeds with probability 1/2^N. On a typical 48-bit-VA system with TBI off, you get ~16 PAC bits — meaning ~65,000 attempts on average to forge. That's why PAC is paired with fault-on-fail: the process dies on the first wrong guess, so attackers don't get 65,000 tries.

The catch: PAC is statistical, not absolute. A signing oracle (any code path that returns a signed pointer the attacker can read) breaks it instantly. And PAC doesn't protect against an attacker who replays a valid signed pointer at a different call site — which is why the SP context modifier matters.

See it in action: Check out 2019 LLVM Developers’ Meeting: A. Bougacha
Key Takeaway: ARM PAC embeds a per-process keyed MAC of each pointer in its unused upper bits, turning return-address overwrites and vtable hijacking into a forgery problem that fails closed on the first guess.