Summary

Guarded Control Stack (GCS) is a security feature available on arm64 designed to mitigate Return-Oriented Programming (ROP) attacks. GCS is available starting with Armv9.3, and it is opt-in: the user can enable it with a glibc tunable.

Software support

Toolchain support for GCS has been introduced starting with GCC 15 and binutils 2.43. GCS is enabled by default in Debian: the build flags set by dpkg-buildflags include -mbranch-protection=standard, which enables GCS as well as PAC and BTI.

Dealing with assembly code

Binaries that support GCS are annotated with a ELF property that can be read with readelf. Programs written in C/C++ get automatically annotated by the toolchain.

$ readelf -n ./hello | grep AArch64
Properties: AArch64 feature: BTI, PAC, GCS

Assembly code needs to be manually annotated. Consider the following example:

$ echo > example.c ; echo > assembly.S ; cc -mbranch-protection=standard -c example.c assembly.S

The resulting object file example.o does have the required properties, while assembly.o does not. One can define the properties in assembly.S as follows. See this series of articles for an in-depth explanation of enabling PAC/BTI support in assembler.

#define GNU_PROPERTY_AARCH64_BTI 1
#define GNU_PROPERTY_AARCH64_POINTER_AUTH 2
#define GNU_PROPERTY_AARCH64_GCS 4

.pushsection .note.gnu.property, "a"; /* Start a new allocatable section */
.balign 8; /* align it on a byte boundry */
.long 4; /* size of "GNU\0" */
.long 0x10; /* size of descriptor */
.long 0x5; /* NT_GNU_PROPERTY_TYPE_0 */
.asciz "GNU";
.long 0xc0000000; /* GNU_PROPERTY_AARCH64_FEATURE_1_AND */
.long 4; /* Four bytes of data */
.long (GNU_PROPERTY_AARCH64_BTI|GNU_PROPERTY_AARCH64_POINTER_AUTH|GNU_PROPERTY_AARCH64_GCS); /* BTI or PAC or GCS are enabled */
.long 0; /* padding for 8 byte alignment */
.popsection; /* end the section */

One can see how GCC creates the notes section by doing the following:

$ echo > example.c ; gcc -S -mbranch-protection=standard example.c -o -
        .arch armv8-a
        .file   "example.c"
        .text
        .ident  "GCC: (Debian 15.2.0-3) 15.2.0"
        .section        .note.GNU-stack,"",@progbits
        .section        .note.gnu.property,"a"
        .align  3
        .word   4
        .word   16
        .word   5
        .string "GNU"
        .word   3221225472
        .word   4
        .word   7
        .align  3

Linker warnings

In order to help with feature adoption, binutils prints a warning by default when building a program with GCS if the shared libraries needed by the program do not have GCS turned on. For example:

/usr/lib/gcc/aarch64-linux-gnu/15/../../../aarch64-linux-gnu/libncursesw.so.6: warning: GCS is required by -z gcs, but this shared library lacks the necessary property note. The dynamic loader might not enable GCS or refuse to load the program unless all the shared library dependencies have the GCS marking.

The warning does not mean that there is anything wrong with the program being built, just that GCS won't work if using the shared library currently installed on the build system (libncursesw.so.6 in the example above). The following can be added to LDFLAGS to silence the warning if needed:

-Wl,-z,gcs-report-dynamic=none

One can do that in debian/rules as follows:

ifeq ($(DEB_TARGET_ARCH),arm64)
export DEB_LDFLAGS_MAINT_APPEND=-Wl,-z,gcs-report-dynamic=none
endif

GLIBC tunable

Set the following glibc tunable to run the program with GCS enabled:

$ GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1 ./hello

The values of glibc.cpu.aarch64_gcs are:

Value

Meaning

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

Hardware support

The kernel detects the feature at runtime on systems supporting it. Check for the following string in dmesg to see if your system supports GCS:

CPU features: detected: Guarded Control Stack (GCS)

References