[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