[PATCH v3 1/2] aarch64: Lock GCS status at startup
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Thu Feb 12 14:36:45 GMT 2026
On 09/02/26 08:58, Yury Khrustalev wrote:
> If GCS is enabled (see tunable glibc.cpu.aarch64_gcs), we lock all GCS
> operations (including status, write on shadow stack, and push to shadow
> stack) unless OPTIONAL policy is used.
Hi, Yury.
I think this approach is better than adding a tunable to enable/disable
locking. I think we might need to eventually move the glibc.cpu.aarch64_gcs
semantic to a proper GNU attribute, but this is a different discussion.
Some comment below.
> ---
> manual/tunables.texi | 46 ++++++++++++--------
> sysdeps/aarch64/dl-gcs.c | 2 +-
> sysdeps/aarch64/dl-start.S | 18 +++++++-
> sysdeps/unix/sysv/linux/aarch64/libc-start.h | 29 +++++++++---
> 4 files changed, 70 insertions(+), 25 deletions(-)
>
> diff --git a/manual/tunables.texi b/manual/tunables.texi
> index cacc0ea652..b1d880375c 100644
> --- a/manual/tunables.texi
> +++ b/manual/tunables.texi
> @@ -604,13 +604,15 @@ This tunable controls Guarded Control Stack (GCS) for the process.
>
> Accepted values are:
>
> -0 = disabled: do not enable GCS.
> -
> -1 = enforced: check markings and fail if any binary is not marked.
> -
> -2 = optional: check markings but keep GCS off if any binary is unmarked.
> -
> -3 = override: enable GCS, markings are ignored.
> +@itemize @bullet
> +@item @code{0} = disabled: do not enable GCS.
> +@item @code{1} = enforced: check markings and abort if any binary is not
> +marked, otherwise enable GCS and lock all GCS features.
> +@item @code{2} = optional: check markings but keep GCS off if any binary
> +is unmarked, otherwise enable GCS but do not lock any GCS features.
> +@item @code{3} = override: enable GCS and lock all GCS features, markings
> +are ignored.
> +@end itemize
>
> If unmarked binary is loaded via @code{dlopen} when GCS is enabled and
> markings are not ignored (@code{aarch64_gcs == 1} or @code{2}), then
> @@ -621,25 +623,35 @@ Default is @code{0}, so GCS is disabled by default.
> This tunable is specific to AArch64. On systems that do not support
> Guarded Control Stack this tunable has no effect.
>
> +GCS features (or operations on shadow stack) such as @code{STATUS} (i.e.
> +enabling and disabling GCS), @code{WRITE}, and @code{PUSH}, will be locked
> +for the @code{enforced} and @code{override} tunable values.
> +
> Before enabling GCS for the process the value of this tunable is checked
> and depending on it the following outcomes are possible.
>
> -@code{aarch64_gcs == 0}: GCS will not be enabled and GCS markings will not be
> +@itemize @bullet
> +@item
> +@code{aarch64_gcs == 0}: GCS will remain disabled and GCS markings will not be
> checked for any binaries.
> -
> +@item
> @code{aarch64_gcs == 1}: GCS markings will be checked for all binaries loaded
> -at startup and, only if all binaries are GCS-marked, GCS will be enabled. If
> -any of the binaries are not GCS-marked, the process will abort. Subsequent call
> -to @code{dlopen} for an unmarked binary will also result in abort.
> -
> +at startup and, only if all binaries are GCS-marked, GCS will be enabled and
> +all GCS features will be locked. If any of the binaries are not GCS-marked,
> +the process will abort. Subsequent call to @code{dlopen} for an unmarked binary
> +will also result in abort.
I think 'abort' is not the correct description for dlopen, it might confusing
with the previous description of process abort. Maybe state that dlopen will
not load a module without GCS markings in such case.
> +@item
> @code{aarch64_gcs == 2}: GCS markings will be checked for all binaries loaded
> at startup and, if any of such binaries are not GCS-marked, GCS will not be
> enabled and there will be no more checks for GCS marking. If all binaries
> -loaded at startup are GCS-marked, then GCS will be enabled, in which case a
> -call to @code{dlopen} for an unmarked binary will also result in abort.
> +loaded at startup are GCS-marked, then GCS will be enabled but GCS features
> +will not be locked. In this case a call to @code{dlopen} for an unmarked binary
> +will also result in abort.
> +@item
> +@code{aarch64_gcs == 3}: GCS will be enabled and all GCS features will be
> +locked. GCS markings will not be checked for any binaries.
> +@end itemize
>
> -@code{aarch64_gcs == 3}: GCS will be enabled and GCS markings will not be
> -checked for any binaries.
> @end deftp
>
> @node Memory Related Tunables
> diff --git a/sysdeps/aarch64/dl-gcs.c b/sysdeps/aarch64/dl-gcs.c
> index e1d1db4852..563d1ae0c3 100644
> --- a/sysdeps/aarch64/dl-gcs.c
> +++ b/sysdeps/aarch64/dl-gcs.c
> @@ -145,5 +145,5 @@ _dl_gcs_check (struct link_map *l, const char *program, int dlopen_mode)
> /* Used to report error when prctl system call to enabled GCS fails. */
> void _dl_gcs_enable_failed (int code)
> {
> - _dl_fatal_printf ("failed to enable GCS: %d\n", -code);
> + _dl_fatal_printf ("failed to enable or lock GCS: %d\n", -code);
> }
> diff --git a/sysdeps/aarch64/dl-start.S b/sysdeps/aarch64/dl-start.S
> index 3b5ff2cccb..71b805cdaa 100644
> --- a/sysdeps/aarch64/dl-start.S
> +++ b/sysdeps/aarch64/dl-start.S
> @@ -35,12 +35,13 @@ ENTRY (_start)
> /* Use GL(dl_aarch64_gcs) to set the shadow stack status. */
> adrp x16, _rtld_local
> add x16, x16, :lo12:_rtld_local
> - ldr x1, [x16, GL_DL_AARCH64_GCS_OFFSET]
> - cbz x1, L(skip_gcs_enable)
> + ldr x22, [x16, GL_DL_AARCH64_GCS_OFFSET]
> + cbz x22, L(skip_gcs_enable)
>
> /* Enable GCS before user code runs. Note that IFUNC resolvers and
> LD_AUDIT hooks may run before, but should not create threads. */
> #define PR_SET_SHADOW_STACK_STATUS 75
> +#define PR_LOCK_SHADOW_STACK_STATUS 76
> #define PR_SHADOW_STACK_ENABLE (1UL << 0)
> mov x0, PR_SET_SHADOW_STACK_STATUS
> mov x1, PR_SHADOW_STACK_ENABLE
> @@ -50,6 +51,19 @@ ENTRY (_start)
> mov x8, #SYS_ify(prctl)
> svc 0x0
> cbnz w0, L(failed_gcs_enable)
> + /* Check if we need to lock GCS features. */
> + /* If the aarch64_gcs tunable is either 0 or 2 do not lock GCS. */
> + tst x22, #-3
> + beq L(skip_gcs_enable)
> + mov x0, PR_LOCK_SHADOW_STACK_STATUS
> + /* Lock everything including future operations. */
> + mov x1, ~0
> + mov x2, 0
> + mov x3, 0
> + mov x4, 0
> + mov x8, #SYS_ify(prctl)
> + svc 0x0
> + cbnz w0, L(failed_gcs_enable)
> L(skip_gcs_enable):
>
> .globl _dl_start_user
These will issue 2 prctl syscalls, where if locking is required it can just
issue one with PR_SHADOW_STACK_ENABLE | PR_LOCK_SHADOW_STACK_STATUS.
Also, I think maybe we should make _dl_gcs_enable_failed print the command
that was issue instead of a generic message for both enablement and locking.
> diff --git a/sysdeps/unix/sysv/linux/aarch64/libc-start.h b/sysdeps/unix/sysv/linux/aarch64/libc-start.h
> index 9eecc557fd..24eb5ce967 100644
> --- a/sysdeps/unix/sysv/linux/aarch64/libc-start.h
> +++ b/sysdeps/unix/sysv/linux/aarch64/libc-start.h
> @@ -25,9 +25,17 @@
>
> # ifndef PR_SET_SHADOW_STACK_STATUS
> # define PR_SET_SHADOW_STACK_STATUS 75
> +# define PR_LOCK_SHADOW_STACK_STATUS 76
> # define PR_SHADOW_STACK_ENABLE (1UL << 0)
> # endif
>
> +# ifndef GCS_POLICY_DISABLED
> +/* GCS is disabled. */
> +# define GCS_POLICY_DISABLED 0
> +/* Optionally enable GCS if all startup dependencies are marked. */
> +# define GCS_POLICY_OPTIONAL 2
> +# endif
> +
These are define by glibc, instead of importing from kernel. I think
it should define in a common header instead of redefine them on both
sysdeps/aarch64/dl-gcs.c and sysdeps/unix/sysv/linux/aarch64/libc-start.h
> /* Must be on a top-level stack frame that does not return. */
> static inline void __attribute__((always_inline))
> aarch64_libc_setup_tls (void)
> @@ -46,12 +54,23 @@ aarch64_libc_setup_tls (void)
>
> _rtld_main_check (main_map, _dl_argv[0]);
>
> - if (GL(dl_aarch64_gcs) != 0)
> + uint64_t gcs = GL (dl_aarch64_gcs);
> + if (gcs != GCS_POLICY_DISABLED)
> {
> - int ret = INLINE_SYSCALL_CALL (prctl, PR_SET_SHADOW_STACK_STATUS,
> - PR_SHADOW_STACK_ENABLE, 0, 0, 0);
> - if (ret)
> - _dl_fatal_printf ("failed to enable GCS: %d\n", -ret);
> + int r0 = INLINE_SYSCALL_CALL (prctl, PR_SET_SHADOW_STACK_STATUS,
> + PR_SHADOW_STACK_ENABLE, 0, 0, 0);
> + if (r0)
> + _dl_fatal_printf ("failed to enable GCS: %d\n", -r0);
> +
> + /* Do not lock GCS features if policy is OPTIONAL. */
> + if (gcs != GCS_POLICY_OPTIONAL)
> + {
> + /* Lock all bits, including future bits. */
> + int r1 = INLINE_SYSCALL_CALL (prctl, PR_LOCK_SHADOW_STACK_STATUS,
> + ~0, 0, 0, 0);
> + if (r1)
> + _dl_fatal_printf ("failed to lock GCS: %d\n", -r1);
> + }
> }
Same as before, it is extra not strictly required syscall.
> }
>
More information about the Libc-alpha
mailing list