[PATCH v4] dlfcn: Preserve destructor order when dlclose is called from atexit handler (BZ 33598)
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Thu Sep 10 18:15:11 GMT 2026
Ping.
On 06/05/26 17:28, Adhemerval Zanella wrote:
> 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
>
> diff --git a/dlfcn/Makefile b/dlfcn/Makefile
> index 00341dd476f..fcfdda2b9b0 100644
> --- a/dlfcn/Makefile
> +++ b/dlfcn/Makefile
> @@ -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
> diff --git a/dlfcn/tst-thrlocal-dlclose1-lib1.cc b/dlfcn/tst-thrlocal-dlclose1-lib1.cc
> new file mode 100644
> index 00000000000..50e7450f535
> --- /dev/null
> +++ b/dlfcn/tst-thrlocal-dlclose1-lib1.cc
> @@ -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;
> diff --git a/dlfcn/tst-thrlocal-dlclose1-lib2.cc b/dlfcn/tst-thrlocal-dlclose1-lib2.cc
> new file mode 100644
> index 00000000000..fecd1895038
> --- /dev/null
> +++ b/dlfcn/tst-thrlocal-dlclose1-lib2.cc
> @@ -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; }
> diff --git a/dlfcn/tst-thrlocal-dlclose1.c b/dlfcn/tst-thrlocal-dlclose1.c
> new file mode 100644
> index 00000000000..602e4ac32c9
> --- /dev/null
> +++ b/dlfcn/tst-thrlocal-dlclose1.c
> @@ -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>
> diff --git a/dlfcn/tst-thrlocal-dlclose2-lib1.cc b/dlfcn/tst-thrlocal-dlclose2-lib1.cc
> new file mode 100644
> index 00000000000..62c31873b6c
> --- /dev/null
> +++ b/dlfcn/tst-thrlocal-dlclose2-lib1.cc
> @@ -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;
> diff --git a/dlfcn/tst-thrlocal-dlclose2-lib2.c b/dlfcn/tst-thrlocal-dlclose2-lib2.c
> new file mode 100644
> index 00000000000..9f688935d7b
> --- /dev/null
> +++ b/dlfcn/tst-thrlocal-dlclose2-lib2.c
> @@ -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);
> +}
> diff --git a/dlfcn/tst-thrlocal-dlclose2.c b/dlfcn/tst-thrlocal-dlclose2.c
> new file mode 100644
> index 00000000000..ad1802df42f
> --- /dev/null
> +++ b/dlfcn/tst-thrlocal-dlclose2.c
> @@ -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>
> diff --git a/elf/dl-close.c b/elf/dl-close.c
> index 92bce07c7a4..00c9f565ac3 100644
> --- a/elf/dl-close.c
> +++ b/elf/dl-close.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. */
> diff --git a/elf/dl-support.c b/elf/dl-support.c
> index 0508d6113b5..50d3bbf5897 100644
> --- a/elf/dl-support.c
> +++ b/elf/dl-support.c
> @@ -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;
> diff --git a/stdlib/exit.c b/stdlib/exit.c
> index 114657a8fb8..eb37a9eae3a 100644
> --- a/stdlib/exit.c
> +++ b/stdlib/exit.c
> @@ -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;
> }
>
> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index 15c46598539..fb378c1aec4 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -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
> {
More information about the Libc-alpha
mailing list