Re: [PATCH] stdlib: Make __cxa_thread_atexit_impl lock-free (BZ 15686)

Adhemerval Zanella <[email protected]> Mon, 3 Aug 2026 17:55:09 -0300
Newsgroups gmane.comp.lib.glibc.alpha
Message-ID <CAMXh4bWULb-yhMMcC7mbuyeQHdQXDPZBsGOrRb29bH9LTZxbbQ@mail.gmail.com>
--000000000000b2bb5b06582ac1dc
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

Hi Artem,



Em sex., 31 de jul. de 2026 =C3=A0s 10:52, Artem Proskurnev <[email protected]>
escreveu:

> Hi!
>
> I attached a minimal reproducible deadlock to BZ 15686.
>
> https://sourceware.org/bugzilla/show_bug.cgi?id=3D15686
>
> Unfortunately, this fix is not sufficient to resolve the issue.
>

Yes, I explicitly stated on the commit message this does not fully fix
BZ#15686.

It is a improvement to the the
__cxa_thread_atexit_impl scalability and can be reviewed and evaluated
separately.


> You suggested trying it here:
>
> https://inbox.sourceware.org/libc-alpha/f8138432-9ef3-41ec-a558-6f53011fe=
[email protected]/
>
> I've done the testing:
>
>
> https://inbox.sourceware.org/libc-alpha/b3fae3e9-e0d5-4e30-8ad6-08bcc8fbe=
[email protected]/
>
> 30.07.2026 21:26, Adhemerval Zanella:
> > __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.
> >
> > 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 by using
> > _dl_find_object instead.
> >
> > 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.
> >
> > The per-thread dso_symbol_cache/lm_cache is also removed, the cache
> > key was never updated.
> >
> > Using _dl_find_object also shows a slight better performance, it
> > replaces a O(n) by a lock-free O(log n) lookup.
> >
> > This does not fully fix BZ 15686.  dlopen still runs ELF
> > constructors with dl_load_lock held, so a thread spawned by a
> > constructor that the constructor then joins still deadlocks if it
> > calls dlopen and related functions; or if it lazily binds to a
> > symbol defined in another dlopen'ed object (which reaches
> > add_dependency in elf/dl-lookup.c).  Only the TLS destructor
> > registration path is addressed here.
> >
> > Checked on aarch64-linux-gnu, x86_64-linux-gnu, i686-linux-gnu, and
> > arm-linux-gnueabihf.
> > ---
> >   stdlib/cxa_thread_atexit_impl.c  | 81 +++++++++++++++++--------------=
-
> >   sysdeps/pthread/Makefile         |  6 +++
> >   sysdeps/pthread/tst-create2.c    | 52 ++++++++++++++++++++
> >   sysdeps/pthread/tst-create2mod.c | 51 ++++++++++++++++++++
> >   4 files changed, 152 insertions(+), 38 deletions(-)
> >   create mode 100644 sysdeps/pthread/tst-create2.c
> >   create mode 100644 sysdeps/pthread/tst-create2mod.c
> >
> > diff --git a/stdlib/cxa_thread_atexit_impl.c
> b/stdlib/cxa_thread_atexit_impl.c
> > index c4382bc9efe..c162461afb5 100644
> > --- a/stdlib/cxa_thread_atexit_impl.c
> > +++ b/stdlib/cxa_thread_atexit_impl.c
> > @@ -25,15 +25,24 @@
> >      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.
> > +   __cxa_thread_atexit_impl does not take dl_load_lock (taking it
> deadlocks
> > +   if this function is reached from a thread spawned by an ELF
> constructor,
> > +   because dlopen runs constructors with dl_load_lock held).  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.
> >
> > -   _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.
> > +   Not taking the lock is safe because DSO_SYMBOL is the address of th=
e
> > +   caller's __dso_handle, so the calling thread is executing code of
> the very
> > +   object.  A concurrent dlclose that unloads the object while this
> function
> > +   runs would unmap the caller's code as well, which is undefined.  A
> > +   conforming program must ensure, via its own synchronization, that t=
he
> > +   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 becaus=
e
> it does
> > @@ -63,7 +72,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 +83,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 +100,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++
> > @@ -102,42 +112,37 @@ __cxa_thread_atexit_impl (dtor_func func, void
> *obj, void *dso_symbol)
> >   {
> >     PTR_MANGLE (func);
> >
> > -  /* Prepend.  */
> >     struct dtor_list *new =3D calloc (1, sizeof (struct dtor_list));
> >     if (__glibc_unlikely (new =3D=3D NULL))
> >       __libc_fatal ("Fatal glibc error: failed to register TLS
> destructor: "
> >                 "out of memory\n");
> > +
> > +  /* A concurrent dlclose may already have reset the link map of a
> matching
> > +     entry, so check for it as well.  Either way the object is being
> unloaded
> > +     from underneath the caller, which is undefined; assume the main
> program
> > +     as for an unrecognized address.  */
> > +  struct link_map *map;
> > +  struct dl_find_object dfo;
> > +  if (GLRO (dl_find_object) (dso_symbol, &dfo) =3D=3D 0
> > +      && dfo.dlfo_link_map !=3D NULL)
> > +    map =3D dfo.dlfo_link_map;
> > +  else
> > +    map =3D GL(dl_ns)[LM_ID_BASE]._ns_loaded;
> > +
> >     new->func =3D func;
> >     new->obj =3D obj;
> > +  new->map =3D map;
> > +
> > +  /* 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 requir=
ed
> > +     ordering (see CONCURRENCY NOTES), so Relaxed MO is sufficient.  *=
/
> > +  atomic_fetch_add_relaxed (&map->l_tls_dtor_count, 1);
> > +
> > +  /* Prepend.  */
> >     new->next =3D tls_dtor_list;
> >     tls_dtor_list =3D 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 !=3D dso_symbol))
> > -    {
> > -      ElfW(Addr) caller =3D (ElfW(Addr)) dso_symbol;
> > -
> > -      struct link_map *l =3D _dl_find_dso_for_object (caller);
> > -
> > -      /* If the address is not recognized the call comes from the main
> > -      program (we hope).  */
> > -      lm_cache =3D 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 =3D lm_cache;
> > -
> >     return 0;
> >   }
> >
> > diff --git a/sysdeps/pthread/Makefile b/sysdeps/pthread/Makefile
> > index d0f3cd59ac6..5b062094ec4 100644
> > --- a/sysdeps/pthread/Makefile
> > +++ b/sysdeps/pthread/Makefile
> > @@ -349,6 +349,7 @@ tests +=3D \
> >     tst-atfork3 \
> >     tst-atfork4 \
> >     tst-create1 \
> > +  tst-create2 \
> >     tst-fini1 \
> >     tst-pt-tls4 \
> >     # tests
> > @@ -365,6 +366,7 @@ modules-names +=3D \
> >     tst-atfork3mod \
> >     tst-atfork4mod \
> >     tst-create1mod \
> > +  tst-create2mod \
> >     tst-fini1mod \
> >     tst-stack2-mod \
> >     tst-tls4moda \
> > @@ -540,6 +542,10 @@ LDFLAGS-tst-create1 =3D -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: $(libsupport) $(shared-thread-library)
> > +$(objpfx)tst-create2.out: $(objpfx)tst-create2mod.so
> > +
> >   $(objpfx)tst-stack2.out: $(objpfx)tst-stack2-mod.so
> >   $(objpfx)tst-stack2-mod.so: $(shared-thread-library)
> >   LDFLAGS-tst-stack2-mod.so =3D -Wl,-z,execstack
> > diff --git a/sysdeps/pthread/tst-create2.c
> b/sysdeps/pthread/tst-create2.c
> > new file mode 100644
> > index 00000000000..b5c81dc64ff
> > --- /dev/null
> > +++ b/sysdeps/pthread/tst-create2.c
> > @@ -0,0 +1,52 @@
> > +/* 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 <support/check.h>
> > +#include <support/xdlfcn.h>
> > +
> > +static int
> > +do_test (void)
> > +{
> > +  void *h =3D xdlopen ("tst-create2mod.so", RTLD_NOW);
> > +
> > +  /* The worker thread exited before the constructor's pthread_join
> > +     returned, so its TLS destructor has already run.  */
> > +  int *dtor_done =3D xdlsym (h, "tst_create2mod_dtor_done");
> > +  TEST_COMPARE (*dtor_done, 1);
> > +
> > +  xdlclose (h);
> > +
> > +  /* The destructor already ran, so no reference is left on the module=
's
> > +     l_tls_dtor_count and dlclose must have unloaded it.  */
> > +  void *h2 =3D dlopen ("tst-create2mod.so", RTLD_NOW | RTLD_NOLOAD);
> > +  TEST_VERIFY (h2 =3D=3D NULL);
> > +  if (h2 !=3D NULL)
> > +    xdlclose (h2);
> > +
> > +  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 00000000000..3ec31ce15ea
> > --- /dev/null
> > +++ b/sysdeps/pthread/tst-create2mod.c
> > @@ -0,0 +1,51 @@
> > +/* 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 <stdlib.h>
> > +#include <dso_handle.h>
> > +#include <support/check.h>
> > +#include <support/xthread.h>
> > +
> > +int tst_create2mod_dtor_done;
> > +
> > +static void
> > +dtor (void *obj)
> > +{
> > +  *(int *) obj =3D 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 *closure)
> > +{
> > +  tls_obj =3D 1;
> > +  TEST_COMPARE (__cxa_thread_atexit_impl (dtor,
> &tst_create2mod_dtor_done,
> > +                                       __dso_handle), 0);
> > +  return NULL;
> > +}
> > +
> > +static void __attribute__ ((constructor))
> > +do_init (void)
> > +{
> > +  xpthread_join (xpthread_create (NULL, worker, NULL));
> > +}
>

--000000000000b2bb5b06582ac1dc
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div dir=3D"auto">Hi Artem,</div><div dir=3D"auto"><br></div><div dir=3D"au=
to"><br></div><div><br><div class=3D"gmail_quote gmail_quote_container"><di=
v dir=3D"ltr" class=3D"gmail_attr">Em sex., 31 de jul. de 2026 =C3=A0s 10:5=
2, Artem Proskurnev &lt;<a href=3D"mailto:[email protected]">[email protected]</a>&=
gt; escreveu:<br></div><blockquote class=3D"gmail_quote" style=3D"margin:0p=
x 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">Hi=
!<br>
<br>
I attached a minimal reproducible deadlock to BZ 15686.<br>
<br>
<a rel=3D"noreferrer">https://sourceware.org/bugzilla/show_bug.cgi?id=3D156=
86</a><br>
<br>
Unfortunately, this fix is not sufficient to resolve the issue.<br>
</blockquote><div dir=3D"auto"><br></div><div dir=3D"auto">Yes, I explicitl=
y=C2=A0stated on the commit message this does not fully fix BZ#15686.</div>=
<div dir=3D"auto"><br></div><div dir=3D"auto">It is a improvement to the th=
e=C2=A0<div style=3D"font-size:inherit" dir=3D"auto">__cxa_thread_atexit_im=
pl scalability and can be reviewed and evaluated separately.</div></div><di=
v dir=3D"auto"><br></div><blockquote class=3D"gmail_quote" style=3D"margin:=
0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">=
<br>
You suggested trying it here:<br>
<a rel=3D"noreferrer">https://inbox.sourceware.org/libc-alpha/f8138432-9ef3=
[email protected]/</a><br>
<br>
I&#39;ve done the testing:<br>
<br>
<a rel=3D"noreferrer">https://inbox.sourceware.org/libc-alpha/b3fae3e9-e0d5=
[email protected]/</a><br>
<br>
30.07.2026 21:26, Adhemerval Zanella:<br>
&gt; __cxa_thread_atexit_impl takes dl_load_lock to protect the<br>
&gt; _dl_find_dso_for_object lookup and the l_tls_dtor_count increment<br>
&gt; against a racing dlclose.=C2=A0 This deadlocks when the function is<br=
>
&gt; reached from a thread spawned by an ELF constructor, because dlopen<br=
>
&gt; runs constructors with dl_load_lock held.<br>
&gt;<br>
&gt; Instead of releasing dl_load_lock around constructor execution<br>
&gt; (which requires per-map serialization of the constructor calls and<br>
&gt; re-auditing all the state accessed by dl_open_worker after the<br>
&gt; initializers run), remove the lock acquisition by using<br>
&gt; _dl_find_object instead.<br>
&gt;<br>
&gt; The lock is not required for correctness, DSO_SYMBOL is the address<br=
>
&gt; of the caller&#39;s __dso_handle, so the calling thread is executing<b=
r>
&gt; code of the object being looked up.=C2=A0 A dlclose that unloaded the<=
br>
&gt; object concurrently would unmap the running code itself, which is<br>
&gt; undefined regardless of the lock.<br>
&gt;<br>
&gt; The per-thread dso_symbol_cache/lm_cache is also removed, the cache<br=
>
&gt; key was never updated.<br>
&gt;<br>
&gt; Using _dl_find_object also shows a slight better performance, it<br>
&gt; replaces a O(n) by a lock-free O(log n) lookup.<br>
&gt;<br>
&gt; This does not fully fix BZ 15686.=C2=A0 dlopen still runs ELF<br>
&gt; constructors with dl_load_lock held, so a thread spawned by a<br>
&gt; constructor that the constructor then joins still deadlocks if it<br>
&gt; calls dlopen and related functions; or if it lazily binds to a<br>
&gt; symbol defined in another dlopen&#39;ed object (which reaches<br>
&gt; add_dependency in elf/dl-lookup.c).=C2=A0 Only the TLS destructor<br>
&gt; registration path is addressed here.<br>
&gt;<br>
&gt; Checked on aarch64-linux-gnu, x86_64-linux-gnu, i686-linux-gnu, and<br=
>
&gt; arm-linux-gnueabihf.<br>
&gt; ---<br>
&gt;=C2=A0 =C2=A0stdlib/cxa_thread_atexit_impl.c=C2=A0 | 81 +++++++++++++++=
++---------------<br>
&gt;=C2=A0 =C2=A0sysdeps/pthread/Makefile=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
|=C2=A0 6 +++<br>
&gt;=C2=A0 =C2=A0sysdeps/pthread/tst-create2.c=C2=A0 =C2=A0 | 52 ++++++++++=
++++++++++<br>
&gt;=C2=A0 =C2=A0sysdeps/pthread/tst-create2mod.c | 51 ++++++++++++++++++++=
<br>
&gt;=C2=A0 =C2=A04 files changed, 152 insertions(+), 38 deletions(-)<br>
&gt;=C2=A0 =C2=A0create mode 100644 sysdeps/pthread/tst-create2.c<br>
&gt;=C2=A0 =C2=A0create mode 100644 sysdeps/pthread/tst-create2mod.c<br>
&gt;<br>
&gt; diff --git a/stdlib/cxa_thread_atexit_impl.c b/stdlib/cxa_thread_atexi=
t_impl.c<br>
&gt; index c4382bc9efe..c162461afb5 100644<br>
&gt; --- a/stdlib/cxa_thread_atexit_impl.c<br>
&gt; +++ b/stdlib/cxa_thread_atexit_impl.c<br>
&gt; @@ -25,15 +25,24 @@<br>
&gt;=C2=A0 =C2=A0 =C2=A0 combinations of all three functions are the link m=
ap list, a link map for a<br>
&gt;=C2=A0 =C2=A0 =C2=A0 DSO and the link map member l_tls_dtor_count.<br>
&gt;=C2=A0 =C2=A0<br>
&gt; -=C2=A0 =C2=A0__cxa_thread_atexit_impl acquires the dl_load_lock befor=
e accessing any<br>
&gt; -=C2=A0 =C2=A0shared state and hence multiple of its instances can saf=
ely execute<br>
&gt; -=C2=A0 =C2=A0concurrently.<br>
&gt; +=C2=A0 =C2=A0__cxa_thread_atexit_impl does not take dl_load_lock (tak=
ing it deadlocks<br>
&gt; +=C2=A0 =C2=A0if this function is reached from a thread spawned by an =
ELF constructor,<br>
&gt; +=C2=A0 =C2=A0because dlopen runs constructors with dl_load_lock held)=
.=C2=A0 It locates the<br>
&gt; +=C2=A0 =C2=A0caller&#39;s link map with _dl_find_object, which is asy=
nc-signal-safe and<br>
&gt; +=C2=A0 =C2=A0lock-free, and then increments l_tls_dtor_count atomical=
ly.<br>
&gt;=C2=A0 =C2=A0<br>
&gt; -=C2=A0 =C2=A0_dl_close_worker acquires the dl_load_lock before access=
ing any shared state<br>
&gt; -=C2=A0 =C2=A0as well and hence can concurrently execute multiple of i=
ts own instances as<br>
&gt; -=C2=A0 =C2=A0well as those of __cxa_thread_atexit_impl safely.=C2=A0 =
Not all accesses to<br>
&gt; -=C2=A0 =C2=A0l_tls_dtor_count are protected by the dl_load_lock, so w=
e need to<br>
&gt; -=C2=A0 =C2=A0synchronize using atomics.<br>
&gt; +=C2=A0 =C2=A0Not taking the lock is safe because DSO_SYMBOL is the ad=
dress of the<br>
&gt; +=C2=A0 =C2=A0caller&#39;s __dso_handle, so the calling thread is exec=
uting code of the very<br>
&gt; +=C2=A0 =C2=A0object.=C2=A0 A concurrent dlclose that unloads the obje=
ct while this function<br>
&gt; +=C2=A0 =C2=A0runs would unmap the caller&#39;s code as well, which is=
 undefined.=C2=A0 A<br>
&gt; +=C2=A0 =C2=A0conforming program must ensure, via its own synchronizat=
ion, that the<br>
&gt; +=C2=A0 =C2=A0object stays loaded across this call, and that same sync=
hronization<br>
&gt; +=C2=A0 =C2=A0publishes the l_tls_dtor_count increment to any subseque=
nt<br>
&gt; +=C2=A0 =C2=A0_dl_close_worker.<br>
&gt; +<br>
&gt; +=C2=A0 =C2=A0_dl_close_worker acquires the dl_load_lock before access=
ing any shared<br>
&gt; +=C2=A0 =C2=A0state.=C2=A0 Not all accesses to l_tls_dtor_count are pr=
otected by the<br>
&gt; +=C2=A0 =C2=A0dl_load_lock, so we need to synchronize using atomics.<b=
r>
&gt;=C2=A0 =C2=A0<br>
&gt;=C2=A0 =C2=A0 =C2=A0 __call_tls_dtors accesses the l_tls_dtor_count wit=
hout taking the lock; it<br>
&gt;=C2=A0 =C2=A0 =C2=A0 decrements the value by one.=C2=A0 It does not nee=
d the big lock because it does<br>
&gt; @@ -63,7 +72,9 @@<br>
&gt;=C2=A0 =C2=A0<br>
&gt;=C2=A0 =C2=A0 =C2=A0 Concurrent executions of __call_tls_dtors should o=
nly ensure that the value<br>
&gt;=C2=A0 =C2=A0 =C2=A0 is accessed atomically; no reordering constraints =
need to be considered.<br>
&gt; -=C2=A0 =C2=A0Likewise for the increment of l_tls_dtor_count in __cxa_=
thread_atexit_impl.<br>
&gt; +=C2=A0 =C2=A0The same holds for the increment in __cxa_thread_atexit_=
impl, whose<br>
&gt; +=C2=A0 =C2=A0ordering against _dl_close_worker is provided by the cal=
ler as described<br>
&gt; +=C2=A0 =C2=A0above.<br>
&gt;=C2=A0 =C2=A0<br>
&gt;=C2=A0 =C2=A0 =C2=A0 There is still a possibility on concurrent executi=
on of _dl_close_worker and<br>
&gt;=C2=A0 =C2=A0 =C2=A0 __call_tls_dtors where _dl_close_worker reads the =
value of l_tls_dtor_count<br>
&gt; @@ -72,6 +83,7 @@<br>
&gt;=C2=A0 =C2=A0 =C2=A0 is not very different from a case where __call_tls=
_dtors is called after<br>
&gt;=C2=A0 =C2=A0 =C2=A0 _dl_close_worker on the DSO and hence is an accept=
ed execution.=C2=A0 */<br>
&gt;=C2=A0 =C2=A0<br>
&gt; +#include &lt;dlfcn.h&gt;<br>
&gt;=C2=A0 =C2=A0#include &lt;stdio.h&gt;<br>
&gt;=C2=A0 =C2=A0#include &lt;stdlib.h&gt;<br>
&gt;=C2=A0 =C2=A0#include &lt;ldsodefs.h&gt;<br>
&gt; @@ -88,8 +100,6 @@ struct dtor_list<br>
&gt;=C2=A0 =C2=A0};<br>
&gt;=C2=A0 =C2=A0<br>
&gt;=C2=A0 =C2=A0static __thread struct dtor_list *tls_dtor_list;<br>
&gt; -static __thread void *dso_symbol_cache;<br>
&gt; -static __thread struct link_map *lm_cache;<br>
&gt;=C2=A0 =C2=A0<br>
&gt;=C2=A0 =C2=A0/* Register a destructor for TLS variables declared with t=
he &#39;thread_local&#39;<br>
&gt;=C2=A0 =C2=A0 =C2=A0 keyword.=C2=A0 This function is only called from c=
ode generated by the C++<br>
&gt; @@ -102,42 +112,37 @@ __cxa_thread_atexit_impl (dtor_func func, void *=
obj, void *dso_symbol)<br>
&gt;=C2=A0 =C2=A0{<br>
&gt;=C2=A0 =C2=A0 =C2=A0PTR_MANGLE (func);<br>
&gt;=C2=A0 =C2=A0<br>
&gt; -=C2=A0 /* Prepend.=C2=A0 */<br>
&gt;=C2=A0 =C2=A0 =C2=A0struct dtor_list *new =3D calloc (1, sizeof (struct=
 dtor_list));<br>
&gt;=C2=A0 =C2=A0 =C2=A0if (__glibc_unlikely (new =3D=3D NULL))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0__libc_fatal (&quot;Fatal glibc error: faile=
d to register TLS destructor: &quot;<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;out=
 of memory\n&quot;);<br>
&gt; +<br>
&gt; +=C2=A0 /* A concurrent dlclose may already have reset the link map of=
 a matching<br>
&gt; +=C2=A0 =C2=A0 =C2=A0entry, so check for it as well.=C2=A0 Either way =
the object is being unloaded<br>
&gt; +=C2=A0 =C2=A0 =C2=A0from underneath the caller, which is undefined; a=
ssume the main program<br>
&gt; +=C2=A0 =C2=A0 =C2=A0as for an unrecognized address.=C2=A0 */<br>
&gt; +=C2=A0 struct link_map *map;<br>
&gt; +=C2=A0 struct dl_find_object dfo;<br>
&gt; +=C2=A0 if (GLRO (dl_find_object) (dso_symbol, &amp;dfo) =3D=3D 0<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 &amp;&amp; dfo.dlfo_link_map !=3D NULL)<br>
&gt; +=C2=A0 =C2=A0 map =3D dfo.dlfo_link_map;<br>
&gt; +=C2=A0 else<br>
&gt; +=C2=A0 =C2=A0 map =3D GL(dl_ns)[LM_ID_BASE]._ns_loaded;<br>
&gt; +<br>
&gt;=C2=A0 =C2=A0 =C2=A0new-&gt;func =3D func;<br>
&gt;=C2=A0 =C2=A0 =C2=A0new-&gt;obj =3D obj;<br>
&gt; +=C2=A0 new-&gt;map =3D map;<br>
&gt; +<br>
&gt; +=C2=A0 /* This increment is only concurrently observed by the decreme=
nt in<br>
&gt; +=C2=A0 =C2=A0 =C2=A0__call_tls_dtors and by the load in _dl_close_wor=
ker.=C2=A0 For the latter,<br>
&gt; +=C2=A0 =C2=A0 =C2=A0the caller&#39;s own synchronization with dlclose=
 provides the required<br>
&gt; +=C2=A0 =C2=A0 =C2=A0ordering (see CONCURRENCY NOTES), so Relaxed MO i=
s sufficient.=C2=A0 */<br>
&gt; +=C2=A0 atomic_fetch_add_relaxed (&amp;map-&gt;l_tls_dtor_count, 1);<b=
r>
&gt; +<br>
&gt; +=C2=A0 /* Prepend.=C2=A0 */<br>
&gt;=C2=A0 =C2=A0 =C2=A0new-&gt;next =3D tls_dtor_list;<br>
&gt;=C2=A0 =C2=A0 =C2=A0tls_dtor_list =3D new;<br>
&gt;=C2=A0 =C2=A0<br>
&gt; -=C2=A0 /* We have to acquire the big lock to prevent a racing dlclose=
 from pulling<br>
&gt; -=C2=A0 =C2=A0 =C2=A0our DSO from underneath us while we&#39;re settin=
g up our destructor.=C2=A0 */<br>
&gt; -=C2=A0 __rtld_lock_lock_recursive (GL(dl_load_lock));<br>
&gt; -<br>
&gt; -=C2=A0 /* See if we already encountered the DSO.=C2=A0 */<br>
&gt; -=C2=A0 if (__glibc_unlikely (dso_symbol_cache !=3D dso_symbol))<br>
&gt; -=C2=A0 =C2=A0 {<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 ElfW(Addr) caller =3D (ElfW(Addr)) dso_symbol;<b=
r>
&gt; -<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 struct link_map *l =3D _dl_find_dso_for_object (=
caller);<br>
&gt; -<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 /* If the address is not recognized the call com=
es from the main<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 program (we hope).=C2=A0 */<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 lm_cache =3D l ? l : GL(dl_ns)[LM_ID_BASE]._ns_l=
oaded;<br>
&gt; -=C2=A0 =C2=A0 }<br>
&gt; -<br>
&gt; -=C2=A0 /* This increment may only be concurrently observed either by =
the decrement<br>
&gt; -=C2=A0 =C2=A0 =C2=A0in __call_tls_dtors since the other l_tls_dtor_co=
unt access in<br>
&gt; -=C2=A0 =C2=A0 =C2=A0_dl_close_worker is protected by the dl_load_lock=
.=C2=A0 The execution in<br>
&gt; -=C2=A0 =C2=A0 =C2=A0__call_tls_dtors does not really depend on this v=
alue beyond the fact that<br>
&gt; -=C2=A0 =C2=A0 =C2=A0it should be atomic, so Relaxed MO should be suff=
icient.=C2=A0 */<br>
&gt; -=C2=A0 atomic_fetch_add_relaxed (&amp;lm_cache-&gt;l_tls_dtor_count, =
1);<br>
&gt; -=C2=A0 __rtld_lock_unlock_recursive (GL(dl_load_lock));<br>
&gt; -<br>
&gt; -=C2=A0 new-&gt;map =3D lm_cache;<br>
&gt; -<br>
&gt;=C2=A0 =C2=A0 =C2=A0return 0;<br>
&gt;=C2=A0 =C2=A0}<br>
&gt;=C2=A0 =C2=A0<br>
&gt; diff --git a/sysdeps/pthread/Makefile b/sysdeps/pthread/Makefile<br>
&gt; index d0f3cd59ac6..5b062094ec4 100644<br>
&gt; --- a/sysdeps/pthread/Makefile<br>
&gt; +++ b/sysdeps/pthread/Makefile<br>
&gt; @@ -349,6 +349,7 @@ tests +=3D \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-atfork3 \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-atfork4 \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-create1 \<br>
&gt; +=C2=A0 tst-create2 \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-fini1 \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-pt-tls4 \<br>
&gt;=C2=A0 =C2=A0 =C2=A0# tests<br>
&gt; @@ -365,6 +366,7 @@ modules-names +=3D \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-atfork3mod \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-atfork4mod \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-create1mod \<br>
&gt; +=C2=A0 tst-create2mod \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-fini1mod \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-stack2-mod \<br>
&gt;=C2=A0 =C2=A0 =C2=A0tst-tls4moda \<br>
&gt; @@ -540,6 +542,10 @@ LDFLAGS-tst-create1 =3D -Wl,-export-dynamic<br>
&gt;=C2=A0 =C2=A0$(objpfx)tst-create1: $(shared-thread-library)<br>
&gt;=C2=A0 =C2=A0$(objpfx)tst-create1.out: $(objpfx)tst-create1mod.so<br>
&gt;=C2=A0 =C2=A0<br>
&gt; +$(objpfx)tst-create2: $(shared-thread-library)<br>
&gt; +$(objpfx)tst-create2mod.so: $(libsupport) $(shared-thread-library)<br=
>
&gt; +$(objpfx)tst-create2.out: $(objpfx)tst-create2mod.so<br>
&gt; +<br>
&gt;=C2=A0 =C2=A0$(objpfx)tst-stack2.out: $(objpfx)tst-stack2-mod.so<br>
&gt;=C2=A0 =C2=A0$(objpfx)tst-stack2-mod.so: $(shared-thread-library)<br>
&gt;=C2=A0 =C2=A0LDFLAGS-tst-stack2-mod.so =3D -Wl,-z,execstack<br>
&gt; diff --git a/sysdeps/pthread/tst-create2.c b/sysdeps/pthread/tst-creat=
e2.c<br>
&gt; new file mode 100644<br>
&gt; index 00000000000..b5c81dc64ff<br>
&gt; --- /dev/null<br>
&gt; +++ b/sysdeps/pthread/tst-create2.c<br>
&gt; @@ -0,0 +1,52 @@<br>
&gt; +/* Verify that a thread spawned by a dlopen constructor can register =
a<br>
&gt; +=C2=A0 =C2=A0TLS destructor without deadlocking (BZ 15686).<br>
&gt; +=C2=A0 =C2=A0Copyright (C) 2026 Free Software Foundation, Inc.<br>
&gt; +=C2=A0 =C2=A0This file is part of the GNU C Library.<br>
&gt; +<br>
&gt; +=C2=A0 =C2=A0The GNU C Library is free software; you can redistribute=
 it and/or<br>
&gt; +=C2=A0 =C2=A0modify it under the terms of the GNU Lesser General Publ=
ic<br>
&gt; +=C2=A0 =C2=A0License as published by the Free Software Foundation; ei=
ther<br>
&gt; +=C2=A0 =C2=A0version 2.1 of the License, or (at your option) any late=
r version.<br>
&gt; +<br>
&gt; +=C2=A0 =C2=A0The GNU C Library is distributed in the hope that it wil=
l be useful,<br>
&gt; +=C2=A0 =C2=A0but WITHOUT ANY WARRANTY; without even the implied warra=
nty of<br>
&gt; +=C2=A0 =C2=A0MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.=C2=
=A0 See the GNU<br>
&gt; +=C2=A0 =C2=A0Lesser General Public License for more details.<br>
&gt; +<br>
&gt; +=C2=A0 =C2=A0You should have received a copy of the GNU Lesser Genera=
l Public<br>
&gt; +=C2=A0 =C2=A0License along with the GNU C Library; if not, see<br>
&gt; +=C2=A0 =C2=A0&lt;<a rel=3D"noreferrer">https://www.gnu.org/licenses/<=
/a>&gt;.=C2=A0 */<br>
&gt; +<br>
&gt; +/* thread 1: dlopen -&gt; ctor -&gt; pthread_create (worker) -&gt; pt=
hread_join<br>
&gt; +=C2=A0 =C2=A0thread 2 (worker): __cxa_thread_atexit_impl -&gt; lock (=
dl_load_lock)<br>
&gt; +<br>
&gt; +=C2=A0 =C2=A0dl_load_lock is held by thread 1 across the constructor =
execution, so if<br>
&gt; +=C2=A0 =C2=A0__cxa_thread_atexit_impl acquires it the worker thread b=
locks forever and<br>
&gt; +=C2=A0 =C2=A0pthread_join in the constructor never returns.=C2=A0 */<=
br>
&gt; +<br>
&gt; +#include &lt;support/check.h&gt;<br>
&gt; +#include &lt;support/xdlfcn.h&gt;<br>
&gt; +<br>
&gt; +static int<br>
&gt; +do_test (void)<br>
&gt; +{<br>
&gt; +=C2=A0 void *h =3D xdlopen (&quot;tst-create2mod.so&quot;, RTLD_NOW);=
<br>
&gt; +<br>
&gt; +=C2=A0 /* The worker thread exited before the constructor&#39;s pthre=
ad_join<br>
&gt; +=C2=A0 =C2=A0 =C2=A0returned, so its TLS destructor has already run.=
=C2=A0 */<br>
&gt; +=C2=A0 int *dtor_done =3D xdlsym (h, &quot;tst_create2mod_dtor_done&q=
uot;);<br>
&gt; +=C2=A0 TEST_COMPARE (*dtor_done, 1);<br>
&gt; +<br>
&gt; +=C2=A0 xdlclose (h);<br>
&gt; +<br>
&gt; +=C2=A0 /* The destructor already ran, so no reference is left on the =
module&#39;s<br>
&gt; +=C2=A0 =C2=A0 =C2=A0l_tls_dtor_count and dlclose must have unloaded i=
t.=C2=A0 */<br>
&gt; +=C2=A0 void *h2 =3D dlopen (&quot;tst-create2mod.so&quot;, RTLD_NOW |=
 RTLD_NOLOAD);<br>
&gt; +=C2=A0 TEST_VERIFY (h2 =3D=3D NULL);<br>
&gt; +=C2=A0 if (h2 !=3D NULL)<br>
&gt; +=C2=A0 =C2=A0 xdlclose (h2);<br>
&gt; +<br>
&gt; +=C2=A0 return 0;<br>
&gt; +}<br>
&gt; +<br>
&gt; +#include &lt;support/test-driver.c&gt;<br>
&gt; diff --git a/sysdeps/pthread/tst-create2mod.c b/sysdeps/pthread/tst-cr=
eate2mod.c<br>
&gt; new file mode 100644<br>
&gt; index 00000000000..3ec31ce15ea<br>
&gt; --- /dev/null<br>
&gt; +++ b/sysdeps/pthread/tst-create2mod.c<br>
&gt; @@ -0,0 +1,51 @@<br>
&gt; +/* Verify that a thread spawned by a dlopen constructor can register =
a<br>
&gt; +=C2=A0 =C2=A0TLS destructor without deadlocking (BZ 15686).<br>
&gt; +=C2=A0 =C2=A0Copyright (C) 2026 Free Software Foundation, Inc.<br>
&gt; +=C2=A0 =C2=A0This file is part of the GNU C Library.<br>
&gt; +<br>
&gt; +=C2=A0 =C2=A0The GNU C Library is free software; you can redistribute=
 it and/or<br>
&gt; +=C2=A0 =C2=A0modify it under the terms of the GNU Lesser General Publ=
ic<br>
&gt; +=C2=A0 =C2=A0License as published by the Free Software Foundation; ei=
ther<br>
&gt; +=C2=A0 =C2=A0version 2.1 of the License, or (at your option) any late=
r version.<br>
&gt; +<br>
&gt; +=C2=A0 =C2=A0The GNU C Library is distributed in the hope that it wil=
l be useful,<br>
&gt; +=C2=A0 =C2=A0but WITHOUT ANY WARRANTY; without even the implied warra=
nty of<br>
&gt; +=C2=A0 =C2=A0MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.=C2=
=A0 See the GNU<br>
&gt; +=C2=A0 =C2=A0Lesser General Public License for more details.<br>
&gt; +<br>
&gt; +=C2=A0 =C2=A0You should have received a copy of the GNU Lesser Genera=
l Public<br>
&gt; +=C2=A0 =C2=A0License along with the GNU C Library; if not, see<br>
&gt; +=C2=A0 =C2=A0&lt;<a rel=3D"noreferrer">https://www.gnu.org/licenses/<=
/a>&gt;.=C2=A0 */<br>
&gt; +<br>
&gt; +#include &lt;stdlib.h&gt;<br>
&gt; +#include &lt;dso_handle.h&gt;<br>
&gt; +#include &lt;support/check.h&gt;<br>
&gt; +#include &lt;support/xthread.h&gt;<br>
&gt; +<br>
&gt; +int tst_create2mod_dtor_done;<br>
&gt; +<br>
&gt; +static void<br>
&gt; +dtor (void *obj)<br>
&gt; +{<br>
&gt; +=C2=A0 *(int *) obj =3D 1;<br>
&gt; +}<br>
&gt; +<br>
&gt; +/* The module TLS access mirrors the real-world trigger (a C++ thread=
_local<br>
&gt; +=C2=A0 =C2=A0or Rust thread_local! first access), exercising __tls_ge=
t_addr from the<br>
&gt; +=C2=A0 =C2=A0spawned thread as well.=C2=A0 */<br>
&gt; +static __thread int tls_obj;<br>
&gt; +<br>
&gt; +static void *<br>
&gt; +worker (void *closure)<br>
&gt; +{<br>
&gt; +=C2=A0 tls_obj =3D 1;<br>
&gt; +=C2=A0 TEST_COMPARE (__cxa_thread_atexit_impl (dtor, &amp;tst_create2=
mod_dtor_done,<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0__dso=
_handle), 0);<br>
&gt; +=C2=A0 return NULL;<br>
&gt; +}<br>
&gt; +<br>
&gt; +static void __attribute__ ((constructor))<br>
&gt; +do_init (void)<br>
&gt; +{<br>
&gt; +=C2=A0 xpthread_join (xpthread_create (NULL, worker, NULL));<br>
&gt; +}<br>
</blockquote></div></div>

--000000000000b2bb5b06582ac1dc--