[glibc/azanella/bz15686] stdlib: Make __cxa_thread_atexit_impl lock-free (BZ 15686)

Adhemerval Zanella via Glibc-cvs <[email protected]> Sun, 19 Jul 2026 13:34:02 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=9039a9cb0fb9cdd4c17d6ee2687389ee2e5e63eb

commit 9039a9cb0fb9cdd4c17d6ee2687389ee2e5e63eb
Author: Adhemerval Zanella <[email protected]>
Date:   Sun Jul 19 09:59:53 2026 -0300

    stdlib: Make __cxa_thread_atexit_impl lock-free (BZ 15686)
    
    __cxa_thread_atexit_impl takes dl_load_lock to protect the
    _dl_find_dso_for_object lookup and the l_tls_dtor_count increment
    against a racing dlclose.  This deadlocks when the function is
    reached from a thread spawned by an ELF constructor, because dlopen
    runs constructors with dl_load_lock held.  The constructor blocks in
    pthread_join on the new thread, while the new thread blocks on
    dl_load_lock at its first thread_local access.
    
    Instead of releasing dl_load_lock around constructor execution
    (which requires per-map serialization of the constructor calls and
    re-auditing all the state accessed by dl_open_worker after the
    initializers run), remove the lock acquisition from
    __cxa_thread_atexit_impl:
    
      * The link map lookup now uses _dl_find_object, which is lock-free
        and async-signal-safe.  The newly loaded object is inserted into
        the _dl_find_object data structures by dl_open_worker before its
        constructors run, so lookups from constructor-spawned threads
        succeed.
    
      * The lock is not required for correctness: DSO_SYMBOL is the
        address of the caller's __dso_handle, so the calling thread is
        executing code of the object being looked up.  A dlclose that
        unloaded the object concurrently would unmap the running code
        itself, which is undefined regardless of the lock; a conforming
        program has to ensure the object stays loaded across the call,
        and the synchronization it uses for that also orders the
        l_tls_dtor_count increment before any subsequent
        _dl_close_worker check.
    
    The per-thread dso_symbol_cache/lm_cache is removed: the cache key
    was never updated, so every call already performed the O(n)
    _dl_find_dso_for_object walk over the loaded objects while holding
    dl_load_lock.  The replacement is a lock-free O(log n) lookup, so
    this is also a performance improvement for TLS destructor
    registration.
    
    Checked on aarch64-linux-gnu, x86_64-linux-gnu, i686-linux-gnu, and
    arm-linux-gnueabihf.

Diff:
---
 stdlib/cxa_thread_atexit_impl.c    | 78 +++++++++++++++++---------------
 sysdeps/pthread/Makefile           | 25 +++++++++++
 sysdeps/pthread/tst-create2.c      | 55 +++++++++++++++++++++++
 sysdeps/pthread/tst-create2mod.c   | 57 +++++++++++++++++++++++
 sysdeps/pthread/tst-create3.c      | 92 ++++++++++++++++++++++++++++++++++++++
 sysdeps/pthread/tst-create3.h      | 27 +++++++++++
 sysdeps/pthread/tst-create3mod.c   | 53 ++++++++++++++++++++++
 sysdeps/pthread/tst-create4.c      | 90 +++++++++++++++++++++++++++++++++++++
 sysdeps/pthread/tst-create4.h      | 34 ++++++++++++++
 sysdeps/pthread/tst-create4mod-a.c | 35 +++++++++++++++
 sysdeps/pthread/tst-create4mod-b.c | 35 +++++++++++++++
 sysdeps/pthread/tst-create5.c      | 50 +++++++++++++++++++++
 12 files changed, 594 insertions(+), 37 deletions(-)

diff --git a/stdlib/cxa_thread_atexit_impl.c b/stdlib/cxa_thread_atexit_impl.c
index c4382bc9ef..3d143795dc 100644
--- a/stdlib/cxa_thread_atexit_impl.c
+++ b/stdlib/cxa_thread_atexit_impl.c
@@ -25,15 +25,25 @@
    combinations of all three functions are the link map list, a link map for a
    DSO and the link map member l_tls_dtor_count.
 
-   __cxa_thread_atexit_impl acquires the dl_load_lock before accessing any
-   shared state and hence multiple of its instances can safely execute
-   concurrently.
-
-   _dl_close_worker acquires the dl_load_lock before accessing any shared state
-   as well and hence can concurrently execute multiple of its own instances as
-   well as those of __cxa_thread_atexit_impl safely.  Not all accesses to
-   l_tls_dtor_count are protected by the dl_load_lock, so we need to
-   synchronize using atomics.
+   __cxa_thread_atexit_impl does not take dl_load_lock (taking the lock
+   deadlocks if this function is reached from a thread spawned by an ELF
+   constructor running under dl_load_lock inside dlopen).  It locates the
+   caller's link map with _dl_find_object, which is async-signal-safe and
+   lock-free, and then increments l_tls_dtor_count atomically.
+
+   Not taking the lock is safe because DSO_SYMBOL is the address of the
+   caller's __dso_handle, so the calling thread is executing code of the very
+   object whose link map is being looked up.  A concurrent dlclose that
+   unloads the object while this function runs would unmap the caller's code
+   as well, which is undefined regardless of this function.  A conforming
+   program must ensure (via its own synchronization, e.g. joining the thread
+   before dlclose) that the object stays loaded across this call, and that
+   same synchronization publishes the l_tls_dtor_count increment to any
+   subsequent _dl_close_worker.
+
+   _dl_close_worker acquires the dl_load_lock before accessing any shared
+   state.  Not all accesses to l_tls_dtor_count are protected by the
+   dl_load_lock, so we need to synchronize using atomics.
 
    __call_tls_dtors accesses the l_tls_dtor_count without taking the lock; it
    decrements the value by one.  It does not need the big lock because it does
@@ -63,7 +73,9 @@
 
    Concurrent executions of __call_tls_dtors should only ensure that the value
    is accessed atomically; no reordering constraints need to be considered.
-   Likewise for the increment of l_tls_dtor_count in __cxa_thread_atexit_impl.
+   The same holds for the increment in __cxa_thread_atexit_impl, whose
+   ordering against _dl_close_worker is provided by the caller as described
+   above.
 
    There is still a possibility on concurrent execution of _dl_close_worker and
    __call_tls_dtors where _dl_close_worker reads the value of l_tls_dtor_count
@@ -72,6 +84,7 @@
    is not very different from a case where __call_tls_dtors is called after
    _dl_close_worker on the DSO and hence is an accepted execution.  */
 
+#include <dlfcn.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <ldsodefs.h>
@@ -88,8 +101,6 @@ struct dtor_list
 };
 
 static __thread struct dtor_list *tls_dtor_list;
-static __thread void *dso_symbol_cache;
-static __thread struct link_map *lm_cache;
 
 /* Register a destructor for TLS variables declared with the 'thread_local'
    keyword.  This function is only called from code generated by the C++
@@ -112,31 +123,24 @@ __cxa_thread_atexit_impl (dtor_func func, void *obj, void *dso_symbol)
   new->next = tls_dtor_list;
   tls_dtor_list = new;
 
-  /* We have to acquire the big lock to prevent a racing dlclose from pulling
-     our DSO from underneath us while we're setting up our destructor.  */
-  __rtld_lock_lock_recursive (GL(dl_load_lock));
-
-  /* See if we already encountered the DSO.  */
-  if (__glibc_unlikely (dso_symbol_cache != dso_symbol))
-    {
-      ElfW(Addr) caller = (ElfW(Addr)) dso_symbol;
-
-      struct link_map *l = _dl_find_dso_for_object (caller);
-
-      /* If the address is not recognized the call comes from the main
-	 program (we hope).  */
-      lm_cache = l ? l : GL(dl_ns)[LM_ID_BASE]._ns_loaded;
-    }
-
-  /* This increment may only be concurrently observed either by the decrement
-     in __call_tls_dtors since the other l_tls_dtor_count access in
-     _dl_close_worker is protected by the dl_load_lock.  The execution in
-     __call_tls_dtors does not really depend on this value beyond the fact that
-     it should be atomic, so Relaxed MO should be sufficient.  */
-  atomic_fetch_add_relaxed (&lm_cache->l_tls_dtor_count, 1);
-  __rtld_lock_unlock_recursive (GL(dl_load_lock));
-
-  new->map = lm_cache;
+  /* Locate the link map of the caller without taking dl_load_lock.  */
+  struct link_map *map;
+  struct dl_find_object dfo;
+  if (GLRO (dl_find_object) (dso_symbol, &dfo) == 0
+      && dfo.dlfo_link_map != NULL)
+    map = dfo.dlfo_link_map;
+  else
+    /* If the address is not recognized the call comes assume from the main
+       program.  */
+    map = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
+
+  /* This increment is only concurrently observed by the decrement in
+     __call_tls_dtors and by the load in _dl_close_worker; for the latter,
+     the caller's own synchronization with dlclose provides the required
+     ordering (see CONCURRENCY NOTES), so Relaxed MO is sufficient.  */
+  atomic_fetch_add_relaxed (&map->l_tls_dtor_count, 1);
+
+  new->map = map;
 
   return 0;
 }
diff --git a/sysdeps/pthread/Makefile b/sysdeps/pthread/Makefile
index d0f3cd59ac..9eebbb9ab3 100644
--- a/sysdeps/pthread/Makefile
+++ b/sysdeps/pthread/Makefile
@@ -349,6 +349,10 @@ tests += \
   tst-atfork3 \
   tst-atfork4 \
   tst-create1 \
+  tst-create2 \
+  tst-create3 \
+  tst-create4 \
+  tst-create5 \
   tst-fini1 \
   tst-pt-tls4 \
   # tests
@@ -365,6 +369,10 @@ modules-names += \
   tst-atfork3mod \
   tst-atfork4mod \
   tst-create1mod \
+  tst-create2mod \
+  tst-create3mod \
+  tst-create4mod-a \
+  tst-create4mod-b \
   tst-fini1mod \
   tst-stack2-mod \
   tst-tls4moda \
@@ -377,6 +385,9 @@ tst-atfork2mod.so-no-z-defs = yes
 tst-atfork3mod.so-no-z-defs = yes
 tst-atfork4mod.so-no-z-defs = yes
 tst-create1mod.so-no-z-defs = yes
+tst-create2mod.so-no-z-defs = yes
+tst-create4mod-a.so-no-z-defs = yes
+tst-create4mod-b.so-no-z-defs = yes
 
 ifeq ($(build-shared),yes)
 # Build all the modules even when not actually running test programs.
@@ -540,6 +551,20 @@ LDFLAGS-tst-create1 = -Wl,-export-dynamic
 $(objpfx)tst-create1: $(shared-thread-library)
 $(objpfx)tst-create1.out: $(objpfx)tst-create1mod.so
 
+$(objpfx)tst-create2: $(shared-thread-library)
+$(objpfx)tst-create2mod.so: $(shared-thread-library)
+$(objpfx)tst-create2.out: $(objpfx)tst-create2mod.so
+
+$(objpfx)tst-create3: $(shared-thread-library)
+$(objpfx)tst-create3.out: $(objpfx)tst-create3mod.so
+
+LDFLAGS-tst-create4 = -Wl,-export-dynamic
+$(objpfx)tst-create4: $(shared-thread-library)
+$(objpfx)tst-create4.out: \
+  $(objpfx)tst-create4mod-a.so $(objpfx)tst-create4mod-b.so
+
+$(objpfx)tst-create5: $(shared-thread-library)
+
 $(objpfx)tst-stack2.out: $(objpfx)tst-stack2-mod.so
 $(objpfx)tst-stack2-mod.so: $(shared-thread-library)
 LDFLAGS-tst-stack2-mod.so = -Wl,-z,execstack
diff --git a/sysdeps/pthread/tst-create2.c b/sysdeps/pthread/tst-create2.c
new file mode 100644
index 0000000000..961ee5ceea
--- /dev/null
+++ b/sysdeps/pthread/tst-create2.c
@@ -0,0 +1,55 @@
+/* Verify that a thread spawned by a dlopen constructor can register a
+   TLS destructor without deadlocking (BZ 15686).
+   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/>.  */
+
+
+/* thread 1: dlopen -> ctor -> pthread_create (worker) -> pthread_join
+   thread 2 (worker): __cxa_thread_atexit_impl -> lock (dl_load_lock)
+
+   dl_load_lock is held by thread 1 across the constructor execution, so if
+   __cxa_thread_atexit_impl acquires it the worker thread blocks forever and
+   pthread_join in the constructor never returns.  */
+
+#include <stdio.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+
+static int
+do_test (void)
+{
+  printf ("main: dlopen tst-create2mod.so\n");
+  void *h = xdlopen ("tst-create2mod.so", RTLD_NOW);
+  printf ("main: dlopen done\n");
+
+  /* The worker thread exited before the constructor's pthread_join
+     returned, so its TLS destructor has already run.  */
+  int *dtor_done = xdlsym (h, "tst_create2mod_dtor_done");
+  TEST_COMPARE (*dtor_done, 1);
+
+  xdlclose (h);
+  printf ("main: dlclose done\n");
+
+  /* The destructor already ran, so no reference is left on the
+     module's l_tls_dtor_count and dlclose must have unloaded it.  */
+  TEST_VERIFY (dlopen ("tst-create2mod.so", RTLD_NOW | RTLD_NOLOAD)
+	       == NULL);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/sysdeps/pthread/tst-create2mod.c b/sysdeps/pthread/tst-create2mod.c
new file mode 100644
index 0000000000..9351c160b2
--- /dev/null
+++ b/sysdeps/pthread/tst-create2mod.c
@@ -0,0 +1,57 @@
+/* Verify that a thread spawned by a dlopen constructor can register a
+   TLS destructor without deadlocking (BZ 15686).
+   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 <pthread.h>
+#include <stdlib.h>
+#include <dso_handle.h>
+
+extern int __cxa_thread_atexit_impl (void (*) (void *), void *, void *);
+
+int tst_create2mod_dtor_done;
+
+static void
+dtor (void *obj)
+{
+  *(int *) obj = 1;
+}
+
+/* The module TLS access mirrors the real-world trigger (a C++ thread_local
+   or Rust thread_local! first access), exercising __tls_get_addr from the
+   spawned thread as well.  */
+static __thread int tls_obj;
+
+static void *
+worker (void *arg)
+{
+  tls_obj = 1;
+  if (__cxa_thread_atexit_impl (dtor, &tst_create2mod_dtor_done,
+				__dso_handle) != 0)
+    abort ();
+  return &tls_obj;
+}
+
+static void __attribute__ ((constructor))
+do_init (void)
+{
+  pthread_t t;
+  if (pthread_create (&t, NULL, worker, NULL) != 0)
+    abort ();
+  if (pthread_join (t, NULL) != 0)
+    abort ();
+}
diff --git a/sysdeps/pthread/tst-create3.c b/sysdeps/pthread/tst-create3.c
new file mode 100644
index 0000000000..2122a473b0
--- /dev/null
+++ b/sysdeps/pthread/tst-create3.c
@@ -0,0 +1,92 @@
+/* Verify concurrent dlopen of the same DSO runs the constructor once
+   and does not return before the constructor has finished.
+   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/>.  */
+
+
+/* Two invariants are checked for concurrent dlopen of the same DSO:
+
+   1. The DSO's constructor runs exactly once, even if N threads race
+      into _dl_open.
+
+   2. No dlopen caller returns before the constructor has finished.
+      A caller that beats the constructor would observe globals in
+      their BSS-zeroed state.  */
+
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+#include <support/xthread.h>
+
+#include "tst-create3.h"
+
+#define NTHREADS 8
+
+static pthread_barrier_t start_barrier;
+static void *handles[NTHREADS];
+
+static void *
+worker (void *arg)
+{
+  int idx = (int) (intptr_t) arg;
+
+  /* Release all workers at once.  */
+  xpthread_barrier_wait (&start_barrier);
+
+  void *h = xdlopen ("tst-create3mod.so", RTLD_NOW);
+
+  /* The "done" flag is the constructor's final write.  If dlopen
+     returned before the constructor finished, this load will observe
+     0 instead of TST_CREATE3_MAGIC_DONE.  */
+  _Atomic unsigned int *done = xdlsym (h, "tst_create3mod_done");
+  unsigned int done_val = atomic_load_explicit (done, memory_order_acquire);
+  if (done_val != TST_CREATE3_MAGIC_DONE)
+    FAIL ("thread %d returned from dlopen before the constructor finished"
+	  " (tst_create3mod_done=0x%x)", idx, done_val);
+
+  /* Keep the handle open; main dlcloses after the count check.  */
+  handles[idx] = h;
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  pthread_t threads[NTHREADS];
+
+  xpthread_barrier_init (&start_barrier, NULL, NTHREADS);
+
+  for (int i = 0; i < NTHREADS; ++i)
+    threads[i] = xpthread_create (0, worker, (void *) (intptr_t) i);
+
+  for (int i = 0; i < NTHREADS; ++i)
+    xpthread_join (threads[i]);
+
+  /* The DSO is still loaded (all handles open), so the counter
+     reflects every constructor execution during the race above.  */
+  _Atomic int *count = xdlsym (handles[0], "tst_create3mod_ctor_count");
+  TEST_COMPARE (atomic_load_explicit (count, memory_order_acquire), 1);
+
+  for (int i = 0; i < NTHREADS; ++i)
+    xdlclose (handles[i]);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/sysdeps/pthread/tst-create3.h b/sysdeps/pthread/tst-create3.h
new file mode 100644
index 0000000000..9a448ba2bf
--- /dev/null
+++ b/sysdeps/pthread/tst-create3.h
@@ -0,0 +1,27 @@
+/* Shared definitions for tst-create3 and tst-create3mod.
+   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/>.  */
+
+#ifndef _TST_CREATE3_H
+#define _TST_CREATE3_H
+
+/* Magic value published by the module constructor as its final action.
+   Any dlopen caller that observes tst_create3mod_done with a different
+   value immediately after dlopen returned has beaten the constructor.  */
+#define TST_CREATE3_MAGIC_DONE 0xCAFEBABEu
+
+#endif
diff --git a/sysdeps/pthread/tst-create3mod.c b/sysdeps/pthread/tst-create3mod.c
new file mode 100644
index 0000000000..b9390ae043
--- /dev/null
+++ b/sysdeps/pthread/tst-create3mod.c
@@ -0,0 +1,53 @@
+/* DSO for tst-create3: concurrent dlopen constructor-once test.
+   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 <stdatomic.h>
+#include <time.h>
+
+#include "tst-create3.h"
+
+/* How long (nanoseconds) the constructor sleeps to simulate slow
+   initialization, so that concurrent dlopen callers are very likely
+   to reach _dl_open while the constructor is still running.  */
+#define TST_CREATE3_CTOR_SLEEP_NS 200000000	/* 200 ms */
+
+/* Counter incremented by the constructor.  Must be exactly 1 after
+   concurrent dlopen.  */
+_Atomic int tst_create3mod_ctor_count = 0;
+
+_Atomic unsigned int tst_create3mod_done = 0;
+
+static void __attribute__ ((constructor))
+do_init (void)
+{
+  atomic_fetch_add_explicit (&tst_create3mod_ctor_count, 1,
+			     memory_order_relaxed);
+
+  /* Slow the constructor down to widen the window in which a buggy
+     implementation would let a concurrent caller return from dlopen
+     before initialization finished.  */
+  struct timespec ts =
+    {
+      .tv_sec = TST_CREATE3_CTOR_SLEEP_NS / 1000000000L,
+      .tv_nsec = TST_CREATE3_CTOR_SLEEP_NS % 1000000000L
+    };
+  nanosleep (&ts, NULL);
+
+  atomic_store_explicit (&tst_create3mod_done, TST_CREATE3_MAGIC_DONE,
+			 memory_order_release);
+}
diff --git a/sysdeps/pthread/tst-create4.c b/sysdeps/pthread/tst-create4.c
new file mode 100644
index 0000000000..ce611dc2ea
--- /dev/null
+++ b/sysdeps/pthread/tst-create4.c
@@ -0,0 +1,90 @@
+/* Verify that constructors of independent DSOs loaded from different
+   threads do not interleave.
+   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/>.  */
+
+
+/* Two worker threads concurrently dlopen two independent DSOs (no DT_NEEDED
+   between them).  Each DSO's constructor appends a character to a shared
+   buffer in the main executable several times.  The test then asserts that
+   the two constructors did not interleave: the buffer must contain either
+   "AAAAABBBBB" or "BBBBBAAAAA".
+
+   This is a property of the current implementation, not of POSIX or the ELF
+   specification: dl_load_lock is held across the entire _dl_open call,
+   including constructor execution, so dlopen calls from different threads
+   are fully serialized.  */
+
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdio.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+#include <support/xthread.h>
+
+#include "tst-create4.h"
+
+_Atomic int tst_create4_seq_idx = 0;
+char tst_create4_seq_buf[TST_CREATE4_NSTEPS * 2 + 1];
+
+static pthread_barrier_t start_barrier;
+
+static void *
+worker_a (void *unused)
+{
+  xpthread_barrier_wait (&start_barrier);
+  void *h = xdlopen ("tst-create4mod-a.so", RTLD_NOW);
+  xdlclose (h);
+  return NULL;
+}
+
+static void *
+worker_b (void *unused)
+{
+  xpthread_barrier_wait (&start_barrier);
+  void *h = xdlopen ("tst-create4mod-b.so", RTLD_NOW);
+  xdlclose (h);
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  xpthread_barrier_init (&start_barrier, NULL, 2);
+
+  pthread_t ta = xpthread_create (0, worker_a, NULL);
+  pthread_t tb = xpthread_create (0, worker_b, NULL);
+  xpthread_join (ta);
+  xpthread_join (tb);
+
+  int len = atomic_load_explicit (&tst_create4_seq_idx,
+				  memory_order_acquire);
+  TEST_COMPARE (len, TST_CREATE4_NSTEPS * 2);
+  tst_create4_seq_buf[len] = '\0';
+
+  printf ("info: observed sequence: %s\n", tst_create4_seq_buf);
+
+  if (strcmp (tst_create4_seq_buf, "AAAAABBBBB") != 0
+      && strcmp (tst_create4_seq_buf, "BBBBBAAAAA") != 0)
+    FAIL ("constructors of independent DSOs interleaved: \"%s\"",
+	  tst_create4_seq_buf);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/sysdeps/pthread/tst-create4.h b/sysdeps/pthread/tst-create4.h
new file mode 100644
index 0000000000..5e13875e6f
--- /dev/null
+++ b/sysdeps/pthread/tst-create4.h
@@ -0,0 +1,34 @@
+/* Shared definitions for tst-create4 and its modules.
+   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/>.  */
+
+#ifndef _TST_CREATE4_H
+#define _TST_CREATE4_H
+
+#include <stdatomic.h>
+
+/* Number of characters each module constructor appends to the shared
+   buffer.  */
+#define TST_CREATE4_NSTEPS 5
+
+/* Shared buffer filled in by the two DSO constructors.  Defined in the
+   main executable and exported to the modules via the dynamic symbol
+   table (-Wl,-export-dynamic).  */
+extern _Atomic int tst_create4_seq_idx;
+extern char tst_create4_seq_buf[TST_CREATE4_NSTEPS * 2 + 1];
+
+#endif
diff --git a/sysdeps/pthread/tst-create4mod-a.c b/sysdeps/pthread/tst-create4mod-a.c
new file mode 100644
index 0000000000..947680d743
--- /dev/null
+++ b/sysdeps/pthread/tst-create4mod-a.c
@@ -0,0 +1,35 @@
+/* DSO A for tst-create4: writes 'A' into the shared buffer several
+   times, yielding between writes to maximize visible interleaving.
+   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 <stdatomic.h>
+#include <sched.h>
+
+#include "tst-create4.h"
+
+static void __attribute__ ((constructor))
+init_a (void)
+{
+  for (int i = 0; i < TST_CREATE4_NSTEPS; ++i)
+    {
+      int idx = atomic_fetch_add_explicit (&tst_create4_seq_idx, 1,
+					   memory_order_relaxed);
+      tst_create4_seq_buf[idx] = 'A';
+      sched_yield ();
+    }
+}
diff --git a/sysdeps/pthread/tst-create4mod-b.c b/sysdeps/pthread/tst-create4mod-b.c
new file mode 100644
index 0000000000..4ca4c19755
--- /dev/null
+++ b/sysdeps/pthread/tst-create4mod-b.c
@@ -0,0 +1,35 @@
+/* DSO B for tst-create4: writes 'B' into the shared buffer several
+   times, yielding between writes to maximize visible interleaving.
+   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 <stdatomic.h>
+#include <sched.h>
+
+#include "tst-create4.h"
+
+static void __attribute__ ((constructor))
+init_b (void)
+{
+  for (int i = 0; i < TST_CREATE4_NSTEPS; ++i)
+    {
+      int idx = atomic_fetch_add_explicit (&tst_create4_seq_idx, 1,
+					   memory_order_relaxed);
+      tst_create4_seq_buf[idx] = 'B';
+      sched_yield ();
+    }
+}
diff --git a/sysdeps/pthread/tst-create5.c b/sysdeps/pthread/tst-create5.c
new file mode 100644
index 0000000000..85945d6653
--- /dev/null
+++ b/sysdeps/pthread/tst-create5.c
@@ -0,0 +1,50 @@
+/* Verify that dlopen (NULL) from a worker thread does not deadlock.
+   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/>.  */
+
+
+/* An implementation that tracks per-object initialization state must
+   mark the main executable as fully initialized, otherwise a later
+   multi-threaded dlopen (NULL) /__RTLD_OPENEXEC call takes the
+   already-loaded early-return path in _dl_open and can wait forever for a
+   "pending" constructor.  */
+
+#include <stdio.h>
+#include <support/xdlfcn.h>
+#include <support/xthread.h>
+
+static void *
+worker (void *arg)
+{
+  printf ("worker: dlopen(NULL)\n");
+  void *h = xdlopen (NULL, RTLD_NOW);
+  printf ("worker: dlopen(NULL) done\n");
+  xdlclose (h);
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  pthread_t t = xpthread_create (0, worker, NULL);
+  xpthread_join (t);
+
+  printf ("main: worker finished\n");
+  return 0;
+}
+
+#include <support/test-driver.c>