Arm Security Features for Linux System Administrators
Explore Arm security features for Linux, including PAC, BTI, and GCS, with practical steps to check CPU support, configure protection, and diagnose faults

Introduction
Arm processors provide various security features designed to mitigate Control Flow attacks, such as those using Return-Oriented Programming (ROP) and Jump-Oriented Programming (JOP) techniques. In this category of attacks, successful exploits overwrite the return address of the function currently executing (ROP). Alternatively, they overwrite the target pointer of an upcoming jump (JOP) with the address of a memory area controlled by the attacker.
The Arm security features designed to mitigate Control-Flow attacks are Pointer Authentication (PAC), Branch Target Identification (BTI) and Guarded Control Stack (GCS). When an attack is detected, the offending process is terminated, typically with a SIGILL or SIGSEGV. PAC, BTI and GCS are implemented in hardware, but using them requires cooperation from both the kernel and userspace. You need all three pieces in place: an Arm CPU that supports them, a recent enough Linux kernel, and a distribution that enables them in userspace.
This article aims to illustrate the features to Linux system administrators. For each feature, it covers:
- The Linux version that first introduced the feature, with links to the relevant commits
- How to determine whether an Arm processor supports the feature
- How to enable and disable the feature
- How the feature works in practice, using simple proof-of-concept programs to verify that it prevents the attack
In addition to the signal number itself, for example 11 (SIGSEGV) or 4 (SIGILL), the kernel supplies an associated code, si_code, identifying the specific condition that caused the signal to be generated.
We added support in Linux 7.1 to expose si_code to user space, and in systemd 261 to record and report it through systemd-coredump. This makes it possible for system administrators to distinguish ordinary application faults from those raised by control-flow protection mechanisms such as Pointer Authentication (PAC), Branch Target Identification (BTI), and Guarded Control Stack (GCS). Install and enable systemd-coredump and update the system to Linux 7.1 and systemd 261. These versions provide the facilities needed to examine faults on Arm systems that use control-flow protection mechanisms.
The following table provides a summary of each feature. The rest of the article elaborates on the details.
| Feature | Linux version | cpuinfo flag | Disable cmdline | SIGNAL / CODE |
|---|---|---|---|---|
| PAC | 5.0 | paca, pacg |
arm64.nopauth |
SIGILL (ILL_ILLOPN)SIGSEGV (SEGV_MAPERR) |
| BTI | 5.8 | bti |
arm64.nobti |
SIGILL (ILL_ILLOPC) |
| GCS | 6.13 | gcs |
arm64.nogcs |
SIGSEGV (SEGV_CPERR) |
PAC
Pointer Authentication was initially introduced in Linux 5.0. Refer to the following commits for the initial implementation details:
- arm64: add pointer authentication register bits
- arm64/cpufeature: detect pointer authentication
- arm64: add basic pointer authentication support
Use the lscpu command to determine whether an Arm CPU supports pointer authentication. If the output contains the paca or pacg flags, pointer authentication support is present.
$ lscpu | grep pac
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp uscat ilrcpc flagm paca pacg
Additionally, the kernel will print the following lines if pointer authentication is available:
CPU features: detected: Address authentication (architected QARMA5 algorithm)
CPU features: detected: Generic authentication (architected QARMA5 algorithm)
Exact output for the string in brackets may vary depending on implementation details. For example, IMP DEF algorithm could be printed instead.
Linux distributions typically enable pointer authentication by default. The relevant kernel configuration options to enable the feature in self-built kernels are:
CONFIG_ARM64_PTR_AUTH=y
CONFIG_ARM64_PTR_AUTH_KERNEL=y
The former enables pointer authentication for userspace programs, and the latter enables it for the kernel.
To disable pointer authentication system-wide, boot the kernel with arm64.nopauth.
User-space binaries use Pointer Authentication only if they were compiled with PAC support. Major Linux distributions generally enable this for their AArch64 packages by default. This can be verified by inspecting the ELF metadata with readelf. For example:
$ readelf -n /usr/bin/ls | grep PAC
Properties: AArch64 feature: BTI, PAC
At runtime, the behavior of Pointer Authentication depends on whether the system supports the extension FEAT_FPAC, Faulting on AUT instructions. On systems with FEAT_FPAC, the offending process receives an Illegal instruction (SIGILL) signal and code ILL_ILLOPN. On systems without FEAT_FPAC, a Segmentation fault (SIGSEGV) is used instead.
To determine whether FEAT_FPAC is supported, run the following program. The program prints FPAC: yes if the feature is supported, FPAC: no otherwise.
#include <asm/hwcap.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/auxv.h>
static inline uint64_t read_id_aa64isar1_el1(void)
{
uint64_t v;
asm("mrs %0, ID_AA64ISAR1_EL1":"=r"(v));
return v;
}
static inline uint64_t read_id_aa64isar2_el1(void)
{
uint64_t v;
asm("mrs %0, ID_AA64ISAR2_EL1":"=r"(v));
return v;
}
int main(void)
{
if (!(getauxval(AT_HWCAP) & HWCAP_CPUID)) {
puts("No HWCAP_CPUID; cannot query ID registers from EL0");
return 1;
}
uint64_t isar1 = read_id_aa64isar1_el1();
uint64_t isar2 = read_id_aa64isar2_el1();
unsigned apa = (isar1 >> 4) & 0xf;
unsigned api = (isar1 >> 8) & 0xf;
unsigned apa3 = (isar2 >> 12) & 0xf;
printf("FPAC: %s\n",
(apa >= 4 || api >= 4 || apa3 >= 4) ? "yes" : "no");
return 0;
}
To demonstrate the feature, consider the following basic Return-Oriented-Programming proof of concept:
// rop.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
void target() {
printf("ROP attack worked\n");
exit(-1);
}
void hello() {
int xxx;
printf("Trying to overwrite return address of hello()\n");
*((&xxx) - 0x5) = (intptr_t)target;
}
int main() {
hello();
}
Build the program without Pointer Authentication to reproduce a successful attack:
$ gcc rop.c && ./a.out
Trying to overwrite return address of hello()
ROP attack worked
Pass -mbranch-protection=pac-ret to the compiler to enable Pointer Authentication. On systems without FEAT_FPAC, the attack is stopped with a SIGSEGV:
$ gcc -mbranch-protection=pac-ret rop.c && ./a.out
Trying to overwrite return address of hello()
The signal number and code can be inspected with coredumpctl:
Segmentation fault (core dumped)
$ coredumpctl info | grep Signal
Signal: 11 (SEGV) si_code: SEGV_MAPERR
On systems with FEAT_FPAC the attack is stopped with a SIGILL instead:
$ gcc -mbranch-protection=pac-ret rop.c && ./a.out
Trying to overwrite return address of hello()
Illegal instruction (core dumped) ./a.out
The signal number and code can be inspected with coredumpctl:
$ coredumpctl info | grep Signal
Signal: 4 (ILL) si_code: ILL_ILLOPN
Use gdb to confirm that the processor generates the fault on the AUT instruction itself.
$ coredumpctl gdb $PID
[...]
Core was generated by `./a.out'.
Program terminated with signal SIGILL, Illegal instruction.
#0 0x0000b5c1ea9408fc in hello ()
(gdb) disassemble
Dump of assembler code for function hello:
0x0000b5c1ea9408c8 <+0>: paciasp
0x0000b5c1ea9408cc <+4>: stp x29, x30, [sp, #-32]!
0x0000b5c1ea9408d0 <+8>: mov x29, sp
0x0000b5c1ea9408d4 <+12>: adrp x0, 0xb5c1ea940000
0x0000b5c1ea9408d8 <+16>: add x0, x0, #0x960
0x0000b5c1ea9408dc <+20>: bl 0xb5c1ea940738 <puts@plt+40>
0x0000b5c1ea9408e0 <+24>: adrp x0, 0xb5c1ea940000
0x0000b5c1ea9408e4 <+28>: add x1, x0, #0x8a8
0x0000b5c1ea9408e8 <+32>: add x0, sp, #0x1c
0x0000b5c1ea9408ec <+36>: sub x0, x0, #0x14
0x0000b5c1ea9408f0 <+40>: str w1, [x0]
0x0000b5c1ea9408f4 <+44>: nop
0x0000b5c1ea9408f8 <+48>: ldp x29, x30, [sp], #32
=> 0x0000b5c1ea9408fc <+52>: autiasp
0x0000b5c1ea940900 <+56>: ret
End of assembler dump.
(gdb)
BTI
Branch Target Identification was initially introduced in Linux 5.8. Refer to the following commits for the initial implementation details:
- arm64: Basic Branch Target Identification support
- arm64: elf: Enable BTI at exec based on ELF program properties
Use the lscpu command to determine whether an Arm CPU supports BTI. If the output contains the bti flag, the feature is present.
$ lscpu | grep bti
Flags: [...] dgh bti ecv afp wfxt
Additionally, the kernel will print the following lines if BTI is available:
CPU features: detected: Branch Target Identification
Linux distributions typically enable Branch Target Identification by default. The relevant kernel configuration option to enable the feature in self-built kernels is:
CONFIG_ARM64_BTI=y
To disable BTI system-wide, boot the kernel with `arm64.nobti.
User-space binaries use Branch Target Identification only if they were compiled with BTI support. Major Linux distributions generally enable this for their AArch64 packages by default. This can be verified by inspecting the ELF metadata with readelf. For example:
$ readelf -n /usr/bin/ls | grep BTI
Properties: AArch64 feature: BTI, PAC
The following program can be used to test the feature. It jumps to a location that is not a landing pad, thereby mimicking a jump-oriented programming attack.
// jop.c
#include <stdio.h>
asm(
".global bad_target\n"
"bad_target:\n"
" nop\n"
" ret\n");
int main(void) {
printf("Trying to jump onto a non-landing pad\n");
asm volatile(
"adr x16, bad_target\n"
"blr x16\n");
printf("JOP attack worked\n");
return 0;
}
Build the program without BTI to reproduce a successful attack:
$ gcc jop.c && ./a.out
Trying to jump onto a non-landing pad
JOP attack worked
Now build and run the same program with BTI. The attack is stopped with a SIGILL.
$ gcc -mbranch-protection=bti jop.c && ./a.out
Trying to jump onto a non-landing pad
Illegal instruction (core dumped) ./a.out
$ coredumpctl info | grep Signal
Signal: 4 (ILL) si_code: ILL_ILLOPC
When BTI is enabled, the bti instruction is a valid landing pad. Jumping onto any other instruction results in a fault. Use gdb to confirm that the processor generates the fault, in this case due to landing on a nop.
$ coredumpctl gdb $PID
[...]
Core was generated by `./a.out'.
Program terminated with signal SIGILL, Illegal instruction.
#0 0x0000b159162c0828 in bad_target ()
(gdb) disassemble
Dump of assembler code for function bad_target:
=> 0x0000b159162c0828 <+0>: nop
0x0000b159162c082c <+4>: ret
End of assembler dump.
(gdb)
To determine whether the kernel has enabled BTI enforcement on a running process, examine /proc/$PID/smaps. The VmFlags field contains bt for mappings on which the kernel enforces BTI.
For example:
$ sudo grep VmFlags /proc/[0-9]*/smaps | head -2
/proc/1/smaps:VmFlags: rd ex mr mw me bt
/proc/1/smaps:VmFlags: rd mr mw me ac
BTI is enforced for a process if at least one of its mappings contains the bt flag.
The following command reports the number of mappings with BTI enforcement enabled for each running process:
$ sudo grep VmFlags /proc/[0-9]*/smaps | awk -F: '{ bti[$1] += ($3 ~ "bt") } END { for (p in bti) { printf("%s: %d\n", p, bti[p]); } }'
/proc/954/smaps: 42
/proc/930/smaps: 23
/proc/659/smaps: 9
/proc/366/smaps: 19
/proc/955/smaps: 0
/proc/740/smaps: 20
/proc/648/smaps: 14
/proc/595/smaps: 21
/proc/765/smaps: 5
A value greater than zero indicates that the kernel enforces BTI for one or more mappings in the process. A value of zero indicates that the kernel does not enforce BTI for any of the process's mappings.
In this example, the kernel does not enforce BTI for any mappings in process 955.
GCS
Guarded Control Stack was initially introduced in Linux 6.13. Refer to the following commits for some of the implementation details:
- prctl: arch-agnostic prctl for shadow stack
- arm64/cpufeature: Runtime detection of Guarded Control Stack (GCS)
- arm64/hwcap: Add hwcap for GCS
- arm64/gcs: Implement shadow stack prctl() interface
Use the lscpu command to determine whether an Arm CPU supports GCS. If the output contains the gcs flag, the feature is present. Additionally, the kernel will print the following line if Guarded Control stack is available:
CPU features: detected: Guarded Control Stack (GCS)
Linux distributions typically enable Guarded Control Stack in the kernel by default. The relevant kernel configuration option to enable the feature in self-built kernels is:
CONFIG_ARM64_GCS=y
Contrary to PAC and BTI, GCS is not enabled by default in user-space. The GLIBC tunable glibc.cpu.aarch64_gcs needs to be set in order for the feature to be turned on. To do so system-wide: add GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2 to /etc/environment, and boot with systemd.setenv=GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2.
The tunable accepts the following values:
- 0 Disabled: GCS will not be enabled for the process (default)
- 1 Enforced: check binary markings and fail if any of the binaries are not marked
- 2 Optional: check markings but keep GCS off if there is an unmarked binary
- 3 Optional: enable GCS, regardless of binary markings
To disable GCS system-wide regardless of the values above, boot the kernel with `arm64.nogcs.
User-space binaries use Guarded Control Stack only if they were compiled with GCS support. Major Linux distributions generally enable this for their AArch64 packages by default. This can be verified by inspecting the ELF metadata with readelf. For example:
$ readelf -n /usr/bin/ls | grep GCS
Properties: AArch64 feature: BTI, PAC, GCS
The same program used to demonstrate Pointer Authentication can be used for GCS as well. Build the proof-of-concept with -mbranch-protection=gcs:
$ gcc -mbranch-protection=gcs rop.c
Now build and run the program with GCS enabled. The attack is stopped with a SIGSEGV, and the SEGV_CPERR code (Control Protection Error).
$ GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1 ./a.out
Trying to overwrite return address of hello()
Segmentation fault (core dumped) GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1 ./a.out
$ coredumpctl info | grep Signal:
Signal: 11 (SEGV) si_code: SEGV_CPERR
Check for CPU feature support in systemd
Systemd v262 added aarch64 ConditionCPUFeature support. This allows services to be deployed independently of the underlying CPU, and started only on systems that provide the required support. For example, to test for bti support:
[Unit]
Description=Example service requiring arm64 BTI
ConditionCPUFeature=bti
[Service]
Type=oneshot
ExecStart=/usr/libexec/example-bti-helper
[Install]
WantedBy=multi-user.target
The condition can be tested from the command-line as well:
$ systemd-analyze condition 'ConditionCPUFeature=bti' && echo OK
OK
References
Re-use is only permitted for informational and non-commercial or personal use only.
