Summary

Pointer Authentication (PAC) and Branch Target Identification (BTI) are two security features available on arm64. They are designed to mitigate Return-Oriented Programming (ROP) and Jump-Oriented Programming (JOP) attacks respectively. See the references for all details about the attacks and the protection offered. This document is about the details of how PAC and BTI are enabled in Debian.

Software support

PAC and BTI can be enabled by compiling a program with the GCC flag -mbranch-protection=standard. In Debian, the flag has been enabled by default since August 2023 with the upload of dpkg 1.22.0, which added the branch setting to the hardening area. https://bugs.debian.org/1021292#84

Should the need arise, the feature can be disabled for a specific package with:

PAC

When it comes to enabling PAC, there is no additional requirement: as long as the program is built with -mbranch-protection=standard, the resulting ELF file will include PAC instructions, and CPUs supporting PAC will sign and authenticate addresses. Among the assembly instructions introduced to implement PAC, there are some used to sign an address (eg: PACIASP) and others to verify - or authenticate - an address (eg: AUTIASP, RETAA). To check if a given ELF file was built with PAC support, one can thus simply search for those instructions. For example:

objdump -d /usr/bin/ls | grep -E 'pac|aut'

BTI

BTI is different: it is enabled at runtime by the loader only if all execution units were built with BTI. This means that all .o files combined together by the linker need to be built with BTI support. There is a specific ELF property set on ELF files that support BTI, and it can be checked as follows:

readelf -n /usr/bin/ls | grep Properties
      Properties: AArch64 feature: BTI, PAC

Pretty much all programs are linked using crtbeginS.o and crtendS.o from GCC, as well as crti.o, Scrt1.o and crtn.o from glibc. For this reason, both GCC and glibc need to be built with BTI support in order for any other program to have it too.

In the specific case of Debian, thus, the following steps are prerequisites to enabling BTI:

  1. Build gcc-12 with BTI support https://bugs.debian.org/1057469

  2. Build glibc with gcc-12 from (1) and BTI support https://bugs.debian.org/1063515

  3. Build gcc-13 with BTI support https://bugs.debian.org/1055711

All bugs related to PAC and BTI in Debian should have the pac-bti usertag: https://bugs.debian.org/cgi-bin/pkgreport.cgi?users=debian-arm@lists.debian.org;tag=pac-bti

Hardware support

Not all CPUs support PAC and/or BTI. To check for PAC support one can look for the flags paca and pacg in /proc/cpuinfo. For BTI support, the flag is bti. To implement the features, the Arm architecture has been extended with additional assembly instructions. They are defined in NOP space, which means that CPUs without PAC or BTI support can still execute code built with the new instructions - they simply will execute some nop instructions instead.

In practice

PAC

The following program simulates a ROP attack corrupting the LR (or x30) registry, used on arm64 to store the return address of a function. Building the code without PAC results in the string ROP attack worked being printed.

#include <stdio.h>

void target() { printf("ROP attack worked\n"); }

void hello() {
  int xxx;
  printf("Trying to overwrite return address of hello()\n");
  *((&xxx) - 0x5) = target;
}

int main() {
  hello();
}

Building the program with PAC enabled results in a crash. Which crash precisely depends on the specific CPU in use. On systems with FEAT_FPAC the autiasp instruction fails with a SIGILL when failing to validate the value stored in LR, for example in the case of hello the program would crash before ret is called:

0xaaaaaaaa07ec <hello+48>       ldp     x29, x30, [sp], #32
0xaaaaaaaa07f0 <hello+52>       autiasp
0xaaaaaaaa07f4 <hello+56>       ret

On systems without FEAT_FPAC the autiasp instruction itself does not cause an exception, but instead it leaves a faulting value in LR. The program would therefore segfault at the very beginning of target when executing the paciasp instruction. See section D8.8.4 Faulting on pointer authentication of the Arm Architecture Reference Manual for the details.

BTI

The files under /proc/$pid/smaps can be inspected to check which mappings have BTI enabled. BTI support is shown as bt under VmFlags:

ffff980e0000-ffff98274000 r-xp 00000000 fe:01 8020                       /usr/lib/aarch64-linux-gnu/libc.so.6
Size:               1616 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                1204 kB
Pss:                  69 kB
Pss_Dirty:             0 kB
Shared_Clean:       1204 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:         1204 kB
Anonymous:             0 kB
KSM:                   0 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
FilePmdMapped:         0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB
THPeligible:           0
VmFlags: rd ex mr mw me bt 

Additionally, the following SystemTap script can be used to monitor the activity of the loader at runtime, see which shared libraries are being loaded and whether the have BTI enabled:

global loaded, enabled;

probe process("/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1").function("_dl_map_object") {
  f = user_string($loader->l_name);
  if (strlen(f) > 0 && !loaded[f]) {
    printf("Loaded %s\n", f);
    loaded[f] = 1
  }
}

probe process("/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1").function("_dl_bti_protect") {
  f = user_string($map->l_name);
  if (strlen(f) > 0 && !enabled[f]) {
    printf("BTI enabled for %s\n", f);
    enabled[f] = 1;
  }
}

In the following example, libm.so.6 is loaded with BTI support while libm.so.6 is loaded without it.

BTI enabled for /lib/aarch64-linux-gnu/libm.so.6
Loaded /lib/aarch64-linux-gnu/libpam_misc.so.0
Loaded /lib/aarch64-linux-gnu/libm.so.6

References