Architecture · 15 pages
PAC, BTI, and MTE
The three modern ARM hardware-security extensions — Pointer Authentication (PAC) against ROP, Branch Target Identification (BTI) against JOP, and Memory Tagging Extension (MTE) against use-after-free and buffer overflows. What they cost, where they're deployed, and the threat model each one defeats.
Memory-safety vulnerabilities — buffer overflows, use-after-free, type confusion — are the dominant source of severe security bugs in C and C++ codebases. Software mitigations (ASLR, stack canaries, CFI) help but aren't free and aren't complete. ARM has added three architectural extensions over the past decade specifically to make these bug classes much harder to exploit: PAC (Pointer Authentication Code, ARMv8.3), BTI (Branch Target Identification, ARMv8.5), and MTE (Memory Tagging Extension, ARMv8.5). Each targets a specific exploit pattern; together they raise the cost of a working memory-corruption exploit by orders of magnitude.
PAC defeats return-oriented programming (ROP). BTI defeats jump-oriented programming (JOP). MTE catches use-after-free and out-of-bounds heap access at the hardware level, with low enough overhead to run in production. Cortex-A from Apple, Qualcomm, MediaTek, and Samsung all ship PAC + BTI; MTE is rolling out on Pixel and Galaxy phones running Android 14+. Cortex-M85 brings PAC + BTI (collectively branded PACBTI) to the microcontroller world.
- PAC introduced
- ARMv8.3-A (2017). Optional on Cortex-A75+, A55+, A510+, A710+, etc. Required on ARMv8.6+.
- BTI introduced
- ARMv8.5-A (2018). Optional. Required on ARMv9.x.
- MTE introduced
- ARMv8.5-A (2018). Optional. Required on ARMv9.x.
- PACBTI on M-profile
- ARMv8.1-M extension. Implemented in Cortex-M85.
- PAC threat model
- Return-oriented programming (ROP) and forged function pointers. Sign on store, verify on use.
- BTI threat model
- Jump-oriented programming (JOP). Requires landing-pad instructions at every indirect-branch target.
- MTE threat model
- Use-after-free, linear buffer overflow on the heap (and stack with -fsanitize=memtag).
- MTE tag granule
- 16 bytes (one 4-bit tag per 16-byte aligned chunk of memory).
- Performance impact
- PAC: ~0.5–3% on typical workloads. BTI: <1%. MTE in sync mode: 3–8%; async mode: <2%.
PAC — Pointer Authentication Code
PAC takes the unused top bits of a 64-bit pointer (on a system with 48-bit virtual addresses, the top 16 bits are normally unused or sign-extended) and uses them to store a MAC (Message Authentication Code) computed over the pointer's value and a secret key held in a system register. When code signs a pointer, the hardware writes the MAC into the top bits. When code uses the pointer (to call a function, return, or dereference), the hardware verifies the MAC; if it matches, the top bits are stripped and the bare pointer is used. If it doesn't match, the hardware corrupts the pointer (sets the top bit to an unmapped value), turning the next dereference into a fault.
The signing key is held in PAC key registers (APIAKey_EL1, APIBKey_EL1, APDAKey_EL1, etc. — five keys total). The kernel sets these at boot from a CSPRNG; they're per-process on Linux (Android randomizes per app). The user has no access; the keys never leave the CPU.
The PAC instructions:
| Instruction | Action |
|---|---|
PACIA Xd, Xn | Sign Xd as instruction-address (return address) using key A and modifier Xn |
PACIB Xd, Xn | Sign with key B (Android uses A for return, B for indirect calls) |
PACDA Xd, Xn | Sign as data-address using key A |
AUTIA Xd, Xn | Verify Xd's PAC; if good, strip; if bad, corrupt to fault |
AUTIB Xd, Xn | Verify with key B |
PACIASP / RETAA | Short forms specifically for return addresses, modifier = SP |
XPACI Xd / XPACD Xd | Strip PAC bits without verification (for debugging/logging) |
The compiler emits PACIASP at function entry and RETAA (return + authenticate) at function exit:
my_function:
pacisp @ sign LR using SP as modifier; LR's top bits = MAC(LR, SP, keyA)
stp x29, x30, [sp, #-16]! @ save signed LR to stack
...
ldp x29, x30, [sp], #16 @ load signed LR from stack
retaa @ verify + return; fault if MAC wrongThe threat model is precise: an attacker who corrupts the stack-resident LR can overwrite it with any value, but they cannot forge a valid MAC unless they know the key. The RETAA checks and either returns to the legitimate caller or corrupts the pointer and faults — never returns to attacker-controlled code. ROP chains, which rely on chaining together return-gadgets, are defeated outright.
The performance cost of PAC is small: PACIASP is 1 cycle in the typical front-end, RETAA is 1 cycle in the back-end (replacing a normal ret). Workloads measure 0.5–3% overhead; Apple's iOS sees about 1–2% on average across the system.
BTI — Branch Target Identification
PAC protects return-style indirect branches. BTI protects forward indirect branches — function pointers, virtual calls, computed jumps. The mechanism is different: BTI requires every legitimate indirect-branch target to begin with a BTI instruction (encoded as a hint, so it's a NOP on older CPUs). An indirect branch lands somewhere; the CPU checks the very next instruction; if it's not the expected BTI variant, the CPU raises a Branch Target Exception.
my_indirect_target:
bti j @ landing pad — required for any BR/BLR landing here
...BTI variants:
BTI— generic landing pad. Accepts all indirect branches.BTI c— accepts indirect call (BLR). Used at function entry points exposed via function pointer.BTI j— accepts indirect jump (BR). Used at jump-table targets.BTI jc— accepts both.
The compiler decides which form to emit based on how a function is reachable. __attribute__((section(".text.func_called_via_ptr"))) plus -mbranch-protection=bti gets a BTI c at function entry. Switch-statement jump targets get BTI j.
The threat model: a JOP attack chains together gadgets ending in indirect branches. The attacker corrupts function pointers and jumps the CPU to gadget addresses. With BTI, only addresses immediately preceded by a BTI are valid targets — and a useful gadget is unlikely to land at a BTI. The attack surface shrinks dramatically.
Performance cost of BTI is essentially zero — landing-pad instructions are 1-cycle hints that fold into the front-end. The cost is in binary size: every function exposed via pointer needs a 4-byte BTI prologue.
MTE — Memory Tagging Extension
MTE is the most ambitious of the three. It tags every 16-byte aligned chunk of memory with a 4-bit tag (held in metadata storage maintained by the CPU). It also tags every pointer with a 4-bit tag in the pointer's top bits. On every load and store, the hardware compares the pointer's tag against the memory's tag; if they don't match, the access faults (in synchronous mode) or records the mismatch for later reporting (in asynchronous mode).
The expected use:
malloc(N)returns a tagged pointer. The allocator picks a random 4-bit tag, writes it to the memory's tag store for the allocated region, and OR's it into the pointer's top bits.- The application uses the tagged pointer for loads and stores. Every access checks: memory tag == pointer tag? OK. Mismatch? Fault.
free(p)clears the memory's tag (or sets it to a new random value, depending on the implementation). Any later use of the original pointer fails — its top bits hold the old tag; the memory now has a different one. Use-after-free caught.- A buffer overflow on the heap walks into the next allocation, which has a different tag. Out-of-bounds heap access caught.
The MTE instructions for the allocator:
| Instruction | Action |
|---|---|
IRG Xd, Xn | Insert random tag into Xd's top bits (used by allocator on alloc) |
STG Xt, [Xn] | Store tag — write Xt's tag bits as the memory tag at [Xn] |
ST2G Xt, [Xn] | Store tag, 32-byte version |
STZG Xt, [Xn] | Store tag + zero data |
LDG Xt, [Xn] | Load memory's current tag into Xt |
SUBP Xd, Xn, Xm | Subtract tagged pointers — strips tags before subtraction |
The detection happens on ordinary loads and stores — no instrumentation in the inner loop, no software fast-path overhead. The 4-bit tag space means 1/16 chance of false negative (an attacker who guesses the right tag won't be caught), but that's a powerful filter for randomized bugs.
Sync vs async mode
Synchronous (sync) mode — tag mismatch faults immediately, with the faulting PC pointing at the bad access. Slower (3–8%) because the CPU must check the tag in the critical path of the load. Used in development and high-assurance production.
Asynchronous (async) mode — mismatches are recorded in a register but the access proceeds. Software polls the register periodically (or on context switch) to harvest detected mismatches. Faster (<2% overhead) because tag checks happen out of the critical path. Used in production "sampling" deployments — catch most bugs, accept some delay in detection.
Android's MTE rollout uses async mode for the kernel and sync mode for high-assurance apps; the user-space tagged-malloc (Scudo on Android) integrates with both. The kernel periodically rolls the per-allocation tag to make the detection probabilistic in time as well as space.
PACBTI on Cortex-M85
The v8.1-M PACBTI extension brings PAC and BTI to M-profile. Cortex-M85 implements it; Cortex-M55 does not. The instructions are scaled down to 32-bit Thumb encodings but the semantics are the same as AArch64:
my_m85_function:
pac r12, lr, sp @ sign LR (with r12 scratch) using SP as modifier
push {r4, lr}
...
pop {r4, lr}
aut r12, lr, sp @ authenticate LR
bx lr @ returnOn Cortex-M85, function prologue is pac r12, lr, sp (1 cycle) and epilogue gains an aut r12, lr, sp (1 cycle). BTI landing pads are 1-cycle hints. The combined cost on typical RTOS workloads is ~1–2%, much like Cortex-A.
The key registers (APIAKey_EL1 on AArch64) translate to Cortex-M85's PAC_KEY_* registers, accessed via MRS/MSR. The key is set by Secure-world boot code; Non-secure code reads its own keys (different from Secure's).
For MTE, M-profile has nothing equivalent today — MTE is Cortex-A-only as of mid-2026.
Compiler enablement
GCC and Clang both support these extensions via -mbranch-protection:
# AArch64 — Linux/Android typical
-mbranch-protection=standard
# Equivalent to: pac-ret+leaf+bti
# Cortex-M85
-mbranch-protection=standard -mcpu=cortex-m85The standard setting enables PAC on returns (pac-ret), PAC on leaf functions (+leaf), and BTI (+bti). Granular options:
pac-ret— sign return addressespac-ret+leaf— also sign leaf-function return addresses (extra protection)pac-ret+b-key— use key B instead of Abti— emit BTI landing pads at indirect-branch targets
For MTE, the relevant compiler flags are part of HWASan / MTE-stack-tagging:
# Stack tagging (each stack frame gets its own tag)
clang -fsanitize=memtag-stack
# Heap tagging — paired with a tagged malloc (Scudo)
clang -fsanitize=memtag-heapAdoption status (as of 2026)
| Core | Arch | Pipe | DMIPS | FPU | DSP | MPU | TZ | MVE | Notes |
|---|---|---|---|---|---|---|---|---|---|
| Cortex-M0 | ARMv6-M | 3-stage | 0.84/MHz | — | Subset of Thumb-2. | ||||
| Cortex-M0+ | ARMv6-M | 2-stage | 0.95/MHz | — | Single-cycle I/O port. | ||||
| Cortex-M23 | ARMv8-M Baseline | 2-stage | 0.99/MHz | — | Secure & non-secure worlds. | ||||
| Cortex-M3 | ARMv7-M | 3-stage | 1.25/MHz | — | The classic mid-range core. | ||||
| Cortex-M33 | ARMv8-M Mainline | 3-stage | 1.50/MHz | SP | Used in nRF53, STM32L5/U5. | ||||
| Cortex-M4 | ARMv7E-M | 3-stage | 1.25/MHz | SP | Most popular general-purpose MCU core. | ||||
| Cortex-M55 | ARMv8.1-M | 4-stage | 1.6/MHz | SP/DP | 4–5× DSP/ML uplift vs. M4. | ||||
| Cortex-M7 | ARMv7E-M | 6-stage | 2.14/MHz | SP/DP | Used in i.MX RT, STM32H7. | ||||
| Cortex-M85 | ARMv8.1-M | 7-stage | 3.0/MHz | SP/DP | i.MX RT700, Renesas RA8. |
The general trajectory is clear: PAC + BTI is becoming default on new ARMv8.5+ silicon and new toolchains. MTE is ramping for production workloads now that the performance cost has come down on ARMv9 designs.
What these mitigations don't catch
A short list of bugs not protected:
- Logic bugs — wrong authorization check, mis-set permission flag. No memory-safety extension helps.
- Information disclosure — reading memory you shouldn't, without corrupting anything. MTE catches OOB read; PAC and BTI don't.
- Side channels — Spectre, Meltdown, cache-timing. These extensions don't change microarchitectural behavior.
- Crypto bugs — weak random, key reuse, side-channel-leaking implementations. Different problem space.
- Use-after-free of stack pointers — MTE-stack helps; without it, returned-pointer-to-local bugs are still exploitable.
PAC + BTI + MTE shrink the attack surface for memory-corruption exploits dramatically but don't eliminate it. Defense in depth — sanitizers in CI, fuzzing, code review, ASLR + KASLR, plus these hardware extensions — is still the right model.
Cross-references
- ARMv8-M architecture — PACBTI on Cortex-M85
- TrustZone-M deep dive — orthogonal isolation primitive
- Exclusive monitors — concurrency primitives, complementary
- ARMv8-A Architecture Reference Manual, sections D5 (PAC), D6 (BTI), D7 (MTE)
- "Pointer Authentication on ARMv8.3" (ARM whitepaper, 2017)
- "Memory Tagging Extension: Enhancing memory safety through architecture" (Cortex-A blog, 2019)
- Linux kernel
Documentation/arm64/pointer-authentication.rstandmemory-tagging-extension.rst