Using ARM64 Pointer Authentication to Harden Return‑Address Security
Learn how ARMv8.3‑A Pointer Authentication (PAC) signs pointers to stop return‑oriented programming attacks, how to enable it with compiler flags, and how to verify support on your hardware.
22 Nov 2025, 17:58 UTC

Why return‑address protection matters
Return‑oriented programming (ROP) chains reuse existing code snippets by overwriting return addresses on the stack. Pure software mitigations like stack canaries help, but they can be bypassed if an attacker gains arbitrary write primitives. ARMv8.3‑A introduced Pointer Authentication (PAC) to cryptographically sign pointers, making tampered return addresses detectable without significant performance cost.
How PAC works in a nutshell
PAC uses the unused top bits of a 64‑bit pointer (called TBIsign) to store a Message Authentication Code (MAC). The MAC is computed from the pointer value and a secret key stored in a special system register. When the pointer is later used, the CPU recomputes the MAC and compares it; a mismatch triggers a fault. Two key pairs exist: APIA for instruction pointers and APIB for data pointers. Instructions such as AUTIASP (authenticate) and PACIASP (sign) are inserted around function entry and exit.
Enabling PAC in your build
Both GCC and Clang expose PAC through the -mbranch-protection option. The most common setting for return‑address protection is:
gcc -O2 -mbranch-protection=pac-ret+leaf -o myprog myprog.c
pac-ret signs and authenticates the return address on function entry and exit; leaf adds signing for leaf functions (those that do not call other functions). If you also want to protect data pointers (e.g., vtable entries), add pac-data:
gcc -O2 -mbranch-protection=pac-ret+leaf,pac-data -o myprog myprog.c
These flags cause the compiler to emit PAC* and AUT* instructions automatically. No source‑code changes are required.
Checking for PAC support at runtime
Before relying on PAC‑signed binaries, verify that the CPU implements the feature. On Linux you can check from user space with:
# C snippet
#include <sys/auxv.h>
#include <stdio.h>
unsigned long hwcap = getauxval(AT_HWCAP);
if (hwcap & HWCAP_PAC)
puts("PAC supported");
else
puts("PAC NOT supported");
Compile and run the snippet; it needs no special privileges. Alternatively, inspect /proc/cpuinfo for the pac flag or look for the kernel boot message ARM: Enabling pointer authentication in dmesg.
Worked example: detecting missing PAC
Create a simple test program:
// pac_test.c
#include <stdio.h>
void leaf(void) { puts("inside leaf"); }
int main(void) { leaf(); return 0; }
Compile it with PAC enabled:
gcc -O2 -mbranch-protection=pac-ret -o pac_test pac_test.c
Run on a PAC‑capable CPU (e.g., a recent Cortex‑A76 or Neoverse N1):
$ ./pac_test
inside leaf
$ echo $?
0
The program exits normally, indicating the PAC instructions executed successfully.
Now run the same binary on an older ARMv8.0‑A core that lacks PAC (e.g., a Cortex‑A53 without the extension). Because the binary still contains PAC* instructions, the CPU will raise an undefined‑instruction exception, delivering SIGILL:
$ ./pac_test
Illegal instruction (core dumped)
$ echo $?
138
To avoid crashes on unsupported hardware, add a runtime check that replaces PAC calls with NOPs when the feature is absent. Many distributions provide a glibc wrapper that does this automatically when you link with -lpac or enable -mbranch-protection=pac-ret in conjunction with -moutline-atomics (the exact mechanism varies by toolchain).
Limitations and practical considerations
- CPU requirement: PAC is only available on ARMv8.3‑A and later. Deploying PAC‑signed binaries to older devices requires a fallback path or separate builds.
- Key secrecy: If an attacker can leak the PAC key (via side‑channel, speculative execution, or a software bug), they can forge valid signatures. Keeping the key in secure registers and avoiding speculative leaks is essential.
- Scope: PAC protects pointers that are signed and authenticated. It does not stop data‑only attacks that modify non‑pointer values (e.g., changing a length field). Complement PAC with other mitigations like Control‑Flow Integrity (CFI) or safe‑stack.
- Performance: Enabling PAC adds a few percent overhead on typical workloads, mainly due to the extra signing/authentication instructions. Measure with
perf staton your target workload to confirm acceptability.
Actionable checklist
- Verify CPU support:
getauxval(AT_HWCAP) & HWCAP_PACor check/proc/cpuinfofor thepacflag. - Recompile security‑critical binaries with
-mbranch-protection=pac-ret+leaf(addpac-dataif you need vtable protection). - Test the binary on a PAC‑enabled device to ensure normal execution.
- Test the same binary on a PAC‑disabled device; expect SIGILL unless you have installed a runtime‑fallback library.
- Monitor performance impact in staging; adjust optimization level or disable PAC for low‑latency paths if needed.
- Keep PAC keys confidential: avoid debugging facilities that expose system registers, and apply mitigations for speculative side‑channels.
By following these steps you can leverage ARM64’s built‑in pointer authentication to raise the bar against return‑oriented programming attacks while maintaining visibility into hardware requirements and performance trade‑offs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.