[compiler-rt] [compiler-rt][sanitizer] Strip PAC from return addresses unconditionally on aarch64 (PR #218259)

Avi Kivity via llvm-commits llvm-commits at lists.llvm.org
Sun Aug 23 09:16:18 PDT 2026


https://github.com/avikivity created https://github.com/llvm/llvm-project/pull/218259

Fixes #218255.

`sanitizer_ptrauth.h` gates `STRIP_PAC_PC` on `__ARM_FEATURE_PAC_DEFAULT`, i.e. on whether the sanitizer runtime itself was compiled with `-mbranch-protection`. Its only real consumer is `StackTrace::UnwindFast`:

```c++
uhwptr pc1 = STRIP_PAC_PC((void *)frame[1]);
```

`frame[1]` is a return address spilled into a frame record by the code being unwound, so whether it carries a PAC is a property of *that* code, not of the runtime. The two are separate binaries and routinely disagree — a distro that builds userspace with `-mbranch-protection=standard` (Fedora does for aarch64) while compiler-rt is built from source with default flags gets a runtime with the strip compiled out, unwinding an application whose frame records are all signed. The fast unwinder then records garbage for every allocation: nonsense frames in reports, and LSan suppressions that silently never match.

The header's own comment already describes the intended behaviour — *"Let's stripping the PAC unconditionally because xpaclri is in the NOP space so will do nothing when it is not enabled or not available"* — so this just makes the code agree with it, and rewrites the comment to say why the condition cannot be tested for here.

`XPACLRI` is `HINT #7`: a NOP on cores without FEAT_PAuth, and the identity on an unsigned canonical address, so it is safe to run unconditionally.

### Effect

Compiling the header's only real use, cross-compiled with no `-mbranch-protection`:

```c++
typedef unsigned long uptr;
#include "sanitizer_common/sanitizer_ptrauth.h"
extern "C" uptr next_pc(uptr pc) { return STRIP_PAC_PC((void *)pc) + 4; }
```

Before:

```
0000000000000000 <next_pc>:
       0: 91001000      add     x0, x0, #0x4
       4: d65f03c0      ret
```

After:

```
0000000000000000 <next_pc>:
       0: f81f0ffe      str     x30, [sp, #-0x10]!
       4: aa0003fe      mov     x30, x0
       8: d50320ff      xpaclri
       c: aa1e03e8      mov     x8, x30
      10: aa1f03fe      mov     x30, xzr
      14: 91001100      add     x0, x8, #0x4
      18: f84107fe      ldr     x30, [sp], #0x10
      1c: d65f03c0      ret
```

Apple targets are unaffected: `__has_feature(ptrauth_intrinsics)` still takes precedence, and the `!defined(__APPLE__)` guard is kept.

No test — reproducing the failure needs an aarch64 host with FEAT_PAuth running a PAC-built process under ASan, which the sanitizer_common test suite has no way to require. The change is visible in codegen as shown above.


>From f8ffa6f7fba190702183876771712c441c5b0e5d Mon Sep 17 00:00:00 2001
From: Avi Kivity <avi at scylladb.com>
Date: Sun, 23 Aug 2026 19:15:40 +0300
Subject: [PATCH] [compiler-rt][sanitizer] Strip PAC from return addresses
 unconditionally on aarch64

sanitizer_ptrauth.h gates STRIP_PAC_PC on __ARM_FEATURE_PAC_DEFAULT, so
the strip only exists if the sanitizer runtime itself was compiled with
-mbranch-protection. That is the wrong binary to test. Its only real
consumer is StackTrace::UnwindFast:

  uhwptr pc1 = STRIP_PAC_PC((void *)frame[1]);

frame[1] is a return address spilled into a frame record by the code
being unwound, so whether it carries a PAC depends on how that code was
built, not on how the runtime was built. The two disagree whenever a
distro builds userspace with -mbranch-protection=standard (Fedora does
for aarch64) while compiler-rt is built from source with default flags:
every backtrace the fast unwinder records is then garbage, which shows
up as nonsense frames in reports and as LSan suppressions silently
failing to match.

The header's own comment already says the strip is meant to be
unconditional, "because xpaclri is in the NOP space so will do nothing
when it is not enabled or not available". Make the code agree with it.

Fixes #218255
---
 .../lib/sanitizer_common/sanitizer_ptrauth.h        | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_ptrauth.h b/compiler-rt/lib/sanitizer_common/sanitizer_ptrauth.h
index 265a9925a15a0..ea16a032047ab 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_ptrauth.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_ptrauth.h
@@ -11,11 +11,14 @@
 
 #if __has_feature(ptrauth_intrinsics)
 #  include <ptrauth.h>
-#elif defined(__ARM_FEATURE_PAC_DEFAULT) && !defined(__APPLE__)
-// On the stack the link register is protected with Pointer
-// Authentication Code when compiled with -mbranch-protection.
-// Let's stripping the PAC unconditionally because xpaclri is in
-// the NOP space so will do nothing when it is not enabled or not available.
+#elif defined(__aarch64__) && !defined(__APPLE__)
+// On the stack the link register is protected with a Pointer Authentication
+// Code when the code that spilled it was compiled with -mbranch-protection.
+// That is a property of the code being unwound, not of this runtime, so it
+// cannot be tested for here (__ARM_FEATURE_PAC_DEFAULT would describe the
+// wrong binary). Strip unconditionally instead: xpaclri is in the NOP space,
+// so it does nothing where pointer authentication is not enabled or not
+// available, and it is the identity on an unsigned canonical address.
 #  define ptrauth_strip(__value, __key) \
     ({                                  \
       __typeof(__value) ret;            \



More information about the llvm-commits mailing list