@@ -432,6 +432,7 @@ tests += \
tst-big-note \
tst-bz26577 \
tst-bz26577-minstack \
+ tst-debug-loadlock \
tst-debug1 \
tst-deep1 \
tst-dl-is_dso \
@@ -959,6 +960,7 @@ modules-names += \
tst-auditmod9b \
tst-auxvalmod \
tst-big-note-lib \
+ tst-debug-loadlock-mod \
tst-deep1mod1 \
tst-deep1mod2 \
tst-deep1mod3 \
@@ -2891,6 +2893,9 @@ $(objpfx)tst-nodelete-dlclose.out: $(objpfx)tst-nodelete-dlclose-dso.so \
$(objpfx)tst-debug1.out: $(objpfx)tst-debug1mod1.so
+$(objpfx)tst-debug-loadlock.out: $(objpfx)tst-debug-loadlock-mod.so
+tst-debug-loadlock-ARGS = -- $(host-test-program-cmd)
+
$(objpfx)tst-debug1mod1.so: $(objpfx)testobj1.so
$(OBJCOPY) --only-keep-debug $< $@
@@ -167,3 +167,45 @@ _dl_debug_initialize (ElfW(Addr) ldbase, Lmid_t ns)
return &r->base;
}
+
+/* Log a dl_load_lock acquisition/release site together with a raw-address
+ backtrace. Called only when the DL_DEBUG_LOADLOCK mask is set.
+
+ The backtrace is obtained by walking the frame-pointer chain directly.
+ This is deliberately lock-free, allocation-free and syscall-free: we may
+ be running inside an _dl_load_lock critical section, so we cannot call
+ the libc backtrace() helper or perform any symbol resolution (both would
+ re-enter the dynamic linker and either recurse or deadlock against
+ dl_load_lock itself). Resolve the printed addresses offline with
+ addr2line(1). Frame pointers must be present in the code being traced
+ (the default for the dynamic linker on most targets). */
+void
+_dl_debug_loadlock (const char *action, const char *site)
+{
+ _dl_debug_printf ("dl_load_lock %s at %s\n", action, site);
+
+ struct layout
+ {
+ struct layout *next;
+ void *ret;
+ };
+ struct layout *p = (struct layout *) __builtin_frame_address (0);
+ /* Bound the frame-pointer walk to the current stack so a chain that runs
+ into frame-pointer-less code (e.g. -O2 libc, which does not set up rbp)
+ terminates instead of dereferencing garbage. The stack grows down, so
+ caller frames sit at strictly higher addresses: require a monotonic,
+ in-range, aligned advance. No symbol resolution is performed (it would
+ re-enter the loader and deadlock on dl_load_lock); resolve the printed
+ addresses offline with addr2line(1). */
+ uintptr_t start = (uintptr_t) p;
+ for (int n = 0; p != NULL && n < 32; ++n)
+ {
+ _dl_debug_printf (" #%d 0x%lx\n", n, (unsigned long int) p->ret);
+ uintptr_t naddr = (uintptr_t) p->next;
+ if (naddr <= (uintptr_t) p
+ || naddr - start > (1u << 20)
+ || (naddr & 7) != 0)
+ break;
+ p = p->next;
+ }
+}
@@ -43,6 +43,17 @@
#include <dl-prop.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);
+}
+
+
/* We must be careful not to leave us in an inconsistent state. Thus we
catch any error and re-raise it after cleaning up. */
@@ -644,6 +655,8 @@ dl_open_worker_begin (void *a)
{
__rtld_lock_unlock_recursive (GL(dl_load_tls_lock));
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release",
+ "dl_open_worker_begin(already-loaded)");
}
while (atomic_load_acquire (&new->l_init_once) != 2)
@@ -652,6 +665,8 @@ dl_open_worker_begin (void *a)
if (unlock_for_ctor)
{
__rtld_lock_lock_recursive (GL(dl_load_lock));
+ trace_load_lock ("acquire",
+ "dl_open_worker_begin(already-loaded)");
__rtld_lock_lock_recursive (GL(dl_load_tls_lock));
}
}
@@ -879,7 +894,10 @@ dl_open_worker (void *a)
atomic_store_release (&new->l_init_pending, 1);
if (release_lock_for_ctor)
- __rtld_lock_unlock_recursive (GL(dl_load_lock));
+ {
+ __rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "dl_open_worker(for-ctor)");
+ }
/* Run the initializer functions of new objects. Temporarily
disable the exception handler, so that lazy binding failures are
@@ -889,7 +907,10 @@ dl_open_worker (void *a)
/* Re-acquire dl_load_lock for the final global scope update and
for the lock/unlock pairing expected by _dl_open. */
if (release_lock_for_ctor)
- __rtld_lock_lock_recursive (GL(dl_load_lock));
+ {
+ __rtld_lock_lock_recursive (GL(dl_load_lock));
+ trace_load_lock ("acquire", "dl_open_worker(for-ctor)");
+ }
/* Now we can make the new map available in the global scope. */
if (mode & RTLD_GLOBAL)
@@ -911,6 +932,7 @@ _dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid,
/* Make sure we are alone. */
__rtld_lock_lock_recursive (GL(dl_load_lock));
+ trace_load_lock ("acquire", "_dl_open");
if (__glibc_unlikely (nsid == LM_ID_NEWLM))
{
@@ -923,6 +945,7 @@ _dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid,
{
/* No more namespace available. */
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "_dl_open(no-namespace)");
_dl_signal_error (EINVAL, file, NULL, N_("\
no more namespaces available for dlmopen()"));
@@ -1011,14 +1034,17 @@ no more namespaces available for dlmopen()"));
&& map->l_init_owner != THREAD_GETMEM (THREAD_SELF, tid))
{
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "_dl_open(fast-path wait)");
while (atomic_load_acquire (&map->l_init_once) != 2)
lll_futex_wait (&map->l_init_once, 1, LLL_PRIVATE);
__rtld_lock_lock_recursive (GL(dl_load_lock));
+ trace_load_lock ("acquire", "_dl_open(fast-path wait)");
}
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "_dl_open(fast-path return)");
return map;
}
@@ -1059,6 +1085,7 @@ no more namespaces available for dlmopen()"));
/* Release the lock. */
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "_dl_open(error cleanup)");
/* Reraise the error. */
_dl_signal_exception (errcode, &exception, NULL);
@@ -1070,6 +1097,7 @@ no more namespaces available for dlmopen()"));
/* Release the lock. */
__rtld_lock_unlock_recursive (GL(dl_load_lock));
+ trace_load_lock ("release", "_dl_open(done)");
return args.map;
}
@@ -2438,6 +2438,8 @@ process_dl_debug (struct dl_main_state *state, const char *dl_debug)
DL_DEBUG_STATISTICS },
{ LEN_AND_STR ("unused"), "determined unused DSOs",
DL_DEBUG_UNUSED },
+ { LEN_AND_STR ("loadlock"), "log dl_load_lock acquire/release sites",
+ DL_DEBUG_LOADLOCK },
{ LEN_AND_STR ("help"), "display this help message and exit",
DL_DEBUG_HELP },
};
new file mode 100644
@@ -0,0 +1,4 @@
+/* Loadable module for elf/tst-debug-loadlock.c.
+ No constructor is needed: the dl_load_lock acquire/release traces fire
+ on every dlopen regardless of whether the target runs initializers. */
+int tst_debug_loadlock_mod_variable = 1;
new file mode 100644
@@ -0,0 +1,127 @@
+/* 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.
+
+ 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).
+
+ Copyright (C) 2026 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 <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <array_length.h>
+#include <support/capture_subprocess.h>
+#include <support/check.h>
+#include <support/support.h>
+
+/* Child mode (--restart): perform the dlopen whose dl_load_lock sites we want
+ rtld to trace, then exit. */
+static int restart;
+#define CMDLINE_OPTIONS \
+ { "restart", no_argument, &restart, 1 },
+
+static int
+handle_restart (void)
+{
+ void *h = dlopen ("tst-debug-loadlock-mod.so", RTLD_LAZY);
+ if (h == NULL)
+ {
+ fprintf (stderr, "dlopen failed: %s\n", dlerror ());
+ _exit (1);
+ }
+ dlclose (h);
+ _exit (0);
+}
+
+static int
+do_test (int argc, char *argv[])
+{
+ if (restart)
+ return handle_restart ();
+
+ /* Re-exec ourselves under the freshly built ld.so. argv[1..] holds the
+ ld.so invocation prefix (the tokens of $(host-test-program-cmd)) supplied
+ via tst-debug-loadlock-ARGS; support_test_main has already stripped the
+ leading "--" separator used to terminate option parsing. */
+ char *spargv[argc + 2];
+ int i = 0;
+ for (; i < argc - 1; i++)
+ spargv[i] = argv[i + 1];
+ spargv[i] = NULL;
+
+ /* Check 1: LD_DEBUG=help lists "loadlock". rtld prints the table (via
+ _dl_printf, i.e. to stdout) and _exit()s before main runs, so the child
+ need not (and will not) reach handle_restart. */
+ setenv ("LD_DEBUG", "help", 1);
+ {
+ struct support_capture_subprocess p
+ = support_capture_subprogram (spargv[0], spargv, NULL);
+ support_capture_subprocess_check (&p, "tst-debug-loadlock (help)", 0,
+ sc_allow_stdout);
+ if (strstr (p.out.buffer, "loadlock") == NULL)
+ {
+ support_record_failure ();
+ printf ("LD_DEBUG=help stdout was:\n%s\n", p.out.buffer);
+ FAIL_EXIT1 ("'loadlock' missing from LD_DEBUG=help output");
+ }
+ support_capture_subprocess_free (&p);
+ }
+
+ /* 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.
+ Trace goes to stderr (dl_debug_fd defaults to STDERR_FILENO). */
+ setenv ("LD_DEBUG", "loadlock", 1);
+ {
+ spargv[i++] = (char *) "--restart";
+ spargv[i] = NULL;
+ struct support_capture_subprocess p
+ = support_capture_subprogram (spargv[0], spargv, NULL);
+ support_capture_subprocess_check (&p, "tst-debug-loadlock (loadlock)", 0,
+ sc_allow_stderr);
+ unsetenv ("LD_DEBUG");
+
+ static const char *const needles[] =
+ {
+ "dl_load_lock acquire at _dl_open",
+ "dl_open_worker(for-ctor)",
+ };
+ for (int k = 0; k < (int) array_length (needles); k++)
+ if (strstr (p.err.buffer, needles[k]) == NULL)
+ {
+ support_record_failure ();
+ printf ("LD_DEBUG=loadlock stderr was:\n%s\n", p.err.buffer);
+ FAIL_EXIT1 ("'%s' missing from trace", needles[k]);
+ }
+ support_capture_subprocess_free (&p);
+ }
+
+ return 0;
+}
+
+#define TEST_FUNCTION_ARGV do_test
+#include <support/test-driver.c>
@@ -407,6 +407,17 @@ Display relocation statistics.
@item unused
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
+@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).
+
@item help
Display a help message with all available options and exit.
@end table
@@ -533,6 +533,10 @@ struct rtld_global_ro
#define DL_DEBUG_HELP (1 << 10)
#define DL_DEBUG_TLS (1 << 11)
#define DL_DEBUG_SECURITY (1 << 12)
+/* Trace dl_load_lock acquisitions (with a raw-address backtrace) to help
+ diagnose deadlocks where code running inside a dlopen constructor (or a
+ thread it spawns) re-enters the dynamic linker. */
+#define DL_DEBUG_LOADLOCK (1 << 13)
/* Platform name. */
EXTERN const char *_dl_platform;
@@ -766,6 +770,14 @@ extern void _dl_debug_printf (const char *fmt, ...)
extern void _dl_debug_printf_c (const char *fmt, ...)
__attribute__ ((__format__ (__printf__, 1, 2))) attribute_hidden;
+/* Print "dl_load_lock <action> at <site>" followed by a raw-address
+ backtrace obtained by walking the frame-pointer chain. The caller must
+ have already checked the DL_DEBUG_LOADLOCK mask. No symbol resolution
+ is performed (it would re-enter the loader and deadlock on
+ dl_load_lock); resolve the printed addresses offline with addr2line. */
+extern void _dl_debug_loadlock (const char *action, const char *site)
+ attribute_hidden;
+
/* Write a message on the specified descriptor FD. The parameters are
interpreted as for a `printf' call. */