[PATCH v3 2/2] aarch64: Tests for locking GCS
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Thu Feb 12 16:58:11 GMT 2026
On 09/02/26 08:58, Yury Khrustalev wrote:
> Check that GCS is locked properly based on the value of the
> glibc.cpu.aarch64_gcs tunable.
Maybe add some descriptions of the added test (what they test
and why).
The tests look good, some comments below.
> ---
> sysdeps/unix/sysv/linux/aarch64/Makefile | 23 +++
> .../unix/sysv/linux/aarch64/tst-gcs-execv.c | 60 ++++++++
> .../unix/sysv/linux/aarch64/tst-gcs-fork.c | 79 +++++++++++
> .../sysv/linux/aarch64/tst-gcs-lock-ptrace.c | 133 ++++++++++++++++++
> .../sysv/linux/aarch64/tst-gcs-lock-static.c | 1 +
> .../unix/sysv/linux/aarch64/tst-gcs-lock.c | 63 +++++++++
> .../linux/aarch64/tst-gcs-unlock-static.c | 2 +
> .../unix/sysv/linux/aarch64/tst-gcs-unlock.c | 2 +
> 8 files changed, 363 insertions(+)
> create mode 100644 sysdeps/unix/sysv/linux/aarch64/tst-gcs-execv.c
> create mode 100644 sysdeps/unix/sysv/linux/aarch64/tst-gcs-fork.c
> create mode 100644 sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock-ptrace.c
> create mode 100644 sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock-static.c
> create mode 100644 sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock.c
> create mode 100644 sysdeps/unix/sysv/linux/aarch64/tst-gcs-unlock-static.c
> create mode 100644 sysdeps/unix/sysv/linux/aarch64/tst-gcs-unlock.c
>
> diff --git a/sysdeps/unix/sysv/linux/aarch64/Makefile b/sysdeps/unix/sysv/linux/aarch64/Makefile
> index 2b8401f42e..7ab15cb8b8 100644
> --- a/sysdeps/unix/sysv/linux/aarch64/Makefile
> +++ b/sysdeps/unix/sysv/linux/aarch64/Makefile
> @@ -151,10 +151,14 @@ gcs-tests-dynamic = \
> tst-gcs-dlopen-override \
> tst-gcs-enforced \
> tst-gcs-enforced-abort \
> + tst-gcs-execv \
> + tst-gcs-fork \
> tst-gcs-ld-debug-both \
> tst-gcs-ld-debug-dlopen \
> tst-gcs-ld-debug-exe \
> tst-gcs-ld-debug-shared \
> + tst-gcs-lock \
> + tst-gcs-lock-ptrace \
> tst-gcs-noreturn \
> tst-gcs-optional-off \
> tst-gcs-optional-on \
> @@ -167,15 +171,18 @@ gcs-tests-dynamic = \
> tst-gcs-shared-enforced-abort \
> tst-gcs-shared-optional \
> tst-gcs-shared-override \
> + tst-gcs-unlock \
> # gcs-tests-dynamic
>
> gcs-tests-static = \
> tst-gcs-disabled-static \
> tst-gcs-enforced-static \
> tst-gcs-enforced-static-abort \
> + tst-gcs-lock-static \
> tst-gcs-optional-static-off \
> tst-gcs-optional-static-on \
> tst-gcs-override-static \
> + tst-gcs-unlock-static \
> # gcs-tests-static
>
> tests += \
> @@ -225,6 +232,22 @@ tst-gcs-optional-static-on-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2
> tst-gcs-optional-static-off-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2
> tst-gcs-override-static-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=3
>
> +LDFLAGS-tst-gcs-execv += -Wl,-z,gcs=always
> +tst-gcs-execv-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1
> +LDFLAGS-tst-gcs-fork += -Wl,-z,gcs=always
> +tst-gcs-fork-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2
> +
> +LDFLAGS-tst-gcs-lock += -Wl,-z,gcs=always
> +tst-gcs-lock-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1
> +LDFLAGS-tst-gcs-lock-ptrace += -Wl,-z,gcs=always
> +tst-gcs-lock-ptrace-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1
> +LDFLAGS-tst-gcs-lock-static += -Wl,-z,gcs=always
> +tst-gcs-lock-static-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=1
> +LDFLAGS-tst-gcs-unlock += -Wl,-z,gcs=always
> +tst-gcs-unlock-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2
> +LDFLAGS-tst-gcs-unlock-static += -Wl,-z,gcs=always
> +tst-gcs-unlock-static-ENV = GLIBC_TUNABLES=glibc.cpu.aarch64_gcs=2
> +
> # force one of the dependencies to be unmarked
> LDFLAGS-tst-gcs-mod2.so += -Wl,-z,gcs=never
>
> diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-execv.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-execv.c
> new file mode 100644
> index 0000000000..25a9f9ad75
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-execv.c
> @@ -0,0 +1,60 @@
> +/* AArch64 test for GCS for creating child process.
> + Copyright (C) 2026 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#include "tst-gcs-helper.h"
> +
> +#include <sys/prctl.h>
> +#include <unistd.h>
> +#include <errno.h>
> +
> +#ifndef PR_SET_SHADOW_STACK_STATUS
> +# define PR_SET_SHADOW_STACK_STATUS 75
> +#endif
Maybe move the PR_SET_SHADOW_STACK_* definition to tst-gcs-helper.h
(they are replicate in multiple files).
> +
> +static int
> +do_test_argv (int argc, char **argv)
> +{
> + /* Check if GCS could possible by enabled. */
> + if (!(getauxval (AT_HWCAP) & HWCAP_GCS))
> + FAIL_UNSUPPORTED ("kernel or CPU does not support GCS");
Is checking HWCAP_GCS suffice to avoid false positive while checking GCS locking?
I recall that there was some execve issue fixed only on recent kernels, but I
don't recall whether this gates HWCAP_GCS advertise by the kernel.
I did not see any issue with 6.19 on qemu though.
> +
> + /* GCS should be enabled for this test at the start. */
> + TEST_VERIFY (__check_gcs_status ());
> +
> + if (argc == 1)
> + {
> + printf ("in parent: %u\n", getpid ());
> + /* Spawn child process. */
> + char *args[] = { argv[0], (char *)"child", NULL };
> + execv (args[0], args);
This fails without --enable-hardcoded-path-in-tests, usually the tests that
required re-spawning themselves add an extra argument (though
TEST-ARGS = -- $(host-test-program-cmd) make rule) and handle the arguments
resconstruction (like tst-spawn*.c ones).
You can also use support_spawn_wrap instead, it calls posix_spawn that will
call execve in the end.
> + FAIL_EXIT1 ("execv: %m");
> + }
> +
> + /* In child. */
> + printf ("in child: %u\n", getpid ());
> + TEST_VERIFY (__check_gcs_status ());
> +
> + /* Try disabling GCS (should fail with EBUSY). */
> + int res = prctl (PR_SET_SHADOW_STACK_STATUS, 0, 0, 0, 0);
> + TEST_COMPARE (res, -1);
> + TEST_COMPARE (errno, EBUSY);
> + return 0;
> +}
> +
> +#define TEST_FUNCTION_ARGV do_test_argv
> +#include <support/test-driver.c>
> diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-fork.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-fork.c
> new file mode 100644
> index 0000000000..76d09a538e
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-fork.c
> @@ -0,0 +1,79 @@
> +/* AArch64 test for GCS for creating child process using fork.
> + Copyright (C) 2026 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#include "tst-gcs-helper.h"
> +
> +#include <sys/ptrace.h>
> +#include <sys/prctl.h>
> +#include <sys/wait.h>
> +#include <sys/uio.h>
> +#include <unistd.h>
> +#include <errno.h>
> +
> +#ifndef PR_SET_SHADOW_STACK_STATUS
> +# define PR_GET_SHADOW_STACK_STATUS 74
> +# define PR_SET_SHADOW_STACK_STATUS 75
> +#endif
> +
> +static int
> +do_test (void)
> +{
> + /* Check if GCS could possible by enabled. */
> + if (!(getauxval (AT_HWCAP) & HWCAP_GCS))
> + FAIL_UNSUPPORTED ("kernel or CPU does not support GCS");
> +
> + /* GCS should be enabled for this test at the start. */
> + TEST_VERIFY (__check_gcs_status ());
> +
> + pid_t pid = fork ();
Use xfork here.
> + const char *name;
> + if (pid == 0)
> + name = "child";
> + else
> + name = "parent";
> +
> + /* Both parent and child should initially have GCS enabled. */
> + TEST_VERIFY (__check_gcs_status ());
> + uint64_t data;
> + if (prctl (PR_GET_SHADOW_STACK_STATUS, &data, 0, 0, 0))
> + FAIL_EXIT1 ("prctl: %m");
> + printf ("in %s: gcs status: %016lx\n", name, data);
> +
> + if (pid)
> + {
> + int status;
> + waitpid(pid, &status, 0);
Use xwaitpid here.
> + printf ("in %s: child exited with code %u\n", name, WEXITSTATUS(status));
> + }
> + else
> + {
> + /* Try disabling GCS for the child
> + (should succeed because of the tunable). */
> + if (prctl (PR_SET_SHADOW_STACK_STATUS, 0, 0, 0, 0))
> + FAIL_EXIT1 ("prctl: %m");
> + /* GCS should be disabled. */
> + TEST_VERIFY (!__check_gcs_status ());
> + if (prctl (PR_GET_SHADOW_STACK_STATUS, &data, 0, 0, 0))
> + FAIL_EXIT1 ("prctl: %m");
> + printf ("in %s: gcs status: %016lx\n", name, data);
> + }
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock-ptrace.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock-ptrace.c
> new file mode 100644
> index 0000000000..9559d0f613
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock-ptrace.c
> @@ -0,0 +1,133 @@
> +/* AArch64 test for GCS for creating child process using fork
> + with ptrace to check locked GCS operations.
> + Copyright (C) 2026 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#include "tst-gcs-helper.h"
> +
> +#include <sys/ptrace.h>
> +#include <sys/prctl.h>
> +#include <sys/wait.h>
> +#include <sys/uio.h>
> +#include <unistd.h>
> +#include <string.h>
> +#include <errno.h>
> +
> +#ifndef PR_SET_SHADOW_STACK_STATUS
> +# define PR_SET_SHADOW_STACK_STATUS 75
> +#endif
> +
> +#ifndef NT_ARM_GCS
> +#define NT_ARM_GCS 0x410
> +#endif
This should be always define by elf.h.
> +
> +/* Uapi struct for PTRACE_GETREGSET with NT_ARM_GCS. */
> +struct user_gcs
> +{
> + uint64_t enabled;
> + uint64_t locked;
> + uint64_t gcspr_el0;
> +};
Maybe move these to tst-gcs-helper.h?
> +
> +static int
> +target (void)
> +{
> + /* This signal is raised after the process has started
> + and has been initialised so we can ptrace it at this
> + point and obtain GCS locked features. */
> + raise (SIGUSR1);
Use xraise.
> + return 0;
> +}
> +
> +static void
> +fork_target (char *argv[], uint64_t aarch64_gcs)
> +{
> + /* Currently kernel returns only lower 32 bits of locked
> + features so we only compare them. */
> + bool lock_gcs = aarch64_gcs != 0 && aarch64_gcs != 2;
> + uint64_t expected_locked = lock_gcs ? 0xfffffffful : 0ul;
> + pid_t pid = fork ();
Use xfork here.
> + if (pid == 0)
> + {
> + char *args[] = { argv[0], (char *)"target", NULL };
> + char tunables[90];
> + snprintf (tunables, sizeof (tunables), "GLIBC_TUNABLES="
> + "glibc.cpu.aarch64_gcs=0x%016lx", aarch64_gcs);
> + char *envp[] = { tunables, NULL };
> + /* We need to ptrace child process to use PTRACE_GETREGSET
> + with NT_ARM_GCS after it has started. */
> + ptrace (PTRACE_TRACEME, 0, NULL, NULL);
I think we should also check for the ptrace return code.
> + execve (args[0], args, envp);
Same as the tst-gcs-execv, this fails without --enable-hardcoded-path-in-tests
(which is the dafault).
> + FAIL_EXIT1 ("execve: %m");
> + }
> + while (true)
> + {
> + int status;
> + waitpid (pid, &status, 0);
Use xwaitpid.
> + if (WIFSTOPPED (status))
> + {
> + /* Child stopped by signal. */
> + int sig = WSTOPSIG (status);
> + if (sig == SIGUSR1)
> + {
> + struct user_gcs ugcs = {};
> + struct iovec io;
> + io.iov_base = &ugcs;
> + io.iov_len = sizeof (struct user_gcs);
> + if (ptrace (PTRACE_GETREGSET, pid, NT_ARM_GCS, &io))
> + FAIL_EXIT1 ("ptrace (PTRACE_GETREGSET): %m");
> + printf ("expected vs locked: %016lx %016lx\n",
> + expected_locked, ugcs.locked);
> + if (lock_gcs)
> + TEST_VERIFY_EXIT (ugcs.enabled);
> + TEST_VERIFY_EXIT (ugcs.locked == expected_locked);
> + if (aarch64_gcs != 0)
> + TEST_VERIFY_EXIT ((void *) ugcs.gcspr_el0 != NULL);
> + }
> + }
> + else if (WIFSIGNALED (status))
> + {
> + /* Child terminated by signal. */
> + break;
> + }
> + else if (WIFEXITED (status))
> + {
> + /* Child terminated by normally. */
> + break;
> + }
> + ptrace (PTRACE_CONT, pid, 0, 0);
> + }
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + /* Check if GCS could possible by enabled. */
> + if (!(getauxval (AT_HWCAP) & HWCAP_GCS))
> + FAIL_UNSUPPORTED ("kernel or CPU does not support GCS");
> +
> + /* GCS should be enabled for this test. */
> + TEST_VERIFY (__check_gcs_status ());
> +
> + if (argc == 1)
> + {
> + /* Check all 4 values for the aarch64_gcs tunable. */
> + for (uint64_t aarch64_gcs = 0; aarch64_gcs < 4; aarch64_gcs++)
> + fork_target (argv, aarch64_gcs);
> + return 0;
> + }
> + return target ();
> +}
> diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock-static.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock-static.c
> new file mode 100644
> index 0000000000..b80e2f70e8
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock-static.c
> @@ -0,0 +1 @@
> +#include "tst-gcs-lock.c"
> diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock.c
> new file mode 100644
> index 0000000000..df9692aeae
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-lock.c
> @@ -0,0 +1,63 @@
> +/* AArch64 test for GCS locking.
> + Copyright (C) 2026 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#include "tst-gcs-helper.h"
> +
> +#include <linux/prctl.h>
> +#include <sys/prctl.h>
> +#include <errno.h>
> +
> +#ifndef PR_SET_SHADOW_STACK_STATUS
> +# define PR_SET_SHADOW_STACK_STATUS 75
> +#endif
> +
> +static int
> +do_test (void)
> +{
> + /* Check if GCS could possible by enabled. */
> + if (!(getauxval (AT_HWCAP) & HWCAP_GCS))
> + FAIL_UNSUPPORTED ("kernel or CPU does not support GCS");
> +
> + TEST_VERIFY (__check_gcs_status ());
> +
> + /* Try disabling GCS. */
> + int res = prctl (PR_SET_SHADOW_STACK_STATUS, 0, 0, 0, 0);
> + if (res)
> + {
> + TEST_COMPARE (errno, EBUSY);
> + perror ("prctl");
I think printing an error for the success case is confusing (at least it confuses
me while testing, seeing a prctl printing the errno for a success testcase).
> +#ifdef GCS_SHOULD_UNLOCK
> + FAIL_EXIT1 ("GCS was not unlocked (was supposed to): %m");
> +#else
> + TEST_VERIFY (__check_gcs_status ());
> +#endif
> + }
> + else
> + {
> +#ifdef GCS_SHOULD_UNLOCK
> + TEST_VERIFY (!__check_gcs_status ());
> + puts ("GCS unlocked successfully");
> +#else
> + FAIL_EXIT1 ("GCS was unlocked (was not supposed to)");
> +#endif
> + }
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-unlock-static.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-unlock-static.c
> new file mode 100644
> index 0000000000..7e02820031
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-unlock-static.c
> @@ -0,0 +1,2 @@
> +#define GCS_SHOULD_UNLOCK
> +#include "tst-gcs-lock.c"
> diff --git a/sysdeps/unix/sysv/linux/aarch64/tst-gcs-unlock.c b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-unlock.c
> new file mode 100644
> index 0000000000..7e02820031
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/aarch64/tst-gcs-unlock.c
> @@ -0,0 +1,2 @@
> +#define GCS_SHOULD_UNLOCK
> +#include "tst-gcs-lock.c"
More information about the Libc-alpha
mailing list