Using ARM64 Pointer Authentication to Harden Return Addresses
Learn how ARMv8.3-A Pointer Authentication (PAC) signs return addresses to detect tampering, how to enable it in your toolchain and kernel, and what trade‑offs to consider.
30 Jul 2026, 22:40 UTC

Problem: Return‑address overwrites enable control‑flow hijacks
When an attacker can write arbitrary data to the stack (e.g., via a buffer overflow), they may overwrite the saved link register (LR) and redirect execution to gadgets of their choosing. Classic mitigations like stack canaries help detect corruption before the overwrite, but they do not protect the return address itself once it has been altered.
Takeaway: PAC signs the return address so any change causes a fault
ARMv8.3‑A introduces Pointer Authentication (PAC). A per‑process secret key and a context modifier (commonly the current stack pointer) are used to create a cryptographic signature that is stored in the upper bits of a 64‑bit pointer. Before the pointer is used, the CPU authenticates it; a mismatch triggers a synchronous exception (SIGSEGV on Linux). This makes tampering with a signed return address detectable at the moment of use.
How PAC works for return addresses
Two independent keys exist: IA (instruction address) for code pointers and DA (data address) for data pointers. For return addresses the IA key is used. The instructions involved are:
pciasp– sign LR using the stack pointer as modifierautiasp– authenticate LR using the same modifier
In a typical function prologue the compiler emits pciasp to sign the saved LR before it is spilled to the stack. In the epilogue autiasp restores and checks the signature before the ret instruction executes. If an attacker overwrites the saved LR without also updating the signature, the authentication fails and the processor takes a fault.
Enabling PAC in your toolchain and kernel
Modern Linux distributions ship kernels with PAC support enabled (CONFIG_ARM64_PTR_AUTH=y) and the pac_notify/pac_reset syscalls used during context switches. To generate PAC‑signed code you need a compiler that understands the -mbranch-protection flag.
Compiler command (run on an ARMv8.3‑A capable host or QEMU)
# Replace aarch64-linux-gnu with your cross‑toolchain prefix if needed
clang -target aarch64-linux-gnu -mbranch-protection=pac-ret -O2 -o pac_demo pac_demo.c
Required permissions: none beyond normal user rights to invoke the compiler. Risk: the resulting binary will raise SIGILL on CPUs that lack the PAC instructions (e.g., Cortex‑A53).
Verify that PAC instructions were emitted
objdump -d pac_demo | grep -E 'pcia|autia'
You should see lines similar to pciasp x30, x30, xsp and autiasp x30, x30, xsp in the function prologue and epilogue.
Worked example: detecting a corrupted return address
Consider the following minimal C program:
/* pac_demo.c */
void leaf(void) { /* empty */ }
void victim(void) {
leaf();
}
int main(void) {
victim();
return 0;
}
Compile it with PAC as shown above. Run the binary under a debugger (e.g., gdb) and break at the start of victim:
gdb ./pac_demo
(gdb) break victim
(gdb) run
When the breakpoint hits, examine the saved LR on the stack (the value that will be restored before ret). Then, without re‑signing, overwrite that stack slot with an arbitrary address:
(gdb) set * (unsigned long long*)$sp = 0xdeadbeefdeadbeef
(gdb) continue
Because the overwritten LR no longer carries a valid PAC signature, the subsequent autiasp fails and the process receives a SIGSEGV. You can confirm this by checking the program’s exit status or the signal reported by gdb.
Repeat the experiment with a binary built using -mbranch-protection=none. In that case the same stack overwrite will not cause a fault; execution will jump to the injected address, demonstrating the protective effect of PAC.
Trade‑offs and limitations
PAC adds a small constant‑time overhead: signing and authenticating are single‑cycle instructions on most ARMv8.3‑A cores, and the code‑size increase is typically 1‑2 % for typical binaries. Performance studies on Neoverse N1 and Apple M1 show no measurable regression in SPEC CPU2017 workloads.
However, PAC alone only protects backward‑edge control flow (return addresses and function pointers). Forward‑edge indirect calls (e.g., virtual function pointers, jump tables) remain vulnerable unless combined with Branch Target Identification (BTI), which is available from ARMv8.5‑A. Without BTI an attacker can still jump to any gadget address that lacks a PAC check.
Other practical considerations:
- Hardware requirement: PAC instructions are unavailable on ARMv8.0‑ARMv8.2 cores (e.g., Cortex‑A53, ThunderX). Binaries built with PAC will raise
SIGILLon such hardware unless you implement runtime detection and provide a fallback binary. - Key inheritance: After
forkthe child process inherits the parent’s PAC keys. A compromised child could therefore forge PAC signatures for sibling processes if the keys are leaked via a side channel. - Stack‑clashing attacks: If an attacker can overwrite the stack pointer used as the modifier before the LR is signed, they can bypass PAC. Compile with
-fstack-clash-protectionand maintain guard pages to mitigate this. - JIT runtimes: Virtual machines that generate code at runtime must explicitly sign those pointers and manage PAC keys via
prctl(PR_PAC_RESET_KEYS, …). Not all JITs had completed this work as of 2024.
Actionable steps
- Confirm your target hardware supports PAC:
lscpu | grep -i pacor readID_AA64ISAR1_EL1with a tool likecpuid. ThePAfield should be ≥ 1. - Ensure the kernel is PAC‑aware:
grep CONFIG_ARM64_PTR_AUTH /boot/config-$(uname -r)should returny. - Build your application with
-mbranch-protection=pac-ret(orstandardfor both return and function pointers). - Verify the emitted instructions with
objdumpas shown above. - Test the protection: run the binary under a debugger, overwrite a signed return address, and confirm a SIGSEGV occurs.
- If you must run on older CPUs, maintain a separate binary built without PAC and select it at runtime based on a hardware check.
By following these steps you can leverage ARM64 Pointer Authentication to detect return‑address tampering with minimal performance cost, while remaining aware of its scope and the complementary mitigations needed for full control‑flow integrity.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.