[PATCH v8 00/20] aarch64: Add support for Guarded Control Stack extension
Yury Khrustalev
yury.khrustalev@arm.com
Thu Jan 16 16:27:43 GMT 2025
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.
* 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.
> 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.
> Also, policy=0 seems a debugging/enablement feature and once the program is
> certified to work, you can just run it with policy 2. The policy 1 is still
> not really useful IMHO, since it means that enablement with policy=0 did not
> catch potential issues with GCS (like a configuration where a dlopen is
> not triggered).
>
> >
> > I hope this helps,
> > Yury
> >
>
More information about the Libc-alpha
mailing list