[PATCH v2] dlfcn: Fix dlclose crash during C++ thread_local destructor (BZ 33598)
Adhemerval Zanella
adhemerval.zanella@linaro.org
Fri Dec 12 20:27:49 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.
The most straightforward solution, which does not require any change
when the object is unmapped or add any extra complexity to dependency
tracking, is to assume that all libraries are in use at exit (similar
to the RTLD_NODELETE semantics). It is highly unlikely that a dlclose
at exit handler (either set automatically by the C++ runtime or via
atexit) relies on the DSO being unmapped.
This is accomplished with a new rtld_global variable, dl_at_exit, set
only in __run_exit_handlers. To avoid potential issues with malloc
debug, where __libc_freeres might not free some link_map data structures,
__libc_dlclose does not consider the variable value (which is
implemented via an extra flag).
Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu.
---
dlfcn/Makefile | 24 +++++++++++++
dlfcn/dlclose.c | 8 ++++-
dlfcn/tst-thrlocal-dlclose1-lib1.cc | 40 ++++++++++++++++++++++
dlfcn/tst-thrlocal-dlclose1-lib2.cc | 19 +++++++++++
dlfcn/tst-thrlocal-dlclose1.c | 29 ++++++++++++++++
dlfcn/tst-thrlocal-dlclose2-lib1.cc | 53 +++++++++++++++++++++++++++++
dlfcn/tst-thrlocal-dlclose2-lib2.c | 33 ++++++++++++++++++
dlfcn/tst-thrlocal-dlclose2.c | 29 ++++++++++++++++
elf/dl-close.c | 17 ++++++---
elf/dl-libc.c | 2 +-
elf/dl-open.c | 2 +-
elf/dl-support.c | 2 ++
elf/rtld.c | 2 +-
include/dlfcn.h | 4 +--
stdlib/exit.c | 4 +++
sysdeps/generic/ldsodefs.h | 5 ++-
16 files changed, 262 insertions(+), 11 deletions(-)
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 ab392f48df..0bf0a9d653 100644
--- a/dlfcn/Makefile
+++ b/dlfcn/Makefile
@@ -68,9 +68,18 @@ 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
+ # tests-unsupported
+endif
+
endif
modules-names = \
bug-atexit1-lib \
@@ -90,8 +99,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 +211,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-thread_local1-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-thread_local2-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/dlclose.c b/dlfcn/dlclose.c
index d0ca01f601..dbf887cd84 100644
--- a/dlfcn/dlclose.c
+++ b/dlfcn/dlclose.c
@@ -20,6 +20,12 @@
#include <ldsodefs.h>
#include <shlib-compat.h>
+static void
+dlclose_doit (void *handle)
+{
+ GLRO (dl_close) (handle, false);
+}
+
int
__dlclose (void *handle)
{
@@ -28,7 +34,7 @@ __dlclose (void *handle)
return GLRO (dl_dlfcn_hook)->dlclose (handle);
#endif
- return _dlerror_run (GLRO (dl_close), handle) ? -1 : 0;
+ return _dlerror_run (dlclose_doit, handle) ? -1 : 0;
}
versioned_symbol (libc, __dlclose, dlclose, GLIBC_2_34);
diff --git a/dlfcn/tst-thrlocal-dlclose1-lib1.cc b/dlfcn/tst-thrlocal-dlclose1-lib1.cc
new file mode 100644
index 0000000000..0e87d611a9
--- /dev/null
+++ b/dlfcn/tst-thrlocal-dlclose1-lib1.cc
@@ -0,0 +1,40 @@
+/* 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;
+
+ c1 ()
+ {
+ h = dlopen ("tst-thrlocal-dlclose1-lib2.so", RTLD_NOW);
+ assert (h != NULL);
+ tlp = std::make_unique<int>();
+ }
+
+ ~c1 ()
+ {
+ assert (dlclose (h) == 0);
+ }
+} c1;
diff --git a/dlfcn/tst-thrlocal-dlclose1-lib2.cc b/dlfcn/tst-thrlocal-dlclose1-lib2.cc
new file mode 100644
index 0000000000..88d0f767bf
--- /dev/null
+++ b/dlfcn/tst-thrlocal-dlclose1-lib2.cc
@@ -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-dlclose1.c b/dlfcn/tst-thrlocal-dlclose1.c
new file mode 100644
index 0000000000..602e4ac32c
--- /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 0000000000..c582f0037b
--- /dev/null
+++ b/dlfcn/tst-thrlocal-dlclose2-lib1.cc
@@ -0,0 +1,53 @@
+/* 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);
+
+ 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);
+
+ init ();
+
+ tlp = std::make_unique<int>();
+ }
+
+ ~c1 ()
+ {
+ cleanup ();
+ assert (dlclose (h) == 0);
+ }
+} c1;
diff --git a/dlfcn/tst-thrlocal-dlclose2-lib2.c b/dlfcn/tst-thrlocal-dlclose2-lib2.c
new file mode 100644
index 0000000000..ebb337f319
--- /dev/null
+++ b/dlfcn/tst-thrlocal-dlclose2-lib2.c
@@ -0,0 +1,33 @@
+/* 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;
+
+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 0000000000..ad1802df42
--- /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 83e4f012b2..52c5d0fdb2 100644
--- a/elf/dl-close.c
+++ b/elf/dl-close.c
@@ -106,7 +106,7 @@ 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, bool force, bool ignore_at_exit)
{
/* One less direct use. */
--map->l_direct_opencount;
@@ -139,6 +139,14 @@ _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];
+ /* Assume all objects are still in use during process exit to avoid
+ potential issues where an object is unmapped while still in use
+ (BZ 33598).
+ Also handle special cases where libc requires the object to be unmapped,
+ and not doing so would report a leak issue (__libc_freeres, used by
+ malloc trace). For this case, libc knows it is safe to unmap the
+ object. */
+ bool assume_in_use = ignore_at_exit || !GL(dl_at_exit);
/* Run over the list and assign indexes to the link maps and enter
them into the MAPS array. */
@@ -180,7 +188,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
+ && assume_in_use)
continue;
/* We need this object and we handle it now. */
@@ -754,7 +763,7 @@ _dl_close_worker (struct link_map *map, bool force)
void
-_dl_close (void *_map)
+_dl_close (void *_map, bool ignore_at_exit)
{
struct link_map *map = _map;
@@ -790,7 +799,7 @@ _dl_close (void *_map)
_dl_signal_error (0, map->l_name, NULL, N_("shared object not open"));
}
- _dl_close_worker (map, false);
+ _dl_close_worker (map, false, ignore_at_exit);
__rtld_lock_unlock_recursive (GL(dl_load_lock));
}
diff --git a/elf/dl-libc.c b/elf/dl-libc.c
index 760cb955f7..dd7ffa0d39 100644
--- a/elf/dl-libc.c
+++ b/elf/dl-libc.c
@@ -122,7 +122,7 @@ do_dlvsym (void *ptr)
static void
do_dlclose (void *ptr)
{
- GLRO(dl_close) ((struct link_map *) ptr);
+ GLRO(dl_close) ((struct link_map *) ptr, true);
}
#ifndef SHARED
diff --git a/elf/dl-open.c b/elf/dl-open.c
index 5526065352..9920cf2ae7 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, true, false);
/* 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/dl-support.c b/elf/dl-support.c
index f5b7550bf5..2696c8c0d0 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;
+bool _dl_at_exit = false;
+
#if !defined (__PTHREAD_HTL)
list_t _dl_stack_used;
list_t _dl_stack_user;
diff --git a/elf/rtld.c b/elf/rtld.c
index 5ea5383eb6..bdadbd0bcd 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, true);
/* 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..d5e45a94fa 100644
--- a/include/dlfcn.h
+++ b/include/dlfcn.h
@@ -68,10 +68,10 @@ 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, bool ignore_at_exit) 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, bool force, bool)
attribute_hidden;
/* Look up NAME in shared object HANDLE (which may be RTLD_DEFAULT or
diff --git a/stdlib/exit.c b/stdlib/exit.c
index 7decf58661..7fb5ad8a2b 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
@@ -46,6 +47,9 @@ __run_exit_handlers (int status, struct exit_function_list **listp,
/* The exit should never return, so there is no need to unlock it. */
__libc_lock_lock_recursive (__exit_lock);
+ /* Disable unmap objects through dlclose by TLS destructor (BZ 33598). */
+ GL(dl_at_exit) = true;
+
/* First, call the TLS destructors. */
if (run_dtors)
call_function_static_weak (__call_tls_dtors);
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 8c3541602f..d235fd127a 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -445,6 +445,9 @@ struct rtld_global
EXTERN void (*_dl_init_static_tls) (struct link_map *);
#endif
+ /* Disable unmap objects during __run_exit_handlers. */
+ EXTERN bool _dl_at_exit;
+
/* Scopes to free after next THREAD_GSCOPE_WAIT (). */
EXTERN struct dl_scope_free_list
{
@@ -648,7 +651,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, bool);
/* 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