[patch v1] nptl: namespace-safe pthread keys implementation

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Thu May 21 14:11:23 GMT 2026



On 12/05/26 22:00, DJ Delorie wrote:
> 
> Adhemerval Zanella Netto <adhemerval.zanella@linaro.org> writes:
>> The old design stored a generation sequence number in both the global slot and
>> the per-thread slot (KEY_UNUSED and KEY_USABLE). When a key was deleted and its
>> slot reused, per-thread values from the old key failed the sequence-number check
>> in pthread_getspecific and deallocate_tsd, and were silently ignored.
> 
> Were the ramifications of this memory leak fully understood?

But this new scheme adds a different race scenario:

1. Thread 1 issues setspecific(K, v).
2. Thread 2 issues pthread_key_delete(K) then pthread_key_create(&K') reusing
   the same slot/bucket, registering destructor destr'.
3. T1 exits, deallocate_tsd sees destr' at the slot and T1's per-thread keys[i] == v,
   calling destr'(v) — wrong destructor + wrong data.

The mitigating walk in *_create clears per-thread slots in threads on 
dl_stack_used/dl_stack_user, but only at the *create* moment; a thread that does 
pthread_setspecific between the delete and the create (legal? technically undefined 
per POSIX) is fine, but any in-flight value escaping the walk is mis-destroyed.

So current code leaks (POSIX-permitted, deterministic), while this proposal adds
an undefined behavior on stale data (calls wrong destructor with wrong-type pointer).
I am not sure if this is an improvement.

Maybe a better alternative would be to split the patch in two, one that fixes the
*namespace* share issue (move __pthread_keys[PTHREAD_KEYS_MAX] from libc into 
GL(dl_pthread_keys), nptl_db keeps working with just a symbol-relocation fix),
and then *another* to fix the scalability issue.

I am not sure if we can get away from the sequence number, not without adding
potential other race issues. Maybe we can a different sentinel value, where 
pthread_key_delete sets stating that this bucket is being freed, but deallocate_tsd
is not being called yet (so it will be moved to the free-slots only when the thread
finishes).  I need to think more about it.

Another potential issue which I just realized is destructors can call setspecific,
so it would be good to check the new scheme with this scenario.

> 
>>> +		  for (i = 0; i <slots; i ++)
>>> +		    {
>>> +		      void (*d)(void *) = atomic_load_relaxed (&destr_buckets[b]->keys[i]);
>>> +		      if (d != PTHREAD_KEY_SLOT_FREE && d != NULL
>>> +			  && bucket->keys[i] != NULL)
>>
>> Wouldn't this skip keys created with dest == NULL? I think
>> get_cached_stack might reuses it for a new thread without calling
>> _pthread_key_init.
> 
> We can't call a NULL destructor.  The only side effect would be that a
> key with a NULL destructor doesn't get cleared, but that also doesn't
> set specific_used, so the loop would ignore it a fixed number of times
> before the non-NULL-destructor keys are all destructed.  Then we unmap
> the memory, so all trace of the non-cleared keys vanishes.
> 
> Unless the destructor for one key calls setspecific for a different
> key... but the destructor is only called on thread exit, so why would
> it?

Does every reuse goes through allocate_stack which now unconditionally calls 
_pthread_key_init, even for stack from the cache (get_cached_stack)?

> 
>>>  int
>>>  ___pthread_key_create (pthread_key_t *key, void (*destr) (void *))
>>>  {
>>> -  /* Find a slot in __pthread_keys which is unused.  */
>>> -  for (size_t cnt = 0; cnt < PTHREAD_KEYS_MAX; ++cnt)
>>> +  int b, i;
>>> +  list_t *runp;
>>> +
>>> +  PTHREAD_KEY_LOCK;
>>> +  struct pthread_key_bucket **buckets = GL(dl_pthread_keys_data);
>>> +  if (buckets == NULL)
>>
>> I think this does not address the namespace issue you are trying to
>> fix.  The __pthread_key_lock is defined as attribute_hidden in libc.so
>> and when multiple libc instances are loaded with dlmopen, each
>> namespace has its own copy of the lock.  However, they all share the
>> same key table in ldso via GL(dl_pthread_keys_data).
> 
> Right, move lock to ld.so too...
> 
>>> -  if (__glibc_likely (key < PTHREAD_KEYS_MAX))
>>> +  if (buckets[b] == NULL)
>>>      {
>>> -      unsigned int seq = __pthread_keys[key].seq;
>>> +      PTHREAD_KEY_UNLOCK;
>>> +      return EAGAIN;
>>> +    }
>>
>> Not sure if POSIX allows return EGAIN here, I think it should be EINVAL.
> 
> 1003.1-2024 allows but doesn't list explicit errors, other than to
> forbid EINTR.  I could switch them all to EINVAL though, but do we want
> some way to tell the difference between a key which *could* be valid (in
> bounds but not created), vs a key which *couldn't* be valid (out of
> bounds) ?

But EAGAIN means "transient, retry may succeed.", this is not usually the
expected semantic for pthread_key_delete (retry until is works).

> 
> https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/functions/pthread_key_delete.html
> 



More information about the Libc-alpha mailing list