[PATCH v7 4/4] elf: Add LD_DEBUG=loadlock tracing for the dlclose destructor path (BZ 15686)
[email protected] Mon, 3 Aug 2026 23:03:26 +0300
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Message-ID | <[email protected]> |
From: Artem Proskurnev <[email protected]> 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 <[email protected]> --- 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