[v2,1/1] nptl: namespace-safe pthread keys implementation

Message ID xn7boodcgf.fsf@greed.delorie.com (mailing list archive)
State New
Headers
Series [v2,1/1] nptl: namespace-safe pthread keys implementation |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Test passed
linaro-tcwg-bot/tcwg_glibc_check--master-arm success Test passed

Commit Message

DJ Delorie May 27, 2026, 4:49 p.m. UTC
  This patch makes a couple of key changes to the pthread
thread-specific-data (tsd) key logic:

* Root data is in ld.so and not libc.so
* Malloc is not used for allocation
* No practical key limit

This primarily addresses the problems when dlmopen namespaces are used
and multiple copies of libc.so are loaded; previously there would be
two copies of the root data and thus keys could not be shared across
the namespace boundary.

The mechanism of the change is as follows:

(based on https://conf.gnu-tools-cauldron.org/opo25/talk/LQTU3G/)

The old method supported a fixed 1024 keys; the root data was a single
1024-length array and the thread data was [32][32] array with the
second level allocated with malloc.  The new method uses a [32] array
for the first level like the old method, but the second level is
allocated with mmap, and the zeroth entry contains the number of
entries.  A key's 5 LSBs index the first level, and the remaining bits
index the second level.  The first second level (i.e. [0][]) points to
a small fixed array in the per-thread area, like the old method.  The
second second layer (i.e. [1][]) points to a single mmap'd page and
can hold 255 keys.  Each additional second layer is double the size of
the previous, up to about a quarter trillion keys in the 31st second
layer.

The root data initially points at a structure in ld.so but if it's
NULL a backup copy in libc.so is used; this happens when statically
linked.

The nptl_db library parts, I ended up just commenting out as:

1. The internals are not documented
2. There are no test cases for it
3. I could find nothing that used those functions (not even gdb)
4. An AI-generated test case didn't even work with the old logic.

So with no users and no way to know if the code I was writing was
correct, I just left it empty.

---
changes in v2:
- switched to ld.so lock and lll_lock/lll_unlock
- don't use THREAD_GETMEM in pthread_key_create's thread loop
- Fix destructing keys with NULL destructors
- locked stack_cache in pthread_key_create
- reverted to seq/data pair logic
- renamed bucket2* macros and made them more global

---
 elf/Makefile                                  |   1 +
 elf/dl-pthread-keys.c                         |  17 +++
 elf/dl-support.c                              |   4 +
 elf/rtld.c                                    |   4 +
 nptl/Makefile                                 |   5 +-
 nptl/allocatestack.c                          |   5 +-
 nptl/descr.h                                  |   5 +-
 nptl/nptl_deallocate_tsd.c                    | 131 +++++++++---------
 nptl/pthread_getspecific.c                    |  62 +++++----
 nptl/pthread_key_create.c                     | 126 +++++++++++++++--
 nptl/pthread_key_delete.c                     |  31 +++--
 nptl/pthread_setspecific.c                    |  98 ++++++-------
 nptl/tst-pthread-keys-ns.c                    |  70 ++++++++++
 ...{pthread_keys.c => tst-pthread-keys-ns1.c} |  18 ++-
 nptl_db/structs.def                           |   2 +-
 nptl_db/td_ta_tsd_iter.c                      |   4 +
 nptl_db/td_thr_tsd.c                          |   4 +
 sysdeps/generic/ldsodefs.h                    |   8 ++
 sysdeps/nptl/dl-tls_init_tp.c                 |   2 +-
 sysdeps/nptl/fork.h                           |  28 ++--
 sysdeps/nptl/pthreadP.h                       |  19 ++-
 21 files changed, 447 insertions(+), 197 deletions(-)
 create mode 100644 elf/dl-pthread-keys.c
 create mode 100644 nptl/tst-pthread-keys-ns.c
 rename nptl/{pthread_keys.c => tst-pthread-keys-ns1.c} (65%)
  

Patch

diff --git a/elf/Makefile b/elf/Makefile
index aef13b73ca..47aefd620c 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -74,6 +74,7 @@  dl-routines = \
   dl-open \
   dl-origin \
   dl-printf \
+  dl-pthread-keys \
   dl-readonly-area \
   dl-reloc \
   dl-runtime \
diff --git a/elf/dl-pthread-keys.c b/elf/dl-pthread-keys.c
new file mode 100644
index 0000000000..ceb2173975
--- /dev/null
+++ b/elf/dl-pthread-keys.c
@@ -0,0 +1,17 @@ 
+#include <unistd.h>
+#include <ldsodefs.h>
+#include <list.h>
+#include <libc-lock.h>
+#include "pthreadP.h"
+
+/* This struct must match the global definition in pthreadP.h so that
+   the slot counts match.  */
+static struct pthread_key_struct key_bucket_zero[32] =
+  {
+    { 32, 0 },
+    [1 ... 31] = { 0, 0 }
+  };
+
+/* Table of the key information.  */
+struct pthread_key_struct *_dl_pthread_key_buckets_ldso[32] =
+  { (struct pthread_key_struct *)&key_bucket_zero };
diff --git a/elf/dl-support.c b/elf/dl-support.c
index 0508d6113b..3898979a0b 100644
--- a/elf/dl-support.c
+++ b/elf/dl-support.c
@@ -176,6 +176,10 @@  list_t _dl_stack_cache;
 size_t _dl_stack_cache_actsize;
 uintptr_t _dl_in_flight_stack;
 int _dl_stack_cache_lock;
+
+extern struct pthread_key_bucket *_dl_pthread_key_buckets_ldso[32];
+void *_dl_pthread_keys_data = _dl_pthread_key_buckets_ldso;
+int _dl_pthread_keys_lock = LLL_LOCK_INITIALIZER;
 #endif
 struct dl_scope_free_list *_dl_scope_free_list;
 
diff --git a/elf/rtld.c b/elf/rtld.c
index e926ec73e4..fdb7476dec 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -729,6 +729,8 @@  match_version (const char *string, struct link_map *map)
 
 bool __rtld_tls_init_tp_called;
 
+extern struct pthread_key_bucket *_dl_pthread_key_buckets_ldso[32];
+
 static void *
 init_tls (size_t naudit)
 {
@@ -775,6 +777,8 @@  cannot allocate TLS data structures for initial thread\n");
      so it knows not to pass this dtv to the normal realloc.  */
   GL(dl_initial_dtv) = GET_DTV (tcbp);
 
+  GL(dl_pthread_keys_data) = _dl_pthread_key_buckets_ldso;
+
   /* And finally install it for the main thread.  */
   call_tls_init_tp (tcbp);
   __rtld_tls_init_tp_called = true;
diff --git a/nptl/Makefile b/nptl/Makefile
index 02862d1c04..bfb999b898 100644
--- a/nptl/Makefile
+++ b/nptl/Makefile
@@ -123,7 +123,6 @@  routines = \
   pthread_join_common \
   pthread_key_create \
   pthread_key_delete \
-  pthread_keys \
   pthread_kill \
   pthread_kill_other_threads \
   pthread_mutex_cond_lock \
@@ -321,6 +320,7 @@  tests = \
   tst-pthread-gdb-attach-static \
   tst-pthread-getcpuclockid-invalid \
   tst-pthread-key1-static \
+  tst-pthread-keys-ns \
   tst-pthread-timedlock-lockloop \
   tst-pthread_exit-nothreads \
   tst-pthread_exit-nothreads-static \
@@ -485,6 +485,7 @@  modules-names = \
   tst-audit-threads-mod1 \
   tst-audit-threads-mod2 \
   tst-compat-forwarder-mod \
+  tst-pthread-keys-ns1 \
   tst-stack4mod \
   tst-tls-debug-mod \
   tst-tls3mod \
@@ -687,6 +688,8 @@  $(objpfx)tst-tls6.out: tst-tls6.sh $(objpfx)tst-tls5 \
 	$(evaluate-test)
 endif
 
+$(objpfx)tst-pthread-keys-ns.out: $(objpfx)tst-pthread-keys-ns1.so
+
 LDLIBS-tst-cancel24 = -Wl,--no-as-needed -lstdc++
 LDLIBS-tst-cancel24-static = $(LDLIBS-tst-cancel24)
 
diff --git a/nptl/allocatestack.c b/nptl/allocatestack.c
index b2ecb00113..559c160022 100644
--- a/nptl/allocatestack.c
+++ b/nptl/allocatestack.c
@@ -36,6 +36,7 @@ 
 #include <intprops.h>
 #include <setvmaname.h>
 
+
 /* Default alignment of stack.  */
 #ifndef STACK_ALIGN
 # define STACK_ALIGN __alignof__ (long double)
@@ -424,7 +425,7 @@  allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
       memset (pd, '\0', sizeof (struct pthread));
 
       /* The first TSD block is included in the TCB.  */
-      pd->specific[0] = pd->specific_1stblock;
+      _pthread_key_init (pd);
 
       /* Remember the stack-related values.  */
       pd->stackblock = (char *) stackaddr - size;
@@ -548,7 +549,7 @@  allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
 	  /* We allocated the first block thread-specific data array.
 	     This address will not change for the lifetime of this
 	     descriptor.  */
-	  pd->specific[0] = pd->specific_1stblock;
+	  _pthread_key_init (pd);
 
 	  /* This is at least the second thread.  */
 	  pd->header.multiple_threads = 1;
diff --git a/nptl/descr.h b/nptl/descr.h
index 627cc3980f..c08ca4bc7f 100644
--- a/nptl/descr.h
+++ b/nptl/descr.h
@@ -57,9 +57,6 @@ 
   ((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE - 1) \
    / PTHREAD_KEY_2NDLEVEL_SIZE)
 
-
-
-
 /* Internal version of the buffer to store cancellation handler
    information.  */
 struct pthread_unwind_buf
@@ -320,6 +317,8 @@  struct pthread
 
   /* We allocate one block of references here.  This should be enough
      to avoid allocating any memory dynamically for most applications.  */
+  /* Note that, in level2 data blocks, the [0] element contains the
+     number of elements (including the [0] one) in the allocation.  */
   struct pthread_key_data
   {
     /* Sequence number.  We use uintptr_t to not require padding on
diff --git a/nptl/nptl_deallocate_tsd.c b/nptl/nptl_deallocate_tsd.c
index 4c3960b516..57b685a394 100644
--- a/nptl/nptl_deallocate_tsd.c
+++ b/nptl/nptl_deallocate_tsd.c
@@ -15,48 +15,55 @@ 
    License along with the GNU C Library; if not, see
    <https://www.gnu.org/licenses/>.  */
 
+#include <ldsodefs.h>
 #include <pthreadP.h>
 
+static size_t page_size = 0;
+
+static size_t
+get_page_size(void)
+{
+  page_size = EXEC_PAGESIZE;
+  return page_size;
+}
+
 /* Deallocate POSIX thread-local-storage.  */
 void
 __nptl_deallocate_tsd (void)
 {
-  struct pthread *self = THREAD_SELF;
+  struct pthread_key_struct **global1;
+  struct pthread_key_struct *global2;
+  struct pthread_key_data *level2;
 
   /* Maybe no data was ever allocated.  This happens often so we have
      a flag for this.  */
-  if (THREAD_GETMEM (self, specific_used))
+  if (THREAD_GETMEM (THREAD_SELF, specific_used))
     {
-      size_t round;
-      size_t cnt;
-
-      round = 0;
-      do
-        {
-          size_t idx;
-
-          /* So far no new nonzero data entry.  */
-          THREAD_SETMEM (self, specific_used, false);
-
-          for (cnt = idx = 0; cnt < PTHREAD_KEY_1STLEVEL_SIZE; ++cnt)
-            {
-              struct pthread_key_data *level2;
-
-              level2 = THREAD_GETMEM_NC (self, specific, cnt);
-
-              if (level2 != NULL)
-                {
-                  size_t inner;
-
-                  for (inner = 0; inner < PTHREAD_KEY_2NDLEVEL_SIZE;
-                       ++inner, ++idx)
-                    {
-                      void *data = level2[inner].data;
-
-                      if (data != NULL)
-                        {
+      int b, i, iters_left = 32;
+
+      global1 = GL(dl_pthread_keys_data);
+      
+      /* Destroy all current key values.  */
+      while (--iters_left && THREAD_SELF->specific_used)
+	{
+	  /* So far no new nonzero data entry.  */
+	  THREAD_SETMEM (THREAD_SELF, specific_used, false);
+	  for (b = 0; b < 32; b ++)
+	    {
+	      level2 = THREAD_GETMEM_NC (THREAD_SELF, specific, b);
+	      global2 = global1[b];
+
+	      if (level2 != NULL)
+		{
+		  uintptr_t slots = level2[0].seq;
+		  for (i = 1; i <slots; i ++)
+		    {
+                      void *data = level2[i].data;
+		      
+		      if (data != NULL)
+			{
                           /* Always clear the data.  */
-                          level2[inner].data = NULL;
+			  level2[i].data = NULL;
 
                           /* Make sure the data corresponds to a valid
                              key.  This test fails if the key was
@@ -64,48 +71,38 @@  __nptl_deallocate_tsd (void)
                              re-allocated.  It is the user's
                              responsibility to free the memory in this
                              case.  */
-                          if (level2[inner].seq
-                              == __pthread_keys[idx].seq
+                          if (level2[i].seq
+                              == global2[i].seq
                               /* It is not necessary to register a destructor
                                  function.  */
-                              && __pthread_keys[idx].destr != NULL)
+                              && global2[i].destr != NULL)
                             /* Call the user-provided destructor.  */
-                            __pthread_keys[idx].destr (data);
-                        }
-                    }
-                }
-              else
-                idx += PTHREAD_KEY_1STLEVEL_SIZE;
-            }
-
-          if (THREAD_GETMEM (self, specific_used) == 0)
-            /* No data has been modified.  */
-            goto just_free;
-        }
-      /* We only repeat the process a fixed number of times.  */
-      while (__builtin_expect (++round < PTHREAD_DESTRUCTOR_ITERATIONS, 0));
+                            global2[i].destr (data);
+			}
+		    }
+		}
+	    }
+	}
 
       /* Just clear the memory of the first block for reuse.  */
-      memset (&THREAD_SELF->specific_1stblock, '\0',
-              sizeof (self->specific_1stblock));
+      memset (&THREAD_SELF->specific_1stblock[1], '\0',
+              sizeof (THREAD_SELF->specific_1stblock)
+	      - sizeof(THREAD_SELF->specific_1stblock[0]));
+
+      /* Unmap all mmap'd buckets.  */
+      for (b = 1; b < 32; b ++)
+	{
+	  level2 = THREAD_GETMEM_NC (THREAD_SELF, specific, b);
+	  if (level2 != NULL)
+	    {
+	      size_t mlen = PTHREAD_KEY_BUCKET2MLEN (b);
+	      __munmap (level2, mlen);
+	      THREAD_SETMEM_NC (THREAD_SELF, specific, b, 0);
+	    }
+	}
 
-    just_free:
-      /* Free the memory for the other blocks.  */
-      for (cnt = 1; cnt < PTHREAD_KEY_1STLEVEL_SIZE; ++cnt)
-        {
-          struct pthread_key_data *level2;
-
-          level2 = THREAD_GETMEM_NC (self, specific, cnt);
-          if (level2 != NULL)
-            {
-              /* The first block is allocated as part of the thread
-                 descriptor.  */
-              free (level2);
-              THREAD_SETMEM_NC (self, specific, cnt, NULL);
-            }
-        }
-
-      THREAD_SETMEM (self, specific_used, false);
     }
+  THREAD_SETMEM (THREAD_SELF, specific_used, false);
 }
+
 libc_hidden_def (__nptl_deallocate_tsd)
diff --git a/nptl/pthread_getspecific.c b/nptl/pthread_getspecific.c
index 136f4493ed..eec0a44972 100644
--- a/nptl/pthread_getspecific.c
+++ b/nptl/pthread_getspecific.c
@@ -16,47 +16,53 @@ 
    <https://www.gnu.org/licenses/>.  */
 
 #include <stdlib.h>
-#include "pthreadP.h"
 #include <shlib-compat.h>
 
+#include "pthreadP.h"
+#include "ldsodefs.h"
+
+extern void *___ldso_pthread_getspecific (pthread_key_t key)
+  weak_function;
+
 void *
 ___pthread_getspecific (pthread_key_t key)
 {
   struct pthread_key_data *data;
 
-  /* Special case access to the first 2nd-level block.  This is the
-     usual case.  */
-  if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE))
-    data = &THREAD_SELF->specific_1stblock[key];
-  else
-    {
-      /* Verify the key is sane.  */
-      if (key >= PTHREAD_KEYS_MAX)
-	/* Not valid.  */
-	return NULL;
-
-      unsigned int idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE;
-      unsigned int idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE;
-
-      /* If the sequence number doesn't match or the key cannot be defined
-	 for this thread since the second level array is not allocated
-	 return NULL, too.  */
-      struct pthread_key_data *level2 = THREAD_GETMEM_NC (THREAD_SELF,
-							  specific, idx1st);
-      if (level2 == NULL)
-	/* Not allocated, therefore no data.  */
-	return NULL;
-
-      /* There is data.  */
-      data = &level2[idx2nd];
-    }
+  int b = PTHREAD_KEY_DECODE_BUCKET(key);
+  int i = PTHREAD_KEY_DECODE_SLOT(key);
+
+  /* This is the 1st-level array for the global key data in ld.so.  */
+  struct pthread_key_struct **global1 = GL(dl_pthread_keys_data);
+
+  /* The spec doesn't allow for errors, so remove these checks after
+     development?  */
+  if (global1 == NULL)
+    return NULL;
+
+  struct pthread_key_struct *global2 = global1[b];
+  if (global2 == NULL)
+    return NULL;
+
+  /* The first slot is reserved for 2nd level size.  */
+  if (i < 1 || i >= global2[0].seq)
+    return NULL;
+
+  struct pthread_key_data *level2 = THREAD_GETMEM_NC (THREAD_SELF,
+						      specific, b);
+  if (level2 == NULL)
+    /* Not allocated, therefore no data.  */
+    return NULL;
+
+  /* There is data.  */
+  data = &level2[i];
 
   void *result = data->data;
   if (result != NULL)
     {
       uintptr_t seq = data->seq;
 
-      if (__glibc_unlikely (seq != __pthread_keys[key].seq))
+      if (__glibc_unlikely (seq != global2[i].seq))
 	result = data->data = NULL;
     }
 
diff --git a/nptl/pthread_key_create.c b/nptl/pthread_key_create.c
index e4c963bbc6..7265fed9af 100644
--- a/nptl/pthread_key_create.c
+++ b/nptl/pthread_key_create.c
@@ -15,37 +15,135 @@ 
    License along with the GNU C Library; if not, see
    <https://www.gnu.org/licenses/>.  */
 
+#include <assert.h>
 #include <errno.h>
-#include "pthreadP.h"
+#include <list.h>
 #include <atomic.h>
 #include <shlib-compat.h>
 
+#include "ldsodefs.h"
+#include "pthreadP.h"
+
+_Static_assert (PTHREAD_KEY_1STLEVEL_SIZE >= 32, "incompatible TSD sizes");
+
+/* This struct must match the global definition in pthreadP.h so that
+   the slot counts match.  */
+static struct pthread_key_data key_bucket_zero[32] =
+  {
+    { 32, 0 },
+    [1 ... 31] = { 0, 0 }
+  };
+
+/* Table of the key information.  Only used if ld.so didn't fill in
+   the rtld version of this.  */
+static struct pthread_key_data *__pthread_key_data_local[32] =
+  { (struct pthread_key_data *)&key_bucket_zero };
+
+static size_t page_size = 0;
+
+static size_t
+get_page_size(void)
+{
+  page_size = EXEC_PAGESIZE;
+  return page_size;
+}
+
+void
+_pthread_key_init (struct pthread *pd)
+{
+  /* Initialize global key data if ld.so didn't.  */
+  if (GL(dl_pthread_keys_data) == NULL)
+    GL(dl_pthread_keys_data) = __pthread_key_data_local;
+
+  /* Initialize thread key data.  */
+  pd->specific_1stblock[0].seq = PTHREAD_KEY_2NDLEVEL_SIZE;
+  pd->specific[0] = pd->specific_1stblock;
+}
+libc_hidden_def (_pthread_key_init)
+
+
 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;
+
+  PTHREAD_KEY_LOCK;
+  struct pthread_key_struct **global1 = GL(dl_pthread_keys_data);
+  if (global1 == NULL)
     {
-      uintptr_t seq = __pthread_keys[cnt].seq;
+      _pthread_key_init (THREAD_SELF);
+      global1 = GL(dl_pthread_keys_data);
+    }
 
-      if (KEY_UNUSED (seq) && KEY_USABLE (seq)
-	  /* We found an unused slot.  Try to allocate it.  */
-	  && ! atomic_compare_and_exchange_bool_acq (&__pthread_keys[cnt].seq,
-						     seq + 1, seq))
+  /* There are 32 "buckets" of key data, encoded in the 5 LSBs of the
+     key.  */
+  for (b = 0; b < 32; b ++)
+    {
+      struct pthread_key_struct *global2 = global1[b];
+
+      /* This bucket is missing, and therefor empty, so create it.  */
+      if (global2 == NULL)
 	{
-	  /* Remember the destructor.  */
-	  __pthread_keys[cnt].destr = destr;
+	  if (b == 0)
+	    /* We didn't initialize properly?  */
+	    *((int *)0) = 0;
+	  else
+	    {
+	      void *v;
+	      size_t mlen = PTHREAD_KEY_BUCKET2MLEN (b);
+	      v = __mmap (NULL, mlen, PROT_READ|PROT_WRITE,
+			  MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
+
+	      if (v == MAP_FAILED)
+		{
+		  PTHREAD_KEY_UNLOCK;
+		  /* Not in spec, but EAGAIN is the only other option.  */
+		  return ENOMEM;
+		}
+	      if (v == NULL)
+		*((int *)0) = 0;
 
-	  /* Return the key to the caller.  */
-	  *key = cnt;
+	      global2 = (struct pthread_key_struct *) v;
+	      global1[b] = global2;
+	      int num_slots = PTHREAD_KEY_BUCKET2SLOTS (b);
+	      global2[0].seq = num_slots;
 
-	  /* The call succeeded.  */
-	  return 0;
+	      /* If mmap returns a zero'd block, delete this.  */
+	      for (i=1; i<num_slots; i++)
+		{
+		  global2[i].seq = 0;
+		  global2[i].destr = (void (*)) NULL;
+		}
+	    }
+	}
+
+      /* Search for an empty slot.  Slot [0] contains the number of slots.  */
+      int s = global2[0].seq;
+      for (i = 1; i < s; i ++)
+	{
+	  uintptr_t seq = global2[i].seq;
+
+	  if (KEY_UNUSED (seq) && KEY_USABLE (seq)
+	      && ! atomic_compare_and_exchange_bool_acq (&global2[i].seq,
+							 seq + 1, seq))
+	    {
+	      /* Remember the destructor.  */
+	      global2[i].destr = destr;
+
+	      /* Return the key to the caller.  */
+	      *key = PTHREAD_KEY_ENCODE (b, i);
+
+	      /* The call succeeded.  */
+	      PTHREAD_KEY_UNLOCK;
+	      return 0;
+	    }
 	}
     }
 
+  PTHREAD_KEY_UNLOCK;
   return EAGAIN;
 }
+
 versioned_symbol (libc, ___pthread_key_create, __pthread_key_create,
 		  GLIBC_2_34);
 libc_hidden_ver (___pthread_key_create, __pthread_key_create)
diff --git a/nptl/pthread_key_delete.c b/nptl/pthread_key_delete.c
index 278e523ab1..95664729bb 100644
--- a/nptl/pthread_key_delete.c
+++ b/nptl/pthread_key_delete.c
@@ -16,27 +16,38 @@ 
    <https://www.gnu.org/licenses/>.  */
 
 #include <errno.h>
-#include "pthreadP.h"
 #include <atomic.h>
 #include <shlib-compat.h>
 
+#include "ldsodefs.h"
+#include "pthreadP.h"
+
+extern int ___ldso_pthread_key_delete (pthread_key_t key)
+  weak_function;
+
 int
 ___pthread_key_delete (pthread_key_t key)
 {
-  int result = EINVAL;
+  int b = PTHREAD_KEY_DECODE_BUCKET(key);
+  int i = PTHREAD_KEY_DECODE_SLOT(key);
+  struct pthread_key_struct **global1 = GL(dl_pthread_keys_data);
+
+  if (global1[b] == NULL)
+    return EINVAL;
+
+  struct pthread_key_struct *global2 = global1[b];
+  if (i < 1 || i >= global2[0].seq)
+    return EINVAL;
 
-  if (__glibc_likely (key < PTHREAD_KEYS_MAX))
-    {
-      unsigned int seq = __pthread_keys[key].seq;
+  int seq = global2[i].seq;
 
-      if (__builtin_expect (! KEY_UNUSED (seq), 1)
-	  && ! atomic_compare_and_exchange_bool_acq (&__pthread_keys[key].seq,
+  if (__builtin_expect (! KEY_UNUSED (seq), 1)
+	  && ! atomic_compare_and_exchange_bool_acq (&global2[i].seq,
 						     seq + 1, seq))
 	/* We deleted a valid key.  */
-	result = 0;
-    }
+    return 0;
 
-  return result;
+  return EINVAL;
 }
 versioned_symbol (libc, ___pthread_key_delete, pthread_key_delete,
 		  GLIBC_2_34);
diff --git a/nptl/pthread_setspecific.c b/nptl/pthread_setspecific.c
index 3cf07a020c..0dbba74082 100644
--- a/nptl/pthread_setspecific.c
+++ b/nptl/pthread_setspecific.c
@@ -17,75 +17,77 @@ 
 
 #include <errno.h>
 #include <stdlib.h>
-#include "pthreadP.h"
 #include <shlib-compat.h>
 
+#include "ldsodefs.h"
+#include "pthreadP.h"
+
+static size_t page_size = 0;
+
+static size_t
+get_page_size(void)
+{
+  page_size = EXEC_PAGESIZE;
+  return page_size;
+}
+
+
 int
 ___pthread_setspecific (pthread_key_t key, const void *value)
 {
   struct pthread *self;
-  unsigned int idx1st;
-  unsigned int idx2nd;
-  struct pthread_key_data *level2;
   unsigned int seq;
+  struct pthread_key_data *level2;
+  int b = PTHREAD_KEY_DECODE_BUCKET(key);
+  int i = PTHREAD_KEY_DECODE_SLOT(key);
 
   self = THREAD_SELF;
 
-  /* Special case access to the first 2nd-level block.  This is the
-     usual case.  */
-  if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE))
-    {
-      /* Verify the key is sane.  */
-      if (KEY_UNUSED ((seq = __pthread_keys[key].seq)))
-	/* Not valid.  */
-	return EINVAL;
+  /* The bucket number can only be 0..31, and all are valid.  */
+  struct pthread_key_struct **global1 = GL(dl_pthread_keys_data);
+  struct pthread_key_struct *global2 = global1[b];
+  if (global2 == NULL)
+    return EINVAL;
+  if (i < 0 || i >= global2[0].seq)
+    return EINVAL;
 
-      level2 = &self->specific_1stblock[key];
+  seq = global2[i].seq;
+  if (KEY_UNUSED (seq))
+    return EINVAL;
 
-      /* Remember that we stored at least one set of data.  */
-      if (value != NULL)
-	THREAD_SETMEM (self, specific_used, true);
-    }
-  else
-    {
-      if (key >= PTHREAD_KEYS_MAX
-	  || KEY_UNUSED ((seq = __pthread_keys[key].seq)))
-	/* Not valid.  */
-	return EINVAL;
+  /* If we need to mmap a bucket, do so now.  */
 
-      idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE;
-      idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE;
+  level2 = THREAD_GETMEM_NC (self, specific, b);
+
+  if (level2 == NULL)
+    {
+      size_t mlen = PTHREAD_KEY_BUCKET2MLEN (b);
+      size_t slots = PTHREAD_KEY_BUCKET2SLOTS (b);
+      struct pthread_key_data *m;
 
-      /* This is the second level array.  Allocate it if necessary.  */
-      level2 = THREAD_GETMEM_NC (self, specific, idx1st);
-      if (level2 == NULL)
+      if (b == 0)
 	{
-	  if (value == NULL)
-	    /* We don't have to do anything.  The value would in any case
-	       be NULL.  We can save the memory allocation.  */
-	    return 0;
-
-	  level2
-	    = (struct pthread_key_data *) calloc (PTHREAD_KEY_2NDLEVEL_SIZE,
-						  sizeof (*level2));
-	  if (level2 == NULL)
+	  m = (struct pthread_key_data *) & self->specific_1stblock;
+	  slots = PTHREAD_KEY_2NDLEVEL_SIZE;
+	}
+      else
+	{
+	  m = __mmap (0, mlen, PROT_READ|PROT_WRITE,
+		    MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
+	  if (m == MAP_FAILED)
 	    return ENOMEM;
-
-	  THREAD_SETMEM_NC (self, specific, idx1st, level2);
 	}
 
-      /* Pointer to the right array element.  */
-      level2 = &level2[idx2nd];
+      memset (m, 0, slots * sizeof (struct pthread_key_data));
+      m[0].seq = slots;
 
-      /* Remember that we stored at least one set of data.  */
-      THREAD_SETMEM (self, specific_used, true);
+      level2 = m;
+      THREAD_SETMEM_NC (self, specific, b, m);
     }
 
-  /* Store the data and the sequence number so that we can recognize
-     stale data.  */
-  level2->seq = seq;
-  level2->data = (void *) value;
-
+  THREAD_SETMEM (self, specific_used, 1);
+  level2[i].seq = seq;
+  level2[i].data = (void *) value;
   return 0;
 }
 versioned_symbol (libc, ___pthread_setspecific, pthread_setspecific,
diff --git a/nptl/tst-pthread-keys-ns.c b/nptl/tst-pthread-keys-ns.c
new file mode 100644
index 0000000000..336e161311
--- /dev/null
+++ b/nptl/tst-pthread-keys-ns.c
@@ -0,0 +1,70 @@ 
+/* Verify that keys are in sync across namespaces.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <support/check.h>
+#include <support/xdlfcn.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <pthread.h>
+
+/* This file loads ns1.so in a separate DSO namespace, and uses it to
+   create keys in that namespace, and compares to keys created in this
+   namespace.  */
+
+#define NUM_KEYS 30
+static pthread_key_t our_keys[NUM_KEYS];
+static pthread_key_t ns_keys[NUM_KEYS];
+
+static int (*ns_pthread_key_create)(pthread_key_t *key,
+				    void (*__destr_function) (void *));
+
+static int
+do_test (void)
+{
+  int i, j, errors=0;
+  void *so;
+
+  so = xdlmopen (LM_ID_NEWLM, "tst-pthread-keys-ns1.so", RTLD_NOW);
+  ns_pthread_key_create = dlsym (so, "ns_pthread_key_create");
+
+  for (i=0; i<NUM_KEYS; i++)
+    {
+      TEST_VERIFY (pthread_key_create (&our_keys[i], NULL) == 0);
+      TEST_VERIFY (ns_pthread_key_create (&ns_keys[i], NULL) == 0);
+
+      printf(" %08x %08x\n", our_keys[i], ns_keys[i]);
+    }
+
+  for (i=0; i<NUM_KEYS; i++)
+    for (j=0; j<NUM_KEYS; j++)
+      {
+	if (our_keys[i] == ns_keys[j])
+	  {
+	    if (errors < 5)
+	      printf("collision %x[%d] %x[%d]\n", our_keys[i], i, ns_keys[j], j);
+	    errors ++;
+	  }
+      }
+
+  xdlclose (so);
+
+  return errors;
+}
+
+#include <support/test-driver.c>
diff --git a/nptl/pthread_keys.c b/nptl/tst-pthread-keys-ns1.c
similarity index 65%
rename from nptl/pthread_keys.c
rename to nptl/tst-pthread-keys-ns1.c
index a98479e144..6041920b34 100644
--- a/nptl/pthread_keys.c
+++ b/nptl/tst-pthread-keys-ns1.c
@@ -1,5 +1,5 @@ 
-/* Table of pthread_key_create keys and their destructors.
-   Copyright (C) 2004-2026 Free Software Foundation, Inc.
+/* Verify that keys are in sync across namespaces.
+   Copyright (C) 2026 Free Software Foundation, Inc.
    This file is part of the GNU C Library.
 
    The GNU C Library is free software; you can redistribute it and/or
@@ -16,8 +16,14 @@ 
    License along with the GNU C Library; if not, see
    <https://www.gnu.org/licenses/>.  */
 
-#include <pthreadP.h>
+#include <pthread.h>
+#include <support/check.h>
 
-/* Table of the key information.  */
-struct pthread_key_struct __pthread_keys[PTHREAD_KEYS_MAX];
-libc_hidden_data_def (__pthread_keys)
+/* This file just connects the test case to a glibc in a different DSO
+   namespace.  */
+
+int ns_pthread_key_create (pthread_key_t *key,
+			   void (*__destr_function) (void *))
+{
+  return pthread_key_create (key, __destr_function);
+}
diff --git a/nptl_db/structs.def b/nptl_db/structs.def
index 78cf2a49a4..34322dca00 100644
--- a/nptl_db/structs.def
+++ b/nptl_db/structs.def
@@ -81,7 +81,7 @@  DB_VARIABLE (__nptl_nthreads)
 DB_VARIABLE (__nptl_last_event)
 DB_MAIN_VARIABLE (__nptl_initial_report_events)
 
-DB_ARRAY_VARIABLE (__pthread_keys)
+/*DB_ARRAY_VARIABLE (__pthread_keys)*/
 DB_STRUCT (pthread_key_struct)
 DB_STRUCT_FIELD (pthread_key_struct, seq)
 DB_STRUCT_FIELD (pthread_key_struct, destr)
diff --git a/nptl_db/td_ta_tsd_iter.c b/nptl_db/td_ta_tsd_iter.c
index 19f8c553fb..34c458fbee 100644
--- a/nptl_db/td_ta_tsd_iter.c
+++ b/nptl_db/td_ta_tsd_iter.c
@@ -24,6 +24,9 @@  td_err_e
 td_ta_tsd_iter (const td_thragent_t *ta_arg, td_key_iter_f *callback,
 		void *cbdata_p)
 {
+#if 1
+  return TD_ERR;
+#else
   td_thragent_t *const ta = (td_thragent_t *) ta_arg;
   td_err_e err;
   void *keys;
@@ -77,4 +80,5 @@  td_ta_tsd_iter (const td_thragent_t *ta_arg, td_key_iter_f *callback,
     }
 
   return TD_OK;
+#endif
 }
diff --git a/nptl_db/td_thr_tsd.c b/nptl_db/td_thr_tsd.c
index 4c207e2a7d..70b1ed888e 100644
--- a/nptl_db/td_thr_tsd.c
+++ b/nptl_db/td_thr_tsd.c
@@ -23,6 +23,9 @@ 
 td_err_e
 td_thr_tsd (const td_thrhandle_t *th, const thread_key_t tk, void **data)
 {
+#if 1
+  return TD_ERR;
+#else
   td_err_e err;
   psaddr_t tk_seq, level1, level2, seq, value;
   void *copy;
@@ -92,4 +95,5 @@  td_thr_tsd (const td_thrhandle_t *th, const thread_key_t tk, void **data)
     *data = value;
 
   return err;
+#endif
 }
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 15c4659853..f4a77de132 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -467,6 +467,13 @@  struct rtld_global
 
   /* Mutex protecting the stack lists.  */
   EXTERN int _dl_stack_cache_lock;
+
+  /* Pthread keys global destructor tables.  Actually a pointer to
+     pthread_key_bucket[32].  */
+  EXTERN void *_dl_pthread_keys_data;
+
+  /* Mutex protecting the pthread keys global data.  */
+  EXTERN int _dl_pthread_keys_lock;
 #endif
 #if __PTHREAD_HTL
   /* The total number of thread IDs currently in use, or on the list of
@@ -1455,6 +1462,7 @@  __rtld_mutex_init (void)
   /* The initialization happens later (!__PHREAD_NPTL) or is not
      needed at all (!SHARED).  */
 }
+
 #endif /* !__PHREAD_NPTL */
 
 /* Implementation of GL (dl_libc_freeres).  */
diff --git a/sysdeps/nptl/dl-tls_init_tp.c b/sysdeps/nptl/dl-tls_init_tp.c
index 72cc4087c9..e1c4317b5d 100644
--- a/sysdeps/nptl/dl-tls_init_tp.c
+++ b/sysdeps/nptl/dl-tls_init_tp.c
@@ -74,7 +74,7 @@  __tls_init_tp (void)
 
    /* Early initialization of the TCB.   */
    pd->tid = INTERNAL_SYSCALL_CALL (set_tid_address, &pd->joinstate);
-   THREAD_SETMEM (pd, specific[0], &pd->specific_1stblock[0]);
+   _pthread_key_init (pd);
    THREAD_SETMEM (pd, stack_mode, ALLOCATE_GUARD_USER);
    THREAD_SETMEM (pd, joinstate, THREAD_STATE_JOINABLE);
 
diff --git a/sysdeps/nptl/fork.h b/sysdeps/nptl/fork.h
index c09e57c5ab..fa45aa2ec8 100644
--- a/sysdeps/nptl/fork.h
+++ b/sysdeps/nptl/fork.h
@@ -113,23 +113,23 @@  reclaim_stacks (void)
 
 	  if (curp->specific_used)
 	    {
-	      /* Clear the thread-specific data.  */
-	      memset (curp->specific_1stblock, '\0',
-		      sizeof (curp->specific_1stblock));
-
 	      curp->specific_used = false;
 
-	      for (size_t cnt = 1; cnt < PTHREAD_KEY_1STLEVEL_SIZE; ++cnt)
-		if (curp->specific[cnt] != NULL)
-		  {
-		    memset (curp->specific[cnt], '\0',
-			    sizeof (curp->specific_1stblock));
-
-		    /* We have allocated the block which we do not
-		       free here so re-set the bit.  */
-		    curp->specific_used = true;
-		  }
+	      for (size_t cnt = 0; cnt < PTHREAD_KEY_1STLEVEL_SIZE; ++cnt)
+		{
+		  struct pthread_key_data *b = curp->specific[cnt];
+		  if (b != NULL)
+		    {
+		      memset (b + 1, '\0', (b[0].seq - 1) * sizeof (b[0]));
+
+		      /* We have allocated the block which we do not
+			 free here so re-set the bit.  */
+		      if (cnt > 0)
+			curp->specific_used = true;
+		    }
+		}
 	    }
+	  GL (dl_pthread_keys_lock) = LLL_LOCK_INITIALIZER;
 
 	  call_function_static_weak (__getrandom_reset_state, curp);
 	}
diff --git a/sysdeps/nptl/pthreadP.h b/sysdeps/nptl/pthreadP.h
index de432d4032..d1fb233b5e 100644
--- a/sysdeps/nptl/pthreadP.h
+++ b/sysdeps/nptl/pthreadP.h
@@ -185,8 +185,23 @@  extern int __attr_list_lock attribute_hidden;
 extern int __concurrency_level attribute_hidden;
 
 /* Thread-local data key handling.  */
-extern struct pthread_key_struct __pthread_keys[PTHREAD_KEYS_MAX];
-libc_hidden_proto (__pthread_keys)
+#define PTHREAD_KEY_BUCKET2MLEN(b) \
+  (((page_size != 0) ? page_size : get_page_size()) << (b-1))
+#define PTHREAD_KEY_BUCKET2SLOTS(b) \
+  (PTHREAD_KEY_BUCKET2MLEN(b) / sizeof (struct pthread_key_data))
+#define PTHREAD_KEY_ENCODE(b,i) ((pthread_key_t) (b | (i<<5)))
+#define PTHREAD_KEY_DECODE_BUCKET(k) (((size_t)k) & 31)
+#define PTHREAD_KEY_DECODE_SLOT(k) (((size_t)k) >> 5)
+#if IS_IN(rtld)
+#define PTHREAD_KEY_LOCK
+#define PTHREAD_KEY_UNLOCK
+#else
+#define PTHREAD_KEY_LOCK lll_lock (GL(dl_pthread_keys_lock), LLL_PRIVATE)
+#define PTHREAD_KEY_UNLOCK lll_unlock (GL(dl_pthread_keys_lock), LLL_PRIVATE)
+#endif
+
+extern void _pthread_key_init (struct pthread *);
+libc_hidden_proto (_pthread_key_init);
 
 /* Number of threads running.  */
 extern unsigned int __nptl_nthreads;