When a shared library loaded via dlopen has both a thread_local variable
and a C++ static global destructor that calls dlclose on another library,
the process crashes with a SIGSEGV during exit.
The cause it due __run_exit_handlers calling __call_tls_dtors first,
decrementing l_tls_dtor_count to zero for all libraries. atexit
callbacks then fire in reverse registration order: library destructors
registered via __cxa_atexit at dlopen time run before _dl_fini, which was
registered at startup by __libc_start_main.
Using the example provided by BZ 33598, when lib1's destructor (~c1) calls
dlclose(lib2), _dl_close_worker scans the namespace and finds both lib1
and lib2 with l_direct_opencount==0 and l_tls_dtor_count==0. Both are
marked for unloading and unmapped and the code then returns from dlclose
into the now-unmapped lib1 (note that _dl_fini would have prevented this
by bumping l_direct_opencount before calling finalizers, but it runs too
late in the atexit order).
A new rtld_global field, _dl_exit_cxa_dso_handle (void *), is set in
__run_exit_handlers to f->func.cxa.dso_handle before each ef_cxa callback
fires and cleared to NULL after it returns.
In _dl_close_worker, the marking loop reads this value once as cxa_dso
(under dl_load_lock). For each candidate map with l_direct_opencount==0
and l_tls_dtor_count==0, if cxa_dso falls within [l_map_start, l_map_end)
the map is given IDX_STILL_USED. All other maps are subject to the normal
reference-count rules.
The __libc_freeres path (do_dlclose) needs no special treatment: it runs
outside any __cxa_atexit callback, so _dl_exit_cxa_dso_handle is NULL and
_dl_close_worker behaves normally, performing real unmaps for valgrind
leak detection. This scheme preserves the destructor ordering for both
dclose call during destructors and .initfini calls.
Two tests are added. Each loads lib1 (which has a thread_local and a
static destructor that calls dlclose on lib2) and verifies:
- No crash (the original bug): dlclose from a __cxa_atexit destructor
that was preceded by __call_tls_dtors does not segfault.
- Destructor ordering: lib2 exports set_fini_flag(int *p); its
destructor writes *p=1 through the stored pointer. lib1 registers
the address of a local flag after dlopen, then asserts the flag is
set immediately after dlclose returns. This confirms that lib2's
destructor ran during the dlclose call itself, not deferred to
_dl_fini.
tst-thrlocal-dlclose2 extends the scenario to lib2 itself opening a
third library (libm) during init and closing it during cleanup, covering
the case of nested dynamic dependencies being released from an atexit
handler.
Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu.
---
dlfcn/Makefile | 25 ++++++++++++
dlfcn/tst-thrlocal-dlclose1-lib1.cc | 50 +++++++++++++++++++++++
dlfcn/tst-thrlocal-dlclose1-lib2.cc | 36 +++++++++++++++++
dlfcn/tst-thrlocal-dlclose1.c | 29 ++++++++++++++
dlfcn/tst-thrlocal-dlclose2-lib1.cc | 62 +++++++++++++++++++++++++++++
dlfcn/tst-thrlocal-dlclose2-lib2.c | 47 ++++++++++++++++++++++
dlfcn/tst-thrlocal-dlclose2.c | 29 ++++++++++++++
elf/dl-close.c | 11 ++++-
elf/dl-support.c | 2 +
stdlib/exit.c | 8 ++++
sysdeps/generic/ldsodefs.h | 5 +++
11 files changed, 303 insertions(+), 1 deletion(-)
create mode 100644 dlfcn/tst-thrlocal-dlclose1-lib1.cc
create mode 100644 dlfcn/tst-thrlocal-dlclose1-lib2.cc
create mode 100644 dlfcn/tst-thrlocal-dlclose1.c
create mode 100644 dlfcn/tst-thrlocal-dlclose2-lib1.cc
create mode 100644 dlfcn/tst-thrlocal-dlclose2-lib2.c
create mode 100644 dlfcn/tst-thrlocal-dlclose2.c
@@ -68,9 +68,19 @@ tests = \
tst-dladdr \
tst-dlinfo \
tst-rec-dlopen \
+ tst-thrlocal-dlclose1 \
+ tst-thrlocal-dlclose2 \
tstatexit \
tstcxaatexit \
# tests
+
+ifneq ($(have-cxx-thread_local),yes)
+tests-unsupported += \
+ tst-thrlocal-dlclose1 \
+ tst-thrlocal-dlclose2
+ # tests-unsupported
+endif
+
endif
modules-names = \
bug-atexit1-lib \
@@ -90,8 +100,13 @@ modules-names = \
modcxaatexit \
moddummy1 \
moddummy2 \
+ tst-thrlocal-dlclose1-lib1 \
+ tst-thrlocal-dlclose1-lib2 \
+ tst-thrlocal-dlclose2-lib1 \
+ tst-thrlocal-dlclose2-lib2 \
# modules-names
+
failtestmod.so-no-z-defs = yes
glreflib2.so-no-z-defs = yes
errmsg1mod.so-no-z-defs = yes
@@ -197,3 +212,13 @@ $(objpfx)bug-dl-leaf.out: $(objpfx)bug-dl-leaf-lib-cb.so
$(objpfx)bug-dl-leaf-lib-cb.so: $(objpfx)bug-dl-leaf-lib.so
$(objpfx)tst-rec-dlopen.out: $(objpfx)moddummy1.so $(objpfx)moddummy2.so
+
+CFLAGS-tst-thrlocal-dlclose1-lib1.o += -std=gnu++11
+LDLIBS-tst-thrlocal-dlclose1-lib1.so = -lstdc++
+$(objpfx)tst-thrlocal-dlclose1.out: $(objpfx)tst-thrlocal-dlclose1-lib1.so \
+ $(objpfx)tst-thrlocal-dlclose1-lib2.so
+
+CFLAGS-tst-thrlocal-dlclose2-lib1.o += -std=gnu++11
+LDLIBS-tst-thrlocal-dlclose2-lib1.so = -lstdc++
+$(objpfx)tst-thrlocal-dlclose2.out: $(objpfx)tst-thrlocal-dlclose2-lib1.so \
+ $(objpfx)tst-thrlocal-dlclose2-lib2.so
new file mode 100644
@@ -0,0 +1,50 @@
+/* Module for tst-thrlocal-dlclose test.
+ Copyright (C) 2025 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 <assert.h>
+#include <memory>
+#include <dlfcn.h>
+
+thread_local std::unique_ptr<int> tlp;
+
+static struct c1
+{
+ void *h;
+ int lib2_fini_ran = 0;
+
+ c1 ()
+ {
+ h = dlopen ("tst-thrlocal-dlclose1-lib2.so", RTLD_NOW);
+ assert (h != NULL);
+
+ void (*set_fini_flag)(int *) =
+ (void (*)(int *)) dlsym (h, "set_fini_flag");
+ assert (set_fini_flag != NULL);
+ set_fini_flag (&lib2_fini_ran);
+
+ tlp = std::make_unique<int>();
+ }
+
+ ~c1 ()
+ {
+ assert (dlclose (h) == 0);
+ /* Verify lib2's destructor ran during dlclose, not deferred to
+ _dl_fini. */
+ assert (lib2_fini_ran == 1);
+ }
+} c1;
new file mode 100644
@@ -0,0 +1,36 @@
+/* Module for tst-thrlocal-dlclose test.
+ Copyright (C) 2025 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/>. */
+
+static int *fini_flag;
+
+extern "C" void
+set_fini_flag (int *p)
+{
+ fini_flag = p;
+}
+
+static struct lib2_dtor
+{
+ ~lib2_dtor ()
+ {
+ if (fini_flag != nullptr)
+ *fini_flag = 1;
+ }
+} lib2_dtor_obj;
+
+int foo (void) { return 42; }
new file mode 100644
@@ -0,0 +1,29 @@
+/* Check if thread local destructor dclose does not fail (BZ 33598)
+ Copyright (C) 2025 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <support/xdlfcn.h>
+
+int
+do_test (void)
+{
+ xdlclose (xdlopen ("tst-thrlocal-dlclose1-lib1.so", RTLD_NOW));
+
+ return 0;
+}
+
+#include <support/test-driver.c>
new file mode 100644
@@ -0,0 +1,62 @@
+/* Module for tst-thrlocal-dlclose test.
+ Copyright (C) 2025 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 <assert.h>
+#include <memory>
+#include <dlfcn.h>
+
+thread_local std::unique_ptr<int> tlp;
+
+static struct c1
+{
+ void *h;
+
+ void (*init)(void);
+ void (*cleanup)(void);
+ int lib2_fini_ran = 0;
+
+ c1 ()
+ {
+ h = dlopen ("tst-thrlocal-dlclose2-lib2.so", RTLD_NOW);
+ assert (h != NULL);
+
+ init = (void (*)(void)) dlsym (h, "init");
+ assert (init);
+
+ cleanup = (void (*)(void)) dlsym (h, "cleanup");
+ assert (cleanup);
+
+ void (*set_fini_flag)(int *) =
+ (void (*)(int *)) dlsym (h, "set_fini_flag");
+ assert (set_fini_flag != NULL);
+ set_fini_flag (&lib2_fini_ran);
+
+ init ();
+
+ tlp = std::make_unique<int>();
+ }
+
+ ~c1 ()
+ {
+ cleanup ();
+ assert (dlclose (h) == 0);
+ /* Verify lib2's destructor ran during dlclose, not deferred to
+ _dl_fini. */
+ assert (lib2_fini_ran == 1);
+ }
+} c1;
new file mode 100644
@@ -0,0 +1,47 @@
+/* Module for tst-thrlocal-dlclose test.
+ Copyright (C) 2025 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 <dlfcn.h>
+#include <assert.h>
+
+static void *h;
+static int *fini_flag;
+
+void
+set_fini_flag (int *p)
+{
+ fini_flag = p;
+}
+
+static void __attribute__ ((destructor))
+lib2_fini (void)
+{
+ if (fini_flag != NULL)
+ *fini_flag = 1;
+}
+
+void init (void)
+{
+ h = dlopen ("libm.so.6", RTLD_NOW);
+ assert(h);
+}
+
+void cleanup (void)
+{
+ dlclose (h);
+}
new file mode 100644
@@ -0,0 +1,29 @@
+/* Check if thread local destructor dclose does not fail (BZ 33598)
+ Copyright (C) 2025 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <support/xdlfcn.h>
+
+int
+do_test (void)
+{
+ xdlclose (xdlopen ("tst-thrlocal-dlclose2-lib1.so", RTLD_NOW));
+
+ return 0;
+}
+
+#include <support/test-driver.c>
@@ -144,6 +144,10 @@ _dl_close_worker (struct link_map *map, bool force)
bool any_tls = false;
const unsigned int nloaded = ns->_ns_nloaded;
struct link_map *maps[nloaded];
+ /* If a __cxa_atexit callback is currently running, its library must not
+ be unloaded while still on the call stack. Get the address of the DSO
+ handle to identify which map to protect in the loop below. */
+ ElfW(Addr) cxa_dso = (ElfW(Addr)) GL(dl_exit_cxa_dso_handle);
/* Run over the list and assign indexes to the link maps and enter
them into the MAPS array. */
@@ -185,7 +189,12 @@ _dl_close_worker (struct link_map *map, bool force)
/* See CONCURRENCY NOTES in cxa_thread_atexit_impl.c to know why
acquire is sufficient and correct. */
&& atomic_load_acquire (&l->l_tls_dtor_count) == 0
- && !l->l_map_used)
+ && !l->l_map_used
+ /* Protect the library whose __cxa_atexit callback is currently
+ executing: its pages must not be unmapped while still in use. */
+ && (cxa_dso == 0
+ || cxa_dso < l->l_map_start
+ || cxa_dso >= l->l_map_end))
continue;
/* We need this object and we handle it now. */
@@ -169,6 +169,8 @@ fpu_control_t _dl_fpu_control = _FPU_DEFAULT;
/* Required flags used for stack allocation. */
int _dl_stack_prot_flags = DEFAULT_STACK_PROT_PERMS;
+void *_dl_exit_cxa_dso_handle = NULL;
+
#if __PTHREAD_NPTL
list_t _dl_stack_used;
list_t _dl_stack_user;
@@ -21,6 +21,7 @@
#include <pointer_guard.h>
#include <libc-lock.h>
#include <set-freeres.h>
+#include <ldsodefs.h>
#include "exit.h"
/* Initialize the flag that indicates exit function processing
@@ -113,10 +114,17 @@ __run_exit_handlers (int status, struct exit_function_list **listp,
arg = f->func.cxa.arg;
PTR_DEMANGLE (cxafct);
+ /* Track the DSO handle of the currently-executing callback so
+ that _dl_close_worker can protect it from being unloaded if a
+ destructor calls dlclose on another library (BZ 33598). */
+ GL(dl_exit_cxa_dso_handle) = f->func.cxa.dso_handle;
+
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
cxafct (arg, status);
__libc_lock_lock (__exit_funcs_lock);
+
+ GL(dl_exit_cxa_dso_handle) = NULL;
break;
}
@@ -441,6 +441,11 @@ struct rtld_global
/* Generation counter for the dtv. */
EXTERN size_t _dl_tls_generation;
+ /* DSO handle of the __cxa_atexit callback currently executing, or NULL.
+ Used in _dl_close_worker to protect the executing library from being
+ unloaded while its destructor is still on the call stack (BZ 33598). */
+ EXTERN void *_dl_exit_cxa_dso_handle;
+
/* Scopes to free after next THREAD_GSCOPE_WAIT (). */
EXTERN struct dl_scope_free_list
{