AArch64: GCS lock implementation options

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Thu Feb 5 13:34:25 GMT 2026



On 03/02/26 11:29, Yury Khrustalev wrote:
> On Mon, Feb 02, 2026 at 04:21:12PM -0300, Adhemerval Zanella Netto wrote:
>>
>> On 26/01/26 08:35, Yury Khrustalev wrote:
>>> Hi all,
>>>
>>> A request for feedback about proposed solutions for this problem.
>>>
>>> We need a way to implement GCS (shadow stack) [1] locking for cases
>>> when GCS is enabled via the glibc.cpu.aarch64_gcs tunable.
>>>
>>> ...
>>>
>>
>> I am not sure if the debuggers fit in a usercase scenario where GCS locking 
>> will interfere, since from Documentation/arch/arm64/gcs.rst:
>>
>> * Configuration via ptrace ignores locking of GCS mode bits.
> 
> I should've been more generic and said "binary instrumentation tools".
> Yes, you can manage a process via ptrace, but some tools (for example,
> DynamoRIO [1]) prefer LD_PRELOAD approach. Either way, what might be
> of interest is the ability of the target process to do certain things
> by itself rather than managing it externally. But this all is quite off
> topic for this thread.

The issue is these are two requisites essentially clash together: the ability 
to *enforce* an *opt-in* security hardening through a binary invariant
(assuming the system does support it) and the ability to *opt-out* such
hardening when required.

> 
>> The cases I can think of that might be impacted by GCS state locking are:
>>
>> 1. Legacy Library Interoperability opened with dlopen that might perform unusual
>>    stack operations (like custom longjmp implementation, exception handling, or
>>    manual stack pivoting that does not update the GCSPR_EL0 register).
>>
>> 2. *In-Process* Debugging / Crash Reporting, where if the stack is corrupted, the 
>>    crash reporter might attempt to manually patch the stack or the GCS to allow
>>    for a clean unwind to capture a stack trace.
>>
>> 3. Checkpoint/Restore (CRIU), where it tries to restore a memory layout by using 
>>    PR_SHADOW_STACK_WRITE.
>>
>> 4. User-space threads like Goroutines or code like boost::context where the 
>>    implementations have their own 'green threads' or fibers by manually swapping 
>>    stack pointers along with PR_SHADOW_STACK_WRITE.
>>
>> 5. JIT and Dynamic Code Gen, where the JIT might generate trampolines or stubs 
>>    that do not strictly follow the standard AAPCS wrt to pushing/popping rules 
>>    and paired BL/RET.
> 
> Yes, these are all good examples, but it is not the question really
> whether we need to allow to opt-out from locking GCS operations. The
> question is how to implement it.
> 
>> I don't think 1. also fits as a user case, legacy libraries were by definition not 
>> implemented with GCS in mind, and we already have glibc.cpu.aarch64_gcs={0,2}
>> to overcome it if required (albeit GCS will be disabled anyway).
> 
> So, again, you miss the point that we may want to have GCS enabled for
> the (most of the) process / application even if we have to disable it
> for some parts. The option when we don't use GCS at all is really a last
> resort that is very undesirable.

Although kernel have a per-thread GCS support, I think exposing it through the
*C runtime* is a security liability.  It similar to how we implement setuid, where
Linux does have the concept of different uid/gid per thread but we explicit sync
all threads under the hood.  I still think that exposing a way to programmatically 
change it is a mistake [1] (even though some programs requires it).

This is the same for GCS, if caller want a process without GCS locking it just does
not *opt-in* to it and do all its shenanigans. 

[1] https://sourceware.org/pipermail/libc-alpha/2025-December/173360.html

> 
>> The 2. is also debatable, since if a *in-process* debugging / crash reporting is
>> requiring to change the stack layout for some reason either the process is already 
>> in a undefined state or it does some no really support for GCS.
> 
> IMO, what is debatable is the technicalities of how to implement this.
> The use case itself is very valid.
> 
>> For 3., I don’t think it is an issue because CRIU can use map_shadow_stack instead 
>> of SHADOW_STACK_SET_TOKEN. I am not entirely sure, but I think it would also require 
>> the startup process not to be built with GCS support (CRIU will set it up itself 
>> after the process setup).
>>
>> The 4. I would expect that at least C++ coroutines and similar constructs would work 
>> seamlessly on GCS without the need for extra setup or runtime support (though I am 
>> not entirely sure), and also work with GCS state locking (otherwise using such 
>> features might disable some security hardening).
>>
>> For 5. I am also not sure how GCS will play out, since they are managed runtimes, 
>> and C or unmanaged code is accessed through a specific API or via managed code 
>> (FFI or something similar). I would expect the runtime to enable and/or lock GCS 
>> state itself when it seems fit, without resorting to the C runtime.
> 
> What do you mean by "without resorting to the C runtime"?

I mean environment where calling C code is mediate through the runtime and
caller does not have direct access to the C runtime internal. Something
like interpreted languages or JNI, where you need a proper wrapper to call
native code and it the runtime might not run the native code in the same
thread.

> 
> The JIT example is actually quite important as it shows that we also
> need to allow to opt-out from locking the "status" operation (i.e.
> enable / disable GCS). JIT implies that code can be written in memory,
> so things like CFI might not have as much value anyway. For this reason,
> JIT engines might never implement any support for GCS.

The question is whether the JIT code will use/require to on in the same
stack context as the C runtime; or if it will create managed stacks (as 
golang does) and handle itself any stack management.

In either case, I would expect for managed runtime (like Java or .NET) 
locking GCS is not strictly required: FFI should be mediated through
the runtime and it has full capabilities to know when and if locking GCS
would make sense.

> 
> One example here is a web browser (or any of the host of Electron-based
> desktop applications [2]) where we have several things at once:
> 
>  1) They do rely on OS's dynamic loader and C library in general.
>  2) They prefer to fork "worker threads" [3] rather than exec them.
>  3) Some "worker threads" use JIT and therefore need GCS=off while
>     others may have JIT=off so they would want to have GCS=on.
> 
> In this case, the base process ("zygote") will fork into workers, and
> some of them will need to be able to disable GCS while others will need
> to keep GCS on.

These also have conflicting security requirements and I would expect that
if you have a system that *requires* threads to have different GCS status
to be heavily sandboxes along with other security mitigations (like WASM).

I consider this cases similar to a managed runtime, where it would require
to not *opt-in* to GCS locking and do the management itself (instead of
relying on the C runtime).

> 
>>> This is why we need a runtime option to allow opt-out of locking of
>>> certain operations (except for the 3 operations listed above, these
>>> must be locked unconditionally).
>>>
>>> ...
>>>
>>
>> Although it is not clear why we still need to support a per-process way to disable
>> GCS locking, I think it might be a better way than extending the tunable to accomplish
>> it.
> 
> I agree about not expanding the aarch64_gcs tunable, especially given that we may
> need fine control over each operation (each of 64 of them).
> 
>> The current GCS design sets the ELF attribute only for compatibility, with the 
>> runtime (glibc) responsible for enforcement. The main problem is that tunables
>> were not designed
> 
> There are no other *runtime* mechanism to do that AFAIK.

Yes, and I think GCS locking should *not* be managed by a runtime flag.
In fact, I would like to *move away* from tying glibc GCS support from tunables
and making it a GNU ELF marking instead by mapping the tunable values to
proper GNU attributes.

The glibc tunables is a good approach for debugging/testing/enablement, but it
is *not* a production security strategy. Using environment variables as security
hardening enforcement has multiple issues:

  * AT_SECURE environment filtering, where you setuid binaries will run
    without GCS.  Distros are moving away from setuid, but there are still
    users that rely on it [2].

  * Mutability and Inheritance: where environment variables are part of the 
    process state that is mutable by the user or attacker before execution;
    and can be filtered out and/or removed (thus removing GCS enforcement).

  * Compatibility: it requires you keep an extra 'databases', either by shell
    wrappers, systemd units, etc. to keep track which process is compatible
    or not with GCS.

I do not want to repeat the PT_GNU_STACK mistake, where the glibc changed
the binary invariant under the hoods to keep compatibility (where process
marks that it does not require an executable stack, but during runtime
it loads a shared module that does).

[2] https://sourceware.org/pipermail/libc-alpha/2026-January/174370.html

> 
>> and environment variables are not a good approach for enforcing 
>> security hardening.
> 
> I'd argue that environment variables are OK for enforcing security
> hardening because any other env variable can't be worse for that than
> PATH or any of the LD_* variables.
> 
>> It should be an ELF invariant, not a system deployment 
>> configuration.
> 
> Anything baked in the ELF would not allow to run the *same* binary
> with and without some of the GCS operations being locked.
> 
>> We might revise it if/when we have system-wide tunables, but at the same time, I 
>> think we are complicating things with a different system (GNU ELF flags and glibc 
>> tunables), along with the requirement for experimentation and enablement (a fast way 
>> to enable/disable the security features) over the way to actually deploy the 
>> hardening system-wide. 
>>
>> So, I don’t think we should keep extending the tunables (deployment policy) and 
>> instead move to a binary capability (attribute) to properly enable GCS locking.
>>
>> I would suggest adding a new flag, GNU_PROPERTY_AARCH64_FEATURE_1_GCS_LOCK, which 
>> implies the promise that the application and its plugins shall not violate the GCS 
>> support (GNU_PROPERTY_AARCH64_FEATURE_1_GCS). Along with a new compiler option, 
>> -mbranch-protection=gcs+lock, ld.so scans the main executable (as per current GCS 
>> support) and:
> 
> Again, we need the choice of what to lock to be a *runtime* choice, not
> compile time!

I disagree with this approach, I think the GCS locking should an *opt-in*
feature of the binary.  I think it should up to the system deployment to
enforce/disable GCS locking, similar to systemd 
SystemCallFilter=/MemoryDenyWriteExecute= or AppArmor profiles.

> 
> Once more, my question was about how to implement this quite complex
> tunable in Glibc in a better way. It's not for the discussion whether we
> need to do that or not, we do need it. I'm happy to consider other
> non-tunable approaches, but they have to be runtime rather than compile
> time.
> 
> Thanks,
> Yury
> 
> ---
> 
> [1]: https://dynamorio.org/
> [2]: https://www.electronjs.org/
> [3]: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux/zygote.md
> 



More information about the Libc-help mailing list