algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

PAC and MTE — how ARM64's new walls fall

2026-06-023 min readpwn internals arm

x86 leaned on the stack canary for two decades. ARM64 raised the bar twice: PAC cryptographically signs pointers, and MTE tags memory so a stale or out-of-bounds pointer doesn't match its target. They're genuinely stronger. They also fail to the same primitives we always reach for — leaks, oracles, and brute force against a field that's only a few bits wide.

target
arch
aarch64
PAC
on (ARMv8.3 pointer authentication)
MTE
on (ARMv8.5 memory tagging, synchronous)
note
both are probabilistic / key-based, not absolute

PAC — a MAC in the pointer's spare bits#

A 64-bit pointer doesn't use all 64 bits for the address. PAC fills the unused high bits with a cryptographic MAC (QARMA) computed over the pointer, a 64-bit modifier (context salt), and a secret key held in a system register. The compiler signs the return address on entry and authenticates it on exit:

PAC prologue / epilogueasm
1
2
3
4
5
6
paciasp                 ; sign x30 (return addr) with key A, modifier = sp
stp  x29, x30, [sp,-16]!
; ... function body ...
ldp  x29, x30, [sp],16
autiasp                 ; authenticate x30 against (key A, sp); corrupt it on mismatch
ret

Overwrite the saved x30 with a raw gadget address and autiasp fails: it flips the pointer to a poison value and the ret faults (on FPAC, it faults immediately). You can't forge the MAC without the key. So you don't forge it:

  • Signing gadget / oracle. Find code that runs pac* on a pointer you control with the key and modifier you need — now the hardware signs your gadget for you. The most reliable PAC bypass there is.
  • Leak and replay. A signed pointer is valid for its (key, modifier) pair. If the same modifier recurs (or pointers are signed with modifier 0), leak one signed pointer and reuse it wherever that context applies.
  • Brute force a small field. With a 48-bit VA the PAC is ~16 bits. If failures don't hard-crash (a forking service), guessing is cheap — expect about 2b1 tries for a b-bit field, and here b16.
  • PACMAN. A speculative side channel leaks whether a guessed PAC was correct without the architectural crash — turning the brute force above into a silent oracle.

MTE — a 4-bit lock on every allocation#

MTE tags each aligned 16-byte granule of memory with a 4-bit value, and stores a 4-bit tag in the pointer's top byte (top-byte-ignore). Every access checks pointer tag == memory tag; a mismatch faults. The allocator tags each new allocation randomly, retags on free, and gives neighbours different tags:

why UAF and linear overflow get caughttxt
1
2
3
4
5
6
alloc A -> memory granules tagged 0x7, pointer tagged 0x7   ✓ access ok
free  A -> granules retagged 0xc                            ; pointer still 0x7
use   A -> 0x7 != 0xc                                       ✗ FAULT (use-after-free)

alloc A (tag 0x7) | alloc B (tag 0x2)
overflow A into B -> writing with the 0x7 pointer into 0x2  ✗ FAULT (overflow)

Deterministic-feeling, but it's probabilistic — only 4 bits:

  • Guess the tag. 16 possible tags, so a blind forge matches 1 in 16 — P(match)=116. In async mode, or anywhere a wrong guess doesn't kill the process, you just retry.
  • Leak the tag. Any primitive that reveals the target pointer reveals its tag too; forge a pointer with the matching top byte and the check passes every time.
  • Stay in the granule. Corruption within the same 16-byte granule shares the tag — MTE never sees it. Sub-granule overwrites slip through.
  • Hit untagged memory. MTE usually covers the heap; stack/globals may be untagged depending on config — corrupt those instead.
the shape of a modern aarch64 exploit

Leak first (a tag for MTE, a signed pointer or signing gadget for PAC), then the classic chain runs — the mitigations changed the price of the leak, not the existence of the bug:

txt
segfault{16_bits_is_not_a_lot_of_bits}

PAC and MTE are the best mainstream memory-safety mitigations shipping — they turn "corrupt a pointer" into "leak, or guess a small field, or find a gadget that signs for you." That's a real tax. But it's a tax, not a wall: the bug still exists, and the same leak that armed every x86 exploit arms these too.