[PATCH v4 2/5] nptl: Add libc allocated shadow stack for new threads

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Wed Nov 26 13:58:08 GMT 2025



On 26/11/25 10:41, Yury Khrustalev wrote:
> 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.

But currently we do not enforce GCS with PR_LOCK_SHADOW_STACK_STATUS, so any
thread change disable it but without any control of the shadow stack memory
usage.



More information about the Libc-alpha mailing list