[PATCH v4 2/5] nptl: Add libc allocated shadow stack for new threads
Yury Khrustalev
yury.khrustalev@arm.com
Wed Nov 26 13:41:18 GMT 2025
On Wed, Nov 26, 2025 at 10:28:29AM -0300, Adhemerval Zanella Netto wrote:
>
> On 07/11/25 06:35, Yury Khrustalev wrote:
> > Includes aarch64 GCS support code, does not include x86 shstk
> > support (should be similar to the aarch64 case).
> >
> > Uses extended clone3 interface that allows to pass pointer to
> > architecture-defined token on shadow stack.
> >
> > We allocate shadow stack before we pass it to the clone3 syscall
> > at which point it may fail with E2BIG if the kernel doesn't support
> > new field in struct clone_args (but it still may have support for
> > AArch64's HWCAP_GCS). In this case we would need to de-allocate
> > shadow stack and repeat clone3 syscall without it. Due to complexities
> > of create_thread() and also because we can't know which field was
> > actually a problem when we receive E2BIG error from the clone3 syscall,
> > it is better to check if kernel supports shadow stack in clone_args
> > before we even allocate it.
> >
> > We do this by making a dummy clone3 syscall which is going to fail
> > (so no new task will actually start) but it can fail in one of two
> > possible ways that will tell us whether the shadow_stack_token field
> > is supported by the kernel or not. We then cache the result to avoid
> > doing checks again.
> >
> > We can use relaxed atomics because even if two concurrent threads
> > run the check function, they are guaranteed to get the same result.
> > This even is extremely unlikely, however.
> >
> > Co-authored-by: Szabolcs Nagy <szabolcs.nagy@arm.com>
> > ---
> > nptl/descr.h | 7 ++
> > nptl/pthread_create.c | 29 +++++++++
> > sysdeps/aarch64/libc-shadow-stack.h | 99 +++++++++++++++++++++++++++++
> > sysdeps/generic/libc-shadow-stack.h | 25 ++++++++
> > 4 files changed, 160 insertions(+)
> > create mode 100644 sysdeps/aarch64/libc-shadow-stack.h
> > create mode 100644 sysdeps/generic/libc-shadow-stack.h
> >
> > diff --git a/nptl/descr.h b/nptl/descr.h
> > index ada6867a19..733e7b0ce6 100644
> > --- a/nptl/descr.h
> > +++ b/nptl/descr.h
> > @@ -384,6 +384,13 @@ struct pthread
> > /* This is what the user specified and what we will report. */
> > size_t reported_guardsize;
> >
> > + /* Shadow stack base pointer. */
> > + void *shadow_stack_base;
> > + /* Shadow stack architecture-defined token. */
> > + void *shadow_stack_token;
> > + /* Shadow stack size. */
> > + size_t shadow_stack_size;
> > +
> > /* Thread Priority Protection data. */
> > struct priority_protection_data *tpp;
> >
> > diff --git a/nptl/pthread_create.c b/nptl/pthread_create.c
> > index 19e4ec8064..aec2b717b5 100644
> > --- a/nptl/pthread_create.c
> > +++ b/nptl/pthread_create.c
> > @@ -39,6 +39,7 @@
> > #include <clone_internal.h>
> > #include <futex-internal.h>
> > #include <getrandom-internal.h>
> > +#include <libc-shadow-stack.h>
> >
> > #include <shlib-compat.h>
> >
> > @@ -294,6 +295,9 @@ static int create_thread (struct pthread *pd, const struct pthread_attr *attr,
> > .stack = (uintptr_t) stackaddr,
> > .stack_size = stacksize,
> > .tls = (uintptr_t) tp,
> > + /* This should point to the architecture-defined token placed
> > + * on correctly allocated shadow stack. */
> > + .shadow_stack_token = (uintptr_t) pd->shadow_stack_token,
> > };
> > int ret = __clone_internal (&args, &start_thread, pd);
> > if (__glibc_unlikely (ret == -1))
> > @@ -578,6 +582,10 @@ start_thread (void *arg)
> > pd->setxid_futex = 0;
> > }
> >
> > + /* Shadow stack information required to de-allocate it later. */
> > + void *ss_base = pd->shadow_stack_base;
> > + size_t ss_size = pd->shadow_stack_size;
> > +
> > /* If the thread is detached free the TCB. */
> > if (IS_DETACHED (pd))
> > /* Free the TCB. */
> > @@ -586,6 +594,11 @@ start_thread (void *arg)
> > /* Remove the associated name from the thread stack. */
> > name_stack_maps (pd, false);
> >
> > + /* Free the shadow stack if we allocated one, no more ret
> > + is possible after this, must be inline right before exit. */
> > + if (ss_base)
> > + INTERNAL_SYSCALL_CALL (munmap, ss_base, ss_size);
> > +
> > out:
> > /* We cannot call '_exit' here. '_exit' will terminate the process.
> >
> > @@ -717,6 +730,15 @@ __pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
> > pd->schedpolicy = self->schedpolicy;
> > pd->schedparam = self->schedparam;
> >
> > + /* Set up a shadow stack. */
> > + err = THREAD_ALLOC_SHADOW_STACK (pd, stacksize);
> > + if (__glibc_unlikely (err != 0))
> > + {
> > + __nptl_deallocate_stack (pd);
> > + retval = err == ENOMEM ? EAGAIN : err;
> > + goto out;
> > + }
> > +
>
> What happens if we do not allocate a shadow stack and pass NULL for 'shadow_stack_token'?
> Will kernel not enable shadow stack for PR_SHADOW_STACK_ENABLE or will clone3 fail?
This is a valid use case, and kernel will do allocation of the shadow
stack (if shadow stack is enabled for this (callee) thread) and clone3()
will not fail.
> I wonder if we should stop allocating a shadow stack for user-defined stacks, similar on
> how we handle guard pages. My understanding in such situations, users aims to control
> exactly the memory usage of threads and GCS adds an extra burden for each thread.
Arguably, GCS (or any shadow stack implementation) is an orthogonal
security feature so we should not disable it implicitly because of
something else that a user / application does. What we do here is
just increasing control over memory usage.
Kind regards,
Yury
More information about the Libc-alpha
mailing list