[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