llvm / llvm/llvm-project

[compiler-rt][aarch64] Fast unwinder does not strip PAC unless the sanitizer runtime itself was built with -mbranch-protection

Open Beginner friendly
#218,255 1 comment 0 reactions 0 assignees View on GitHub
compiler-rt
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

`sanitizer_ptrauth.h` decides whether to strip a pointer authentication code from a return address based on whether **the sanitizer runtime itself** was compiled with `-mbranch-protection`:

https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/sanitizer_common/sanitizer_ptrauth.h

```c++
#if __has_feature(ptrauth_intrinsics)
# include
#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.
# define ptrauth_strip(__value, __key) ...xpaclri...
#else
# define ptrauth_strip(__value, __key) __value
#endif
```

That is the wrong binary to test. The only consumer that matters 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**, not by the runtime. Whether it carries a PAC depends on how *that* code was built. The two are independent binaries and routinely disagree: a distro that builds userspace with `-mbranch-protection=standard` (Fedora does for aarch64 — `/usr/lib/rpm/redhat/rpmrc`) while compiler-rt is built from source with default flags gets a runtime whose strip is compiled out, unwinding an application whose frame records are all signed.

The result is that `fast_unwind_on_malloc=1` (the default) produces garbage backtraces for every allocation. Symptoms are the obvious ones: nonsense frames in ASan reports, and LSan suppressions that silently fail to match because the frames they match on never symbolize. The workaround is `fast_unwind_on_malloc=0`, which is very expensive — every allocation pays a DWARF unwind.

The header's own comment already states 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."* The `#elif` contradicts it. `XPACLRI` is `HINT #7`, a NOP on cores without FEAT_PAuth, and on an unsigned canonical address it is the identity, so there is nothing to gate.

### Reproducer

Compiling the header's only real use, with and without branch protection on the *runtime*:

```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; }
```

```
$ clang++ --target=aarch64-unknown-linux-gnu -O2 -c -Icompiler-rt/lib pac.cpp
$ llvm-objdump -d pac.o
0000000000000000 :
0: 91001000 add x0, x0, #0x4
4: d65f03c0 ret
```

The strip is gone. With `-mbranch-protection=standard` added it appears:

```
0: d503233f paciasp
4: f81f0ffe str x30, [sp, #-0x10]!
8: aa0003fe mov x30, x0
c: d50320ff xpaclri
10: aa1e03e8 mov x8, x30
...
```

The first form is what ships in any compiler-rt built without branch protection; in such a build `libclang_rt.asan.a` contains no `xpaclri` in `UnwindFast` at all.

### Suggested fix

```diff
-#elif defined(__ARM_FEATURE_PAC_DEFAULT) && !defined(__APPLE__)
+#elif defined(__aarch64__) && !defined(__APPLE__)
```

which is what the comment says it is doing. Happy to send a PR.

Contributor guide

Open the contributing guide

Research direction

Start with compiler-rt/lib/sanitizer_common/sanitizer_ptrauth.h and the StackTrace::UnwindFast use of STRIP_PAC_PC. Build the provided aarch64 reproducer with and without runtime branch protection, then inspect the generated assembly. Done means the fast unwinder strips PAC independently of the sanitizer runtime's build flags while preserving identity behavior for unsigned addresses.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.