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