[PATCH] dlfcn: Fix dlclose crash during C++ thread_local destructor (BZ 33598)

Adhemerval Zanella adhemerval.zanella@linaro.org
Mon Nov 17 18:51:37 GMT 2025


If a dynamically loaded shared library uses thread_local and unloads
another library in its C++ destructor, glibc will crash because
dlclose(), which is called by the library, unmaps both libraries, and
control from dlclose() is returned to an unmapped page.

For instance:

  $ cat <<EOF > main.c
  #include <dlfcn.h>
  #include <assert.h>
  int main()
  {
    void *h = dlopen ("libt1.so", RTLD_NOW);
    assert (h);
    dlclose (h);
  }
  EOF
  $ cat <<EOF > t1.cpp
  #include <dlfcn.h>
  #include <assert.h>
  #include <memory>
  thread_local std::unique_ptr<int> tlp;
  static struct c1 {
    void *h;
    c1() {
      h = dlopen("libm.so", RTLD_NOW);
      assert(h);
      tlp = std::make_unique<int>();
    }
     ~c1() { dlclose(h); }
  } c1;
  EOF
  $ g++ -Wall -g -fPIC -shared -o libt1.so t1.cpp -ldl
  $ gcc -Wall -g -fPIC    main.c  -ldl -o main
  $ LD_LIBRARY_PATH=. ./main
  Segmentation fault

The issue is that during libt1.so dlclose, no module is unmap because
they are still in use (elf/dl-close.c:178). However, because the
libraries also create a thread-local variable in the main thread, the
struct destructor (c1::~c1) is called during __run_exit_handlers after
main returns.

In this phase, it is considered not loaded (l->l_direct_opencount equals
zero) and is scheduled to be unmapped later in _dl_close_worker. And
since the destructor is called from the library code itself, it returns
to the unmap address.

The easiest solution I could find to handle this is to extend dlclose to
internally pass the caller's linker maps and avoid unmapping the
libraries if the caller itself is responsible.

Checked on aarch64-linux-gnu and x86_64-linux-gnu.
---
 dlfcn/Makefile                     | 17 +++++++++++
 dlfcn/dlclose.c                    | 47 +++++++++++++++++++++++++-----
 dlfcn/tst-thrlocal-dlclose-lib1.cc | 38 ++++++++++++++++++++++++
 dlfcn/tst-thrlocal-dlclose-lib2.c  | 19 ++++++++++++
 dlfcn/tst-thrlocal-dlclose.c       | 29 ++++++++++++++++++
 elf/dl-close.c                     | 19 +++++++++---
 elf/dl-libc.c                      | 14 +++++++--
 elf/dl-open.c                      |  2 +-
 elf/rtld.c                         |  2 +-
 include/dlfcn.h                    | 10 ++++---
 sysdeps/generic/ldsodefs.h         |  2 +-
 11 files changed, 179 insertions(+), 20 deletions(-)
 create mode 100644 dlfcn/tst-thrlocal-dlclose-lib1.cc
 create mode 100644 dlfcn/tst-thrlocal-dlclose-lib2.c
 create mode 100644 dlfcn/tst-thrlocal-dlclose.c

diff --git a/dlfcn/Makefile b/dlfcn/Makefile
index ab392f48df..25ea71808c 100644
--- a/dlfcn/Makefile
+++ b/dlfcn/Makefile
@@ -70,7 +70,15 @@ tests = \
   tst-rec-dlopen \
   tstatexit \
   tstcxaatexit \
+  tst-thrlocal-dlclose \
   # tests
+
+ifneq ($(have-cxx-thread_local),yes)
+tests-unsupported += \
+  tst-thrlocal-dlclose
+  # tests-unsupported
+endif
+
 endif
 modules-names = \
   bug-atexit1-lib \
@@ -90,8 +98,11 @@ modules-names = \
   modcxaatexit \
   moddummy1 \
   moddummy2 \
+  tst-thrlocal-dlclose-lib1 \
+  tst-thrlocal-dlclose-lib2 \
   # modules-names
 
+
 failtestmod.so-no-z-defs = yes
 glreflib2.so-no-z-defs = yes
 errmsg1mod.so-no-z-defs = yes
@@ -197,3 +208,9 @@ $(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-thread_local1-lib1.o += -std=gnu++11
+LDLIBS-tst-thrlocal-dlclose-lib1.so = -lstdc++
+$(objpfx)tst-thrlocal-dlclose-lib1.so: $(libsupport)
+$(objpfx)tst-thrlocal-dlclose.out: $(objpfx)tst-thrlocal-dlclose-lib1.so \
+				   $(objpfx)tst-thrlocal-dlclose-lib2.so
diff --git a/dlfcn/dlclose.c b/dlfcn/dlclose.c
index d0ca01f601..46643e1d81 100644
--- a/dlfcn/dlclose.c
+++ b/dlfcn/dlclose.c
@@ -20,18 +20,51 @@
 #include <ldsodefs.h>
 #include <shlib-compat.h>
 
-int
-__dlclose (void *handle)
+struct dlclose_args
+{
+  void *handle;
+  void *caller;
+};
+
+static void
+dlclose_doit (void *a)
 {
+  struct dlclose_args *args = (struct dlclose_args *) a;
+  GLRO(dl_close) (args->handle, args->caller);
+}
+
+static int
+dlclose_implementation (void *handle, void *dl_caller)
+{
+  return _dlerror_run (dlclose_doit, &(struct dlclose_args) {
+				       .handle= handle,
+				       .caller = RETURN_ADDRESS (0)}) ? -1 : 0;
+}
+
 #ifdef SHARED
+int
+___dlclose (void *handle)
+{
   if (GLRO (dl_dlfcn_hook) != NULL)
-    return GLRO (dl_dlfcn_hook)->dlclose (handle);
-#endif
+    return GLRO (dl_dlfcn_hook)->dlclose (handle, RETURN_ADDRESS (0));
+  return dlclose_implementation (handle, RETURN_ADDRESS (0));
+}
+versioned_symbol (libc, ___dlclose, dlclose, GLIBC_2_34);
+#else
+int
+__dlclose (void *handle, void *dl_caller)
+{
+  return dlclose_implementation (handle, dl_caller);
+}
 
-  return _dlerror_run (GLRO (dl_close), handle) ? -1 : 0;
+int
+___dlclose (void *handle)
+{
+  return __dlclose (handle, RETURN_ADDRESS (0));
 }
-versioned_symbol (libc, __dlclose, dlclose, GLIBC_2_34);
+weak_alias (___dlclose, dlclose)
+#endif
 
 #if OTHER_SHLIB_COMPAT (libdl, GLIBC_2_0, GLIBC_2_34)
-compat_symbol (libdl, __dlclose, dlclose, GLIBC_2_0);
+compat_symbol (libdl, ___dlclose, dlclose, GLIBC_2_0);
 #endif
diff --git a/dlfcn/tst-thrlocal-dlclose-lib1.cc b/dlfcn/tst-thrlocal-dlclose-lib1.cc
new file mode 100644
index 0000000000..e42a0d35ee
--- /dev/null
+++ b/dlfcn/tst-thrlocal-dlclose-lib1.cc
@@ -0,0 +1,38 @@
+/* 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 <memory>
+#include <support/xdlfcn.h>
+
+thread_local std::unique_ptr<int> tlp;
+
+static struct c1
+{
+  void *h;
+
+  c1 ()
+  {
+    h = xdlopen ("tst-thrlocal-dlclose-lib2.so", RTLD_NOW);
+    tlp = std::make_unique<int>();
+  }
+
+  ~c1 ()
+  {
+    xdlclose (h);
+  }
+} c1;
diff --git a/dlfcn/tst-thrlocal-dlclose-lib2.c b/dlfcn/tst-thrlocal-dlclose-lib2.c
new file mode 100644
index 0000000000..88d0f767bf
--- /dev/null
+++ b/dlfcn/tst-thrlocal-dlclose-lib2.c
@@ -0,0 +1,19 @@
+/* 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/>.  */
+
+int foo (void) { return 42; }
diff --git a/dlfcn/tst-thrlocal-dlclose.c b/dlfcn/tst-thrlocal-dlclose.c
new file mode 100644
index 0000000000..aaaca75d45
--- /dev/null
+++ b/dlfcn/tst-thrlocal-dlclose.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-dlclose-lib1.so", RTLD_NOW));
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/dl-close.c b/elf/dl-close.c
index 83e4f012b2..d02069a70d 100644
--- a/elf/dl-close.c
+++ b/elf/dl-close.c
@@ -106,7 +106,8 @@ remove_slotinfo (size_t idx, struct dtv_slotinfo_list *listp, size_t disp,
 }
 
 void
-_dl_close_worker (struct link_map *map, bool force)
+_dl_close_worker (struct link_map *map, const struct link_map *caller_map,
+		  bool force)
 {
   /* One less direct use.  */
   --map->l_direct_opencount;
@@ -180,7 +181,8 @@ _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
+	  && l != caller_map)
 	continue;
 
       /* We need this object and we handle it now.  */
@@ -754,7 +756,7 @@ _dl_close_worker (struct link_map *map, bool force)
 
 
 void
-_dl_close (void *_map)
+_dl_close (void *_map, void *caller_dlclose)
 {
   struct link_map *map = _map;
 
@@ -790,7 +792,16 @@ _dl_close (void *_map)
       _dl_signal_error (0, map->l_name, NULL, N_("shared object not open"));
     }
 
-  _dl_close_worker (map, false);
+  const struct link_map *caller_map;
+  {
+    struct dl_find_object dlfo;
+    if (_dl_find_object (caller_dlclose, &dlfo) == 0)
+      caller_map = dlfo.dlfo_link_map;
+    else
+      caller_map = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
+  }
+
+  _dl_close_worker (map, caller_map, false);
 
   __rtld_lock_unlock_recursive (GL(dl_load_lock));
 }
diff --git a/elf/dl-libc.c b/elf/dl-libc.c
index 760cb955f7..faa8bcd893 100644
--- a/elf/dl-libc.c
+++ b/elf/dl-libc.c
@@ -67,6 +67,12 @@ struct do_dlopen_args
   struct link_map *map;
 };
 
+struct dl_dlclose_args
+{
+  void *handler;
+  void *caller;
+};
+
 struct do_dlsym_args
 {
   /* Arguments to do_dlsym.  */
@@ -122,7 +128,8 @@ do_dlvsym (void *ptr)
 static void
 do_dlclose (void *ptr)
 {
-  GLRO(dl_close) ((struct link_map *) ptr);
+  struct dl_dlclose_args *args = (struct dl_dlclose_args *) ptr;
+  GLRO(dl_close) (args->handler, args->caller);
 }
 
 #ifndef SHARED
@@ -224,5 +231,8 @@ __libc_dlclose (void *map)
   if (GLRO (dl_dlfcn_hook) != NULL)
     return GLRO (dl_dlfcn_hook)->libc_dlclose (map);
 #endif
-  return dlerror_run (do_dlclose, map);
+
+  return dlerror_run (do_dlclose, &(struct dl_dlclose_args) {
+				    .handler = map,
+				    .caller = RETURN_ADDRESS (0)});
 }
diff --git a/elf/dl-open.c b/elf/dl-open.c
index 5526065352..3fac3d6251 100644
--- a/elf/dl-open.c
+++ b/elf/dl-open.c
@@ -929,7 +929,7 @@ no more namespaces available for dlmopen()"));
 	 state if relocation failed, for example.  */
       if (args.map)
 	{
-	  _dl_close_worker (args.map, true);
+	  _dl_close_worker (args.map, caller_dlopen, true);
 
 	  /* All l_nodelete_pending objects should have been deleted
 	     at this point, which is why it is not necessary to reset
diff --git a/elf/rtld.c b/elf/rtld.c
index 5ea5383eb6..20df23d9ed 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -887,7 +887,7 @@ unload_audit_module (struct link_map *map, int original_tls_idx)
 #ifndef NDEBUG
   Lmid_t ns = map->l_ns;
 #endif
-  _dl_close (map);
+  _dl_close (map, dl_main);
 
   /* Make sure the namespace has been cleared entirely.  */
   assert (GL(dl_ns)[ns]._ns_loaded == NULL);
diff --git a/include/dlfcn.h b/include/dlfcn.h
index a44420fa37..0d82c8d855 100644
--- a/include/dlfcn.h
+++ b/include/dlfcn.h
@@ -68,10 +68,12 @@ extern int _dl_addr (const void *address, Dl_info *info,
 struct link_map;
 
 /* Close an object previously opened by _dl_open.  */
-extern void _dl_close (void *map) attribute_hidden;
+extern void _dl_close (void *map, void *dl_caller) attribute_hidden;
 /* Same as above, but without locking and safety checks for user
    provided map arguments.  */
-extern void _dl_close_worker (struct link_map *map, bool force)
+extern void _dl_close_worker (struct link_map *map,
+			      const struct link_map *caller,
+			      bool force)
     attribute_hidden;
 
 /* Look up NAME in shared object HANDLE (which may be RTLD_DEFAULT or
@@ -100,7 +102,7 @@ struct dlfcn_hook
 {
   /* Public interfaces.  */
   void *(*dlopen) (const char *file, int mode, void *dl_caller);
-  int (*dlclose) (void *handle);
+  int (*dlclose) (void *handle, void *dl_caller);
   void *(*dlsym) (void *handle, const char *name, void *dl_caller);
   void *(*dlvsym) (void *handle, const char *name, const char *version,
 		   void *dl_caller);
@@ -125,7 +127,7 @@ struct dlfcn_hook
 extern void *__dlopen (const char *file, int mode, void *caller);
 extern void *__dlmopen (Lmid_t nsid, const char *file, int mode,
 			void *dl_caller);
-extern int __dlclose (void *handle);
+extern int __dlclose (void *handle, void *caller);
 extern void *__dlsym (void *handle, const char *name, void *dl_caller);
 extern void *__dlvsym (void *handle, const char *name, const char *version,
 		       void *dl_caller);
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index cb318ade7b..d4cca1a556 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -648,7 +648,7 @@ struct rtld_global_ro
 				   struct link_map *);
   void *(*_dl_open) (const char *file, int mode, const void *caller_dlopen,
 		     Lmid_t nsid, int argc, char *argv[], char *env[]);
-  void (*_dl_close) (void *map);
+  void (*_dl_close) (void *map, void *caller_dlclose);
   /* libdl in a secondary namespace (after dlopen) must use
      _dl_catch_error from the main namespace, so it has to be
      exported in some way.  */
-- 
2.43.0



More information about the Libc-alpha mailing list