Diagnosing AArch64 PAC/BTI Failures in Mixed C/C++/Assembly Deployments
A diagnostic guide for intermittent SIGSEGV/SIGILL crashes after enabling ARM PAC/BTI on AArch64. Covers binary metadata checks, hardware/kernel verification, crash context capture, and targeted fixes for mixed C/C++/assembly/JIT deployments.
16 May 2026, 06:15 UTC

Recognizable Condition
After rebuilding with -mbranch-protection=standard (or pac-ret+bti), the process crashes with SIGSEGV or SIGILL at or near indirect calls and returns. The failure is intermittent across machines: it appears on ARMv8.3+ CPUs with PAC support or ARMv8.5+ with BTI, but not on older hardware or in VMs that lack the feature bits. The crash often surfaces only after a shared library is loaded, a JIT compiles a hot path, or a signal handler returns.
Cause/Diagnostic Quick Reference
| Symptom | Likely Cause | First Evidence |
|---|---|---|
SIGILL on bti instruction | Indirect branch landed on non-BTI landing pad | Faulting PC points to a bti c or missing bti at function entry |
SIGSEGV on return (ret) | Return-address signature mismatch (PAC) | LR value fails authentication; often cross-DSO boundary |
| Crash only when specific DSO loads | That DSO lacks GNU property note for PAC/BTI | readelf -n shows missing GNU_PROPERTY_AARCH64_FEATURE_1_AND |
| JIT-generated code faults | Emitter not inserting bti or signing returns | Fault inside runtime-generated memory region |
| Signal handler / unwind failure | Frame pointer or LR not signed per PAC ABI | Crash during stack unwind or sigreturn |
Check 1: Binary Metadata — Every Loaded Object
The loader enforces PAC/BTI process-wide based on the union of GNU property notes across the executable and every DSO. A single object built without the note (or stripped post-link) can disable enforcement for the whole process, or conversely, an object that claims support but lacks landing pads will crash when the hardware enforces.
Run on the executable and every .so in the deployment:
readelf -n /path/to/binary | grep -A4 'GNU_PROPERTY_AARCH64_FEATURE_1_AND'
Expected output shows flags:
Properties:
aarch64 feature 1 and: 0x3 [ BTI, PAC ]
Interpretation: Bit 0 = BTI, Bit 1 = PAC. If any DSO shows 0x0 or the note is absent, the process may run without enforcement (masking bugs) or with partial enforcement (causing crashes at boundaries). Stripping with strip --strip-all removes the note; use strip --keep-section=.note.gnu.property instead.
Check 2: Hardware and Kernel Exposure
Compiler flags are not proof of runtime enforcement. Verify the CPU advertises the features and the kernel enables them for userspace.
# CPU feature bits
lscpu | grep -E 'paca|pacg|bti'
# or
cat /proc/cpuinfo | grep Features | head -1
Look for paca (address authentication), pacg (generic authentication), bti. Absence means the hardware will treat PAC/BTI instructions as NOPs — the same binary runs fine here but crashes on newer silicon.
Confirm kernel support (Linux 5.7+ for BTI, 5.10+ for PAC userspace):
dmesg -T | grep -i -e paca -e pacg -e bti
uname -r
If the kernel lacks support, the feature bits may appear in /proc/cpuinfo but instructions trap or are ignored. glibc 2.33+ and binutils 2.36+ are required for full userspace enablement.
Check 3: Crash Context — Capture the Minimal Reproducer
When a crash occurs, collect a core file and register state. The faulting instruction and its neighbors reveal which protection fired.
# Enable cores (once)
ulimit -c unlimited
sysctl -w kernel.core_pattern=core.%e.%p
# After crash
gdb -c core./myapp.12345 /path/to/myapp
(gdb) bt
(gdb) info registers pc lr sp x16 x17
(gdb) disassemble /r $pc-32,$pc+32
Key observations:
- PC at
bti c→ indirect jump landed on a valid BTI landing pad but wrong type (e.g.,bti cfor call,bti jfor jump). - PC at instruction after missing
bti→ hardware enforced BTI, target lacked landing pad. SIGSEGVonretwith bad LR → PAC authentication failed. Check if LR was signed with a different key (cross-DSO) or not signed at all (assembly/JIT).- x16/x17 clobbered → these are intra-procedure-call scratch registers per AArch64 PCS; BTI landing pads expect them preserved across indirect calls.
Fixes Tied to Findings
Missing GNU Property Note on a DSO
Rebuild the offending library with -mbranch-protection=standard (or pac-ret+bti for explicit control). Ensure the link step propagates the note:
# Compile
clang -c -mbranch-protection=standard -fPIC foo.c -o foo.o
# Link shared library
clang -shared -mbranch-protection=standard foo.o -o libfoo.so
# Verify
readelf -n libfoo.so | grep -A2 'GNU_PROPERTY_AARCH64_FEATURE_1_AND'
If the DSO is third-party and cannot be rebuilt, you have two options:
1. Build a shim wrapper that isolates the unprotected DSO in a separate process (IPC).
2. Accept reduced protection for the whole process by linking the main executable without -mbranch-protection — but this disables enforcement globally.
Hand-Written Assembly Missing BTI Landing Pads
Every function entry reachable via indirect branch must start with a bti instruction matching the branch type:
// Assembly function called via function pointer
.global my_handler
.p2align 2
my_handler:
bti c // landing pad for indirect call
// ... function body ...
ret
// Assembly function used as jump target (e.g., switch table)
.global my_jump_target
.p2align 2
my_jump_target:
bti j // landing pad for indirect jump
// ...
For PAC, sign return addresses in assembly prologues/epilogues when mixing with C/C++:
my_asm_func:
paciasp // sign LR with SP as modifier
// ...
autiasp // authenticate LR
ret
Use .arch_extension pac,bti in the assembly source so the assembler emits the GNU property note automatically.
JIT Emitters Not Producing PAC/BTI Sequences
JITs (V8, LuaJIT, custom) must emit bti c at every function entry and sign returns if PAC is enabled. For AArch64, the JIT should query HWCAP2 via getauxval(AT_HWCAP2) at startup:
#include
#include
bool has_bti = (getauxval(AT_HWCAP2) & HWCAP2_BTI);
bool has_pac = (getauxval(AT_HWCAP2) & (HWCAP2_PACA | HWCAP2_PACG));
Only emit PAC/BTI instructions when the corresponding bit is set. On older CPUs, the same code runs as NOPs; on newer CPUs, missing instructions cause SIGILL/SIGSEGV.
Post-Link Stripping or Obfuscation Removing Notes
Tools like strip, objcopy, or binary hardeners may drop .note.gnu.property. Preserve it explicitly:
strip --keep-section=.note.gnu.property -o stripped.so original.so
Verify after any post-processing step.
Escalation Criteria
Escalate to toolchain, OS vendor, or hardware vendor when:
- Vendor-provided libraries (e.g., GPU drivers, proprietary codecs) lack property notes or crash under enforcement. Provide the vendor with
readelf -noutput and core register dump. - JIT-generated code fails only under specific microarchitectures (e.g., Neoverse V2 vs. Cortex-A78) despite correct
HWCAP2detection. Collectlscpu, kernel version, and minimal reproducer. - Signal handlers or setuid binaries behave differently. The kernel may disable PAC/BTI for setuid executables or during signal delivery; check
prctl(PR_GET_TAGGED_ADDR_CTRL)and kernel changelogs. - Cross-DSO PAC key mismatch persists after rebuilding all objects with identical
-mbranch-protectionflags. This may indicate a toolchain bug in key derivation (GCC vs. Clang, or different major versions).
Before filing a bug, produce a minimal two-DSO test case: one library built with -mbranch-protection=standard, one without. Load both and verify whether the loader policy changes process behavior (enforcement enabled vs. disabled). This isolates the defect to the binary boundary.
Verification After Fix
Re-run the fixed artifact on two machines:
- Non-PAC/BTI machine (or VM without feature bits): binary must start and run without
SIGILL. Instructions are NOPs. - PAC/BTI-capable machine: binary must run and enforcement must be active. Confirm by deliberately corrupting a return address in a test harness and observing
SIGSEGVonret.
Use readelf -n on the final deployed artifacts (not just build outputs) to confirm the GNU property notes survived packaging, signing, and distribution.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.