[PATCH v7 4/4] elf: Add LD_DEBUG=loadlock tracing for the dlclose destructor path (BZ 15686)
temap@mail.ru
temap@mail.ru
Mon Aug 3 20:03:26 GMT 2026
From: Artem Proskurnev <temap@mail.ru>
This is the follow-up to the LD_DEBUG=loadlock diagnostic ("elf: Add
LD_DEBUG=loadlock to trace dl_load_lock acquisitions"): it extends coverage
to the dlclose destructor path, complementing the dlopen constructor path
instrumented by the earlier change. Together the two cover both halves of
the BZ #15686 deadlock class -- a destructor that (directly or via a spawned
thread) re-enters the loader and blocks on dl_load_lock is now as easy to
localise as the constructor case.
The DL_DEBUG_LOADLOCK mask bit and the _dl_debug_loadlock helper established
by the first commit are reused unchanged. This commit adds the per-file
inlined trace_load_lock helper to elf/dl-close.c (identical to the one in
dl-open.c) and calls it at every dl_load_lock acquire/release site reachable
from _dl_close:
* _dl_close entry acquire and the nodelete / not-open / done release
paths.
* The BZ #15686 release/reacquire around _dl_call_fini in
_dl_close_worker (the direct mirror of dl_open_worker's release around
the constructor), which is gated on the same
glibc.rtld.strict_init_order tunable.
As on the constructor side, the backtrace is a manual frame-pointer walk
with no symbol resolution, so the trace can be emitted while dl_load_lock
is held without re-entering the loader; raw addresses are resolved offline
with addr2line.
The exit-time finalizer path in elf/dl-fini.c also takes dl_load_lock, but
that path is outside the BZ #15686 dlclose-destructor deadlock class and is
left uninstrumented for now.
No behaviour changes; the option is purely diagnostic.
Signed-off-by: Artem Proskurnev <temap@mail.ru>
---
elf/dl-close.c | 25 +++++++++++++++++++++++--
elf/tst-debug-loadlock.c | 14 +++++++++-----
manual/dynlink.texi | 17 +++++++++--------
3 files changed, 41 insertions(+), 15 deletions(-)
diff --git a/elf/dl-close.c b/elf/dl-close.c
index 8e0b504a52..84e655fd7c 100644
--- a/elf/dl-close.c
+++ b/elf/dl-close.c
@@ -37,6 +37,17 @@
#include <dl-unmap-segments.h>
+/* When LD_DEBUG=loadlock is active, log a dl_load_lock acquire/release site
+ together with a raw-address backtrace (see _dl_debug_loadlock). Inlined so
+ the hot path costs only a single mask test when the flag is off. */
+static inline void
+trace_load_lock (const char *action, const char *site)
+{
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_LOADLOCK))
+ _dl_debug_loadlock (action, site);
+}
+
+
/* Special l_idx value used to indicate which objects remain loaded. */
#define IDX_STILL_USED -1
@@ -317,12 +328,18 @@ _dl_close_worker (struct link_map *map, bool force)
== 0);
if (release_lock_for_fini)
- __rtld_lock_unlock_recursive (GL (dl_load_lock));
+ {
+ __rtld_lock_unlock_recursive (GL (dl_load_lock));
+ trace_load_lock ("release", "_dl_close_worker(for-fini)");
+ }
_dl_catch_exception (NULL, _dl_call_fini, imap);
if (release_lock_for_fini)
- __rtld_lock_lock_recursive (GL (dl_load_lock));
+ {
+ __rtld_lock_lock_recursive (GL (dl_load_lock));
+ trace_load_lock ("acquire", "_dl_close_worker(for-fini)");
+ }
}
#ifdef SHARED
@@ -820,6 +837,7 @@ _dl_close (void *_map)
/* We must take the lock to examine the contents of map and avoid
concurrent dlopens. */
__rtld_lock_lock_recursive (GL(dl_load_lock));
+ trace_load_lock ("acquire", "_dl_close");
/* At this point we are guaranteed nobody else is touching the list of
loaded maps, but a concurrent dlclose might have freed our map
@@ -830,6 +848,7 @@ _dl_close (void *_map)
{
/* Nope. Do nothing. */
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "_dl_close(nodelete)");
return;
}
@@ -846,10 +865,12 @@ _dl_close (void *_map)
if (__builtin_expect (map->l_direct_opencount, 1) == 0)
{
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "_dl_close(not-open)");
_dl_signal_error (0, map->l_name, NULL, N_("shared object not open"));
}
_dl_close_worker (map, false);
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "_dl_close(done)");
}
diff --git a/elf/tst-debug-loadlock.c b/elf/tst-debug-loadlock.c
index 52623c097e..2ff72b4886 100644
--- a/elf/tst-debug-loadlock.c
+++ b/elf/tst-debug-loadlock.c
@@ -1,14 +1,15 @@
/* Test for LD_DEBUG=loadlock.
- Verifies that dl_load_lock acquisitions/releases on the dlopen constructor
- path are logged with a backtrace when LD_DEBUG=loadlock is active, and that
- "loadlock" appears in LD_DEBUG=help output.
+ Verifies that dl_load_lock acquisitions/releases around ELF constructor and
+ destructor execution (on the dlopen and dlclose paths) are logged with a
+ backtrace when LD_DEBUG=loadlock is active, and that "loadlock" appears in
+ LD_DEBUG=help output.
The dl_debug_mask is set by rtld only at process startup, so both checks
re-exec this binary as a child (under the freshly built ld.so, via
$(host-test-program-cmd) passed in tst-debug-loadlock-ARGS) with LD_DEBUG
set in the child environment, and capture the child's std streams. The
trace lines themselves are emitted by the dynamic linker (elf/dl-debug.c,
- elf/dl-open.c).
+ elf/dl-open.c, elf/dl-close.c).
Copyright (C) 2026 Free Software Foundation, Inc.
This file is part of the GNU C Library.
@@ -93,7 +94,8 @@ do_test (int argc, char *argv[])
}
/* Check 2: a real dlopen with LD_DEBUG=loadlock emits the dl_load_lock
- acquire trace for _dl_open and the BZ 15686 release-for-ctor site.
+ acquire trace for both _dl_open (constructor path) and _dl_close
+ (destructor path), plus the BZ 15686 release-for-ctor/for-fini sites.
Trace goes to stderr (dl_debug_fd defaults to STDERR_FILENO). */
setenv ("LD_DEBUG", "loadlock", 1);
{
@@ -109,6 +111,8 @@ do_test (int argc, char *argv[])
{
"dl_load_lock acquire at _dl_open",
"dl_open_worker(for-ctor)",
+ "dl_load_lock acquire at _dl_close",
+ "dl_close_worker(for-fini)",
};
for (int k = 0; k < (int) array_length (needles); k++)
if (strstr (p.err.buffer, needles[k]) == NULL)
diff --git a/manual/dynlink.texi b/manual/dynlink.texi
index 5569830f23..43d84a7417 100644
--- a/manual/dynlink.texi
+++ b/manual/dynlink.texi
@@ -408,15 +408,16 @@ Display relocation statistics.
Determined unused DSOs.
@item loadlock
-Log every acquisition and release of @code{dl_load_lock} on the @code{dlopen}
-constructor path, each followed by a raw return-address backtrace. Use this to
-diagnose deadlocks of the shape described in
+Log every acquisition and release of @code{dl_load_lock} around ELF constructor
+and destructor execution on the @code{dlopen} and @code{dlclose} paths, each
+followed by a raw return-address backtrace. Use this to diagnose deadlocks of
+the shape described in
@uref{https://sourceware.org/bugzilla/show_bug.cgi?id=15686, BZ 15686}, where
-code running inside an ELF constructor (or a thread it spawns) re-enters the
-dynamic linker and blocks on @code{dl_load_lock}. The backtrace lists raw code
-addresses; resolve them offline with @command{addr2line}. Frame pointers must
-be present in the code being traced (the default for the dynamic linker on most
-targets).
+code running inside an ELF constructor or destructor (or a thread it spawns)
+re-enters the dynamic linker and blocks on @code{dl_load_lock}. The backtrace
+lists raw code addresses; resolve them offline with @command{addr2line}. Frame
+pointers must be present in the code being traced (the default for the dynamic
+linker on most targets).
@item help
Display a help message with all available options and exit.
--
2.51.0
More information about the Libc-alpha
mailing list