[PATCH v8 00/20] aarch64: Add support for Guarded Control Stack extension

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Thu Jan 16 16:51:49 GMT 2025



On 16/01/25 13:27, Yury Khrustalev wrote:
> On Thu, Jan 16, 2025 at 12:11:02PM -0300, Adhemerval Zanella Netto wrote:
>>>>
>>>> In fact, the manual sentence:
>>>>
>>>>   GCS policy @code{1} means that GCS is enabled if @code{glibc.cpu.aarch64_gcs}
>>>>   is set and all binaries are GCS-marked. If GCS is required and an incompatible
>>>>   library is loaded via @code{dlopen}, it is an error, otherwise any
>>>>   incompatible binary will disable GCS. 
>>>>
>>>> should be improve to state that what is disabled is the GCS *check* for future
>>>> dlopen, not GCS itself.
>>>
>>> Sorry, this is not correct. What is disabled is GCS itself (making is the same as
>>> when tunable wasn't set from the start).
>>
>> Hum, how exactly GCS is disabled once is enabled?
> 
> It's not. We **keep** it disabled (as in we don't do prctl() call).
> 
>> Afaiu aarch64_gcs=1 enables
>> it with PR_SHADOW_STACK_ENABLE on _start (assuming dynamic case), and there
>> is no other PR_SET_SHADOW_STACK_STATUS command after (so GCS is still enabled
>> in newly created threads). Kernel documentation is not explicit clear on
>> how to disable GCS by the thread, but I assume it through PR_SET_SHADOW_STACK_STATUS
>> with argument 0. 
> 
> Assuming the system supports GCS. When process is started, GCS is not enabled. Before
> we can call prctl() from _start() we need to do a few things. If you look at the code,
> before checking whether we need to call prctl() at all, we call _dl_start() that calls
> 
>   _dl_start_final() -> dl_main() -> _rtld_main_check()
> 
> The latter is doing checks of markings in _dl_gcs_check(). The result of these checks
> will be the desired GCS state: enabled, disabled, error (to be precise, error is not
> really a state, but it's one of the three outcomes of the logic behind _dl_gcs_check()).
> 
> This desired state then determines whether we call prctl() or not. Of course, if we
> got an error, we abort before we even come to this point.
> 
> We need to do all the stuff above before the actuall prctl() syscall because we need:
> 
>  1. To know the values of tunables
>  2. To check markings for which the GNU properties of them should be loaded
> 
> Now in the _dl_gcs_check() we first check if aarch64_gcs is set, if not we do nothing
> and the desired GCS state remains 0 (== "don't enable it"). If it is set, then the
> aarch64_gcs_policy tunable comes into play and allows these situations:
> 
>   * policy 0: enable GCS regardless of marking
> 
>     Here we don't do checks of markings at all, this case may be useful for experiments
>     or, for example, on systems where we know all binaries are built to support GCS in
>     which case why would we spend time checking markings? Note that here GCS will be
>     enabled, and if we attempt to load any binary (with or without dlopen()) we will
>     still run with GCS enabled.
> 
>   * policy 1: run application correctly, with or without GCS depending on marking
> 
>     Here we want to support gradualte deployment of applications and libraries.
>     We check markings in this case. It is not an error if **during startup** we
>     find that some dependency does not have the marking. In this case we will not
>     enable GCS later in _start() and we set the desired GCS state to 0 (even
>     though aarch64_gcs tunable was originally set). The remaining execution will
>     proceed as if aarch64_gcs was 0 from the start. This means that GCS will not
>     be enabled via prctl() and that later when we try dlopen() we don't actually
>     need to check any markings since GCS is not supported for this application
>     (but we allow it because we don't want to disrupt this application).
> 
>     If however **during startup** all dependencies were marked, we will enable GCS.
>     In this case subsequent attempt to dlopen() a binary without marking will be
>     an error. Not that dlopen() does not occur during startup.

Right, it is clear now and sorry for the confusion. Maybe expanding the tunables.texi
with this very explanation, specially for aarch64_gcs_policy=1.

> 
>   * policy 2: run application only if it supports GCS, otherwise abort
> 
>     Here we want to enforce use of GCS. If **during startup** we find any
>     unmarked binaries, we abort. Otherwise, this is the same as policy 1.
> > All four cases seem important and we'd like to keep them all.

> 
>>
>> Am I missing something here?
>>
>>>
>>>> And still think this semantic is far from ideal.
>>>
>>> OK, this may be open to debate, but it would be better if this problem was
>>> brought up earlier...
>>>
>>>>
>>>> In fact, two tunables for GCS support is *really* confusing.  For instance 
>>>> aarch64_gcs_policy=0,aarch64_gcs=1 does not make sense (since aarch64_gcs is
>>>> ignored if aarch64_gcs_policy is not set).
>>>
>>> Sorry, this is wrong. aarch64_gcs is never inored. It's the other way round:
>>> aarch64_gcs_policy is irrelevant when aarch64_gcs is not set.
>>
>> Right, my mistake here; I meant the other way around indeed.
>>
>>>
>>>> Assuming GCS is never disabled once enabled (which seems the intention of this
>>>> initial patchset), I think we can simplify to just one tunable:
>>>>
>>>>   aarch64_gcs=0  -  GCS is disabled and any GCS-marking is ignored.  The default.
>>>>   aarch64_gcs=1  -  GCS is enabled, and taken as a hint at loading time.  If
>>>>                     any binary is not GCS-marked, or if kernel does not
>>>>                     support it, GCS is not enabled. 
>>>>                     dlopen enforces GCS if it was enabled at loading time,
>>>
>>> I don't understand what you mean by "dlopen enforces GCS".
>>
>> If GCS is enabled at _start, because the binary and its dependencies are
>> GCS-marked; dlopen will fail if trying to load a GCS-mark shared library.
> 
> Propagating some return value from _dl_open_check() to dlopen() is not possible
> right now and would require changes to dlopen(). And even we do this, then
> applications would have to take some action on dlopen() error anyway (most likely
> abort), so it was easier just to abort early.

Fair enough.

> 
>> If GCS was not enabled, dlopen will ignore the GCS-marking.
>>
>>>
>>>>                     otherwise GCS-marking is ignored.
>>>>   aarch64_gcs=2  -  GCS is enabled, and it is *enforced*.  If any binary is not
>>>>                     GCS-marked, or if kernel does not support, loading fails.
>>>>                     dlopen is always enforces GCS.
>>>>
>>>> It means that we can eventually set aarch64_gcs=1 as default in a future release,
>>>> and aarch64_gcs=2 might be an option for a hardened distro (or even in some future
>>>> release).
>>>>
>>>
>>> So, right now what we have is 4 outcomes:
>>>
>>> If aarch64_gcs is 0
>>>
>>>  a) policy does not matter and GCS is disabled
>>>
>>> If tunable is set, then
>>>
>>>  b) policy 0: enable GCS regardless of marking
>>>  c) policy 1: run application correctly, with or without GCS depending on marking (see note below)
>>>  d) policy 2: run application only if it supports GCS (all binaries are marked), otherwise abort
>>>  
>>> Note: dlopen of unmarked binary is always an error when we care about markings (policies 1 and 2)
>>>
>>> Policy 1 can switch off GCS but never via dlopen.
>>
>> I still think we can just make it only one tunable, since with aarch64_gcs=0
>> policy does not matter (so it can be policy=0).
> 
> Certainly it's option to use one tunable. With two tunables we use one to
> turn GCS on or off and we use another to select desired behaviour if GCS is
> enabled. Now it's a matter of time and testing though. We didn't get any
> negative feedback about two tunables so far.

Do you plan to expand or add additional semantic to aarch64_gcs=0:aarch64_gcs_policy=N?
Because still really confusing to have two tunables, where one is just a switch
to second.




More information about the Libc-alpha mailing list