How ARM64 Pointer Authentication Bypasses Actually Work
Analyze how ARM64 Pointer Authentication bypasses work, detailing gadget signing oracles, PACMAN speculative timing leaks, and context swaps.

Introduction
For decades, memory safety vulnerabilities like stack buffer overflows, use-after-free conditions, and heap metadata corruptions have served as the foundation for arbitrary code execution. As operating systems adopted Non-Executable stacks ($W\oplus X$), fine-grained Address Space Layout Randomization (ASLR), and software-enforced Control Flow Integrity (CFI), exploit development pivoted to Return-Oriented Programming (ROP) and Jump-Oriented Programming (JOP). In response, ARM introduced Pointer Authentication (PAuth, or PAC) in the ARMv8.3-A architecture to cryptographically enforce pointer integrity directly inside CPU execution pipelines.
Despite these cryptographic guarantees, security researchers have demonstrated that ARM64 Pointer Authentication bypasses remain viable attack paths in real-world systems. Rather than attacking the underlying primitives through mathematical cryptanalysis, modern exploits exploit implementation oversights, microarchitectural side channels, and architectural design constraints.
Understanding how these bypasses function requires dissecting the mechanics of pointer tagging, tweakable block ciphers, speculative execution windows, and kernel-space credential structures.
64-Bit Pointer Format under ARMv8.3-A (48-bit Virtual Addressing):
63 55 54 48 47 0
+------------------+----------+-----------------------------------+
| PAC Tag Field | Context/ | Usable Virtual Address |
| (Authentication | Reserved | (Payload Pointer) |
| Code) | Bits | |
+------------------+----------+-----------------------------------+
|<--- 9 to 16 bits ---------->|<------------ 48 bits ------------>|
Cryptographic Foundations: QARMA, Keys, and Virtual Address Tags
Modern 64-bit microprocessors do not utilize the full 64-bit virtual address space. In typical Linux and Darwin configurations, virtual address translations require either 48 bits (4-level paging) or 52 bits (5-level paging with Large Virtual Addressing, or LVA). Consequently, bits 63:48 or 63:52 in a 64-bit register remain unused canonical extensions.
Pointer Authentication repurposes these unused upper address bits to store a Pointer Authentication Code (PAC). The CPU generates this tag via a hardware tweakable block cipher, historically designated as QARMA-64, or simplified variants such as QARMA3 or specialized hardware implementations of PRINCE/AES-based functions.
The cipher takes three distinct inputs:
- Plaintext ($P$): The raw 64-bit canonical pointer value.
- Modifier ($M$): A 64-bit tweak representing program execution context, such as the current Stack Pointer (
SP), a structure offset, or a type-specific hash. - Key ($K$): A 128-bit internal register key inaccessible to user-space (EL0) execution.
The architecture provisions five dedicated 128-bit keys managed at EL1 (kernel) or EL2 (hypervisor):
APIAKey/APIBKey: Instruction keys used to authenticate return addresses and function pointers.APDAKey/APDBKey: Data keys used to authenticate data references.APGAKey: Generic key used to compute Message Authentication Codes across arbitrary register pairs via thepacgainstruction.
+---------------------------------------------+
| Plaintext Pointer (Bits 47:0 Canonical) |---+
+---------------------------------------------+ |
v
+---------------------------------------------+ +-------------------+
| Modifier / Context (64-bit SP / Struct ID) |-->| QARMA-64 Cipher |
+---------------------------------------------+ | (Internal Core) |
+-------------------+
+---------------------------------------------+ | |
| Secret Key (128-bit: APIAKey, APDAKey, etc.)|---+ v
+---------------------------------------------+ [ Truncation Engine ]
|
v
PAC Tag (16-bit)
|
+------------------------------+
v
+----------------------------+--------------------------------+
| PAC Tag (Bits 63:48) | Plaintext Address (Bits 47:0) |
+----------------------------+--------------------------------+
Signed 64-Bit Pointer Register
Under instruction execution, signing and verifying follow strict mechanical pipelines:
// Function Prologue: Sign LR (X30) using SP as context with APIAKey
paciasp // Equivalent to: pacia x30, sp
// ... Function Body ...
// Function Epilogue: Authenticate LR (X30) using SP as context
autiasp // Verifies tag; corrupts pointer if mismatch
retaa // Authenticates and returns atomically (ARMv8.6+)
When an aut* instruction processes a signed pointer, the CPU recalculates the expected PAC using the current modifier and internal key. If the computed code matches the stored tag, the instruction strips the PAC field, restoring a clean canonical virtual address.
Prior to ARMv8.6-A, if the authentication failed, the instruction did not generate an immediate CPU trap. Instead, it injected an error bit pattern into the upper address bits (typically bit 55 or bit 61). Subsequent dereferences of this corrupted address triggered a synchronous Translation Fault (Data Abort or Instruction Abort) at memory load time, crashing the process.
Anatomy of ARM64 Pointer Authentication Bypasses
Architectural security boundaries rely fundamentally on the assumption that an attacker cannot forge a valid PAC without direct access to the 128-bit key stored inside CPU control registers. However, real-world implementations encounter systemic limitations:
+-------------------------------------------------------------------------+
| Classification of PAC Attack Vectors |
+------------------------------------+------------------------------------+
| Software-Level Bypass Primitives | Microarchitectural Side Channels |
+------------------------------------+------------------------------------+
| 1. Tag Truncation Brute-Force | 1. PACMAN Speculative Oracles |
| - Fork-server key reuse | - Translation-abort suppression |
| - Fixed-entropy exhaustion | - L1D cache line allocation |
| 2. Signing Oracle Gadgets | 2. Exception Suppression Windows |
| - Unintended pacia dispatch | - Out-of-order execution loops |
| - In-register context forging | - Meltdown-style transient read |
| 3. Context / Modifier Confusion | 3. Fault-Injection / Glitching |
| - NULL modifier substitution | - Voltage transient transitions |
| - Struct field type swap | - Clock jitter timing bypass |
+------------------------------------+------------------------------------+
1. Entropy Deficits and Tag Truncation
Because the PAC tag must fit inside unused virtual address bits, its cryptographic strength is bound to memory architecture sizing: $$\text{PAC Bits} = 64 - \text{TBI Bits} - \text{Virtual Address Bits}$$ In systems with 48-bit virtual addresses without Top Byte Ignore (TBI), the tag occupies 16 bits ($2^{16} = 65,536$ combinations). When 52-bit LVA is enabled alongside tagged addressing, the PAC tag size shrinks to 7 to 11 bits ($2^7 = 128$ combinations).
In long-running daemon servers (such as Apache, Nginx, or multi-threaded microservices) where worker child processes are generated via fork(), the address space and key registers (APIAKey_EL1) are duplicated across processes. If an attacker possesses a memory corruption vulnerability that overwrites a return address, they can brute-force the 16-bit PAC tag:
- An incorrect guess crashes only the child process worker with a SIGSEGV.
- The parent master process restarts the worker without rotating the process keys.
- An attacker discovers the valid cryptographic signature for a specific pointer within an average of $2^{15} = 32,768$ attempts, entirely bypassing PAC protection.
2. Modifier and Context Confusion
Pointer Authentication binds a pointer to an intended operational state via the 64-bit modifier. If compilers select static, weak, or zero-valued modifiers, the cryptographic binding degrades.
If an application signs a function pointer with modifier zero (XZR):
// Function Pointer Signing with NULL Context
pacia x1, xzr
That signed pointer is valid anywhere in the application where a function pointer is checked against context 0. If two distinct object classes share the same vtable signature, an attacker can swap the signed target pointer with another signed pointer belonging to an entirely different subsystem without triggering an authentication failure.
Similar challenges appear across specialized runtime systems. While modern low-latency architectures—such as High-Throughput Deterministic Actor Runtimes: Cache-Conscious Architectures, Lock-Free Rings, and Kernel-Bypass I/O—optimize memory layout for hardware pipelines, systems engineers must ensure that pointer tagging modifiers uniquely incorporate execution domain identity to prevent cross-domain pointer substitution.
Signing Oracles and Context Confusion Exploits
A signing oracle is a sequence of native instructions reachable during software execution that signs an attacker-controlled register using a legitimate system key. These oracles turn memory corruption vulnerabilities into arbitrary signing machines without brute-forcing the cipher.
Consider a vulnerable C++ dispatch handler compiled for ARM64:
struct ActionHandler {
void (*execute)(const char *context_data);
unsigned long context_id;
};
void register_custom_action(struct ActionHandler *handler, void (*fn)(const char *)) {
// Programmer creates dynamic signing primitive
handler->execute = (void (*)(const char *))__builtin_ptrauth_sign_unauthenticated(
(void *)fn,
ptrauth_key_asia,
handler->context_id
);
}
The disassembled ARM64 sequence translates to:
register_custom_action:
// X0 = struct ActionHandler *handler
// X1 = void (*fn)(const char *)
ldr x2, [x0, #8] // Load handler->context_id into X2
mov x16, x1 // Move target address to scratchpad X16
mov x17, x2 // Move modifier context to X17
pacia x16, x17 // Sign X16 using APIAKey and X17 modifier
str x16, [x0] // Store signed pointer into handler->execute
ret
If an attacker controls the memory pointed to by X0 via an unrelated heap buffer overflow, they configure:
handler->context_id([X0, #8]) to match their target exploitation context (e.g., the address of the stack pointerSPof a vulnerable thread).fn(X1) to reference a target shellcode payload or a system function such asexecve.
When register_custom_action executes, the kernel or application infrastructure signs the attacker's arbitrary address with the valid host core key. The attacker reads back the newly signed pointer and deploys it to overwrite an authentic execution frame.
Memory Layout Under Heap Primitive:
+-------------------------------------------------------+
X0 --> | Offset +0x00: Target Pointer (e.g., system/execve) |
+-------------------------------------------------------+
| Offset +0x08: Target Modifier Context (e.g., Thread SP)|
+-------------------------------------------------------+
|
v
Execution of Signing Gadget (pacia x16, x17):
Key: APIAKey (In-Silicon) + Plaintext: system() + Modifier: Thread_SP
|
v
Result: Cryptographically Valid Signed Function Pointer
Similar boundaries must be protected when isolating hardware components. When orchestrating Architecting Zero-Trust Hardware Attestation and Cryptographic Isolation for Confidential Accelerators, trusting hardware-level cryptographic operations requires validating that the execution context cannot be hijacked via signing oracles embedded in surrounding device drivers.
PACMAN: Speculative Execution and Cache Timing Attacks
The most sophisticated method for bypassing Pointer Authentication without generating crashes or relying on software oracles is PACMAN (disclosed by MIT CSAIL researchers). PACMAN couples pointer authentication mechanics with speculative execution side channels, transforming microarchitectural cache state into a PAC verification oracle.
Under standard architectural conditions, testing a guessed PAC tag directly would cause an authentication failure: the aut* instruction would corrupt the pointer, and the subsequent load or jump would crash the process with an unrecoverable SIGSEGV or SIGBUS.
PACMAN circumvents this architectural termination by embedding the authentication attempt inside a speculative execution path that is squashed before retirement.
Speculative PACMAN Data Path
[ Branch Predictor (Trained Taken) ]
|
v
+-----------------------------------------------+
| B.EQ speculative_execution_path |
+-----------------------------------------------+
| (Speculative Window Open)
v
+-----------------------------------------------+
| autia x16, x17 (Authenticate Guessed PAC) |
+-----------------------------------------------+
|
+---------------+---------------+
| |
(If PAC Tag is Valid) (If PAC Tag is Invalid)
| |
v v
[ Pointer remains uncorrupted ] [ Error bits injected into X16 ]
| |
v v
+-------------------------------+ +-------------------------------+
| ldr x0, [x16] | | ldr x0, [x16] |
| (Accesses user memory space) | | (Translates to Faulting Addr) |
+-------------------------------+ +-------------------------------+
| |
v v
+-------------------------------+ [ MMU Suppresses Load Window ]
| L1D Cache Line Pulled | |
+-------------------------------+ |
| |
+---------------+---------------+
|
v
[ Branch Evaluates Mispredicted: Architectural Rollback ]
|
v
[ Probe L1D via Timing: Valid Tag Identified Without Crash ]
The PACMAN Execution Chain
The attack structure relies on a conditional construct containing a PAC verification instruction coupled to a dependent speculative load:
// Speculative Gadget Setup
// X16 contains the guessed pointer: [ Guessed PAC (16 bits) | Canonical Target Address ]
// X17 contains the Modifier Context (SP / Type Hash)
// X18 contains the Base Address for a cache monitoring array
pacman_gadget:
// Train Branch Predictor to expect branch taken
cmp x20, #0
b.eq transient_execution_window
ret
transient_execution_window:
// 1. Authenticate guessed pointer speculatively
autia x16, x17
// 2. Dereference the authenticated pointer
ldr x19, [x16]
// 3. Dependent access mapping into cache monitoring buffer
and x19, x19, #0x3F // Normalize offset to cache line
lsl x19, x19, #6 // Multiply by 64 (Cache Line Size)
ldr x21, [x18, x19] // Load line into L1 Data Cache
ret
The Verification Loop
- Flush Stage: The attacker flushes the monitoring buffer mapped at
X18from the CPU cache hierarchy using theDC CIVAC(Data Cache Clean and Invalidate by Virtual Address) instruction. - Mistraining Stage: The branch predictor is trained by repeatedly feeding
X20 = 0, causing the CPU's branch processing unit to assume the conditional branchb.eqwill be taken. - Speculative Trigger: The attacker provides a guess for the 16-bit PAC tag within
X16and triggers the mispredicted execution branch. - Microarchitectural Branch Behavior:
- Case A (Incorrect PAC Guess): The
autiainstruction recognizes the cryptographic mismatch and corrupts the high-order bits ofX16. When the subsequent instructionldr x19, [x16]attempts to read memory, the Virtual Memory System flags a translation error. The core suppresses the speculative load; the dependent memory load at[X18, X19]never executes. - Case B (Correct PAC Guess): The
autiaverification succeeds. The pointer inX16remains unaltered. The instructionldr x19, [x16]loads memory successfully from the canonical target. The dependent load at[X18, X19]pulls that cache line into the core's L1 Data Cache.
- Case A (Incorrect PAC Guess): The
- Squash and Rollback: The branch execution unit verifies that the architectural condition failed. The CPU discards the speculative modifications, squashing instruction retirement. No architectural data abort or
SIGSEGVis dispatched. - Probe Stage: The attacker measures memory read latency across the monitoring buffer using the high-resolution virtual counter timer register (
CNTVCT_EL0):uint64_t measure_access(void *addr) { uint64_t start, end; asm volatile( "isb\n" "mrs %0, cntvct_el0\n" "isb\n" "ldr xzr, [%2]\n" "isb\n" "mrs %1, cntvct_el0\n" "isb\n" : "=&r"(start), "=&r"(end) : "r"(addr) ); return end - start; }
If access latency drops below a predefined cache-hit threshold (e.g., $<30\text{ ns}$ versus $>180\text{ ns}$ for main DRAM retrieval), the attacker knows the speculative load succeeded. The guessed 16-bit PAC is verified without crashing the target process.
Defensive Hardening: FPAC, Key Separation, and Epilogue Integrity
To eliminate PACMAN side channels and signing oracles, ARM and modern compiler toolchains developed hardware revisions and architectural isolation techniques.
Timeline of Pointer Authentication Enhancements
ARMv8.3-A (Base Implementation)
- Deferred fault handling (address corruption)
- Vulnerable to speculative execution access oracles (PACMAN)
- Split Authentication & Jump Instructions (autiasp -> ret)
|
v
ARMv8.6-A / Enhanced PAC (FPAC)
- Synchronous architectural trap generated directly on aut* mismatch
- Eliminates transient execution windows past failed authentication
- Introduction of atomic branch-authentication: retaa, retab
|
v
Compiler Hardening (LLVM / GCC Modern Directives)
- Backward-edge CFI Integration (PAuth-ABI)
- Fine-grained context diversification (Type Hash + Stack Pointer)
- Elimination of zero-modifier (XZR) instruction emissions
1. Faulting Pointer Authentication (FPAC)
Beginning with ARMv8.6-A, ARM introduced FPAC (Faulting PAC). Under an FPAC-compliant execution core, if an aut* instruction encounters an invalid PAC tag, the hardware immediately raises a synchronous architectural exception (Instruction Abort or Data Abort) directly at the authentication instruction, rather than corrupting upper address bits and deferring the fault to dereference time:
Legacy ARMv8.3-A Behavior:
[ autia x16, x17 ] -> Mismatch -> Upper Bits Corrupted -> Window Open -> [ Dereference Fault ]
Modern ARMv8.6-A (FPAC) Behavior:
[ autia x16, x17 ] -> Mismatch -> Immediate Synchronous Architectural Trap (Execution Halts)
By halting execution at the authentication point, FPAC closes the speculative execution window: dependent loads cannot proceed down the pipeline, neutralizing PACMAN-style side-channel leaks.
2. Atomic Verification and Control Flow Transfer
Early compiler implementations emitted decoupled instruction sequences where authentication occurred several instructions before the actual return or branch:
// Vulnerable Epilogue Sequence
autiasp // Authenticate X30 (LR)
nop // Exploitation window: LR exists unprotected in register
ret // Branch to LR
An attacker equipped with an asynchronous thread race, memory corruption primitive, or physical fault injection (voltage drop) could alter register X30 between the autiasp check and the ret jump.
Modern toolchains universally replace this sequence with atomic instructions:
retaa: AuthenticateX30againstSPusingAPIAKeyand return atomically.retab: AuthenticateX30againstSPusingAPIBKeyand return atomically.braa / brab: Authenticate target register against specified context and branch atomically.
No pipeline window exists where an unauthenticated pointer rests inside a general-purpose register.
Understanding hardware-enforced guarantees is equally critical in hardware-isolated environments. When contrasting control-flow mechanisms with the attestation layers discussed in Hardware-Enforced Confidential Computing: Deep Microarchitectural Attestation, Memory Encryption Engines, and Enclave Security Pipelines, software systems rely on these instruction-level atomic invariants to prevent cross-enclave execution hijacking.
3. Compiler-Level Context Diversification
Modern revisions of Clang (-mbranch-protection=pac-ret+b-key+pc) and GCC implement context diversification to eliminate context confusion attacks. Rather than relying solely on SP or XZR, compilers generate unique 64-bit modifiers by hashing the caller site's address (PC) with an internal symbol identifier:
$$\text{Modifier} = \text{Stack Pointer} \oplus \text{Hash}(\text{Type Signature}, \text{Callsite Address})$$
// Diversified Signing Sequence in Modern Clang
movk x17, #0x4a21, lsl #48 // Load static type signature hash into upper bits
movk x17, #0xb83f, lsl #32 // Load callsite diversification identifier
eor x17, x17, sp // XOR with current Stack Pointer
pacia x30, x17 // Sign using globally unique runtime context
Because every function return frame and indirect callsite generates a unique runtime modifier, signed pointers cannot be substituted across functions, neutralizing context substitution vulnerabilities.
Conclusion
ARM64 Pointer Authentication represents a significant architectural improvement in mitigating control-flow hijacking. By moving pointer integrity verification into silicon, the architecture dramatically raises the bar for exploit developers targeting modern operating system kernels and secure runtimes.
However, Pointer Authentication was designed as an integrity verification mechanism, not a general-purpose cryptographic defense. When systems suffer from low entropy pools, forking process models that fail to rotate keys, unprotected signing oracles, or speculative timing leaks such as PACMAN, the security invariants provided by PAC deteriorate.
Building resilient systems on ARM64 requires defense in depth:
- Hardware-enforced FPAC to prevent speculative window abuse.
- Atomic control transfers (
retaa/retab) to prevent transient register manipulation. - Compiler-level context diversification to stop cross-site pointer reuse.
- Software architectures that avoid persistent address-space and key duplication across worker boundaries.
Securing low-level execution paths requires understanding both the architectural specifications and the physical execution behaviors of modern microprocessors.