[PATCH v3 2/2] aarch64: Tests for locking GCS

Yury Khrustalev yury.khrustalev@arm.com
Mon Feb 9 11:58:45 GMT 2026


Check that GCS is locked properly based on the value of the
glibc.cpu.aarch64_gcs tunable.
---
 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
+
+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");
+
+  /* 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);
+      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 ();
+  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);
+      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
+
+/* Uapi struct for PTRACE_GETREGSET with NT_ARM_GCS.  */
+struct user_gcs
+{
+  uint64_t enabled;
+  uint64_t locked;
+  uint64_t gcspr_el0;
+};
+
+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);
+  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 ();
+  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);
+      execve (args[0], args, envp);
+      FAIL_EXIT1 ("execve: %m");
+    }
+  while (true)
+    {
+      int status;
+      waitpid (pid, &status, 0);
+      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");
+#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"
-- 
2.47.3



More information about the Libc-alpha mailing list