[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