Add LD_DEBUG=tls support for tracking TLS and TCB events (RHEL-49785)

Resolves: RHEL-49785
This commit is contained in:
Frédéric Bérat 2026-03-24 11:01:32 +01:00
parent d58e05d599
commit 0e8e4ecae9
6 changed files with 1465 additions and 0 deletions

56
glibc-RHEL-49785-1.patch Normal file
View File

@ -0,0 +1,56 @@
commit a0f9bfc3a5cc10920787d70d0653720a8fa013f3
Author: Adhemerval Zanella <adhemerval.zanella@linaro.org>
Date: Mon Nov 6 17:25:48 2023 -0300
elf: Remove any_debug from dl_main_state
Its usage can be implied by the GLRO(dl_debug_mask).
Reviewed-by: Siddhesh Poyarekar <siddhesh@sourceware.org>
Conflicts:
elf/dl-main.h
(fixup context)
diff --git a/elf/dl-main.h b/elf/dl-main.h
index d3820e0063143878..35728d47d463c3fb 100644
--- a/elf/dl-main.h
+++ b/elf/dl-main.h
@@ -94,9 +94,6 @@ struct dl_main_state
enum rtld_mode mode;
- /* True if any of the debugging options is enabled. */
- bool any_debug;
-
/* True if information about versions has to be printed. */
bool version_info;
};
diff --git a/elf/rtld.c b/elf/rtld.c
index b788857924e5a388..629ad7f8f4f75353 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -305,7 +305,6 @@ dl_main_state_init (struct dl_main_state *state)
state->glibc_hwcaps_prepend = NULL;
state->glibc_hwcaps_mask = NULL;
state->mode = rtld_mode_normal;
- state->any_debug = false;
state->version_info = false;
}
@@ -2707,7 +2706,6 @@ process_dl_debug (struct dl_main_state *state, const char *dl_debug)
&& memcmp (dl_debug, debopts[cnt].name, len) == 0)
{
GLRO(dl_debug_mask) |= debopts[cnt].mask;
- state->any_debug = true;
break;
}
@@ -2954,7 +2952,7 @@ process_envvars (struct dl_main_state *state)
/* If we have to run the dynamic linker in debugging mode and the
LD_DEBUG_OUTPUT environment variable is given, we write the debug
messages to this file. */
- else if (state->any_debug && debug_output != NULL)
+ else if (GLRO(dl_debug_mask) != 0 && debug_output != NULL)
{
const int flags = O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW;
size_t name_len = strlen (debug_output);

374
glibc-RHEL-49785-2.patch Normal file
View File

@ -0,0 +1,374 @@
commit 332f8e62afef53492dd8285490bcf7aeef18c80a
Author: Frédéric Bérat <fberat@redhat.com>
Date: Fri Sep 5 16:14:38 2025 +0200
tls: Add debug logging for TLS and TCB management
Introduce the `DL_DEBUG_TLS` debug mask to enable detailed logging for
Thread-Local Storage (TLS) and Thread Control Block (TCB) management.
This change integrates a new `tls` option into the `LD_DEBUG`
environment variable, allowing developers to trace:
- TCB allocation, deallocation, and reuse events in `dl-tls.c`,
`nptl/allocatestack.c`, and `nptl/nptl-stack.c`.
- Thread startup events, including the TID and TCB address, in
`nptl/pthread_create.c`.
A new test, `tst-dl-debug-tid`, has been added to validate the
functionality of this new debug logging, ensuring that relevant messages
are correctly generated for both main and worker threads.
This enhances the debugging capabilities for diagnosing issues related
to TLS allocation and thread lifecycle within the dynamic linker.
Reviewed-by: DJ Delorie <dj@redhat.com>
Conflicts:
sysdeps/generic/ldsodefs.h
(PRELINK is still there downstream)
diff --git a/elf/dl-tls.c b/elf/dl-tls.c
index 1b1bc4292eb24747..20d6036b891f653c 100644
--- a/elf/dl-tls.c
+++ b/elf/dl-tls.c
@@ -503,6 +503,8 @@ _dl_allocate_tls_storage (void)
result = allocate_dtv (result);
if (result == NULL)
free (allocated);
+ else if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("TCB allocated: 0x%lx\n", (unsigned long int) result);
_dl_tls_allocate_end ();
return result;
@@ -691,6 +693,10 @@ rtld_hidden_def (_dl_allocate_tls)
void
_dl_deallocate_tls (void *tcb, bool dealloc_tcb)
{
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("TCB deallocating: 0x%lx (dealloc_tcb=%d)\n",
+ (unsigned long int) tcb, dealloc_tcb);
+
dtv_t *dtv = GET_DTV (tcb);
/* We need to free the memory allocated for non-static TLS. */
diff --git a/elf/rtld.c b/elf/rtld.c
index 629ad7f8f4f75353..ff4c160d8fafd70d 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -2676,10 +2676,12 @@ process_dl_debug (struct dl_main_state *state, const char *dl_debug)
DL_DEBUG_VERSIONS | DL_DEBUG_IMPCALLS },
{ LEN_AND_STR ("scopes"), "display scope information",
DL_DEBUG_SCOPES },
+ { LEN_AND_STR ("tls"), "display TLS structures processing",
+ DL_DEBUG_TLS },
{ LEN_AND_STR ("all"), "all previous options combined",
DL_DEBUG_LIBS | DL_DEBUG_RELOC | DL_DEBUG_FILES | DL_DEBUG_SYMBOLS
| DL_DEBUG_BINDINGS | DL_DEBUG_VERSIONS | DL_DEBUG_IMPCALLS
- | DL_DEBUG_SCOPES },
+ | DL_DEBUG_SCOPES | DL_DEBUG_TLS },
{ LEN_AND_STR ("statistics"), "display relocation statistics",
DL_DEBUG_STATISTICS },
{ LEN_AND_STR ("unused"), "determined unused DSOs",
diff --git a/nptl/Makefile b/nptl/Makefile
index 80c7587f0086677b..7001ecd17b752dba 100644
--- a/nptl/Makefile
+++ b/nptl/Makefile
@@ -374,6 +374,7 @@ tests-container = tst-pthread-getattr
tests-internal := \
tst-barrier5 \
tst-cond22 \
+ tst-dl-debug-tid \
tst-mutex8 \
tst-mutex8-static \
tst-mutexpi8 \
@@ -562,6 +563,7 @@ xtests-static += tst-setuid1-static
ifeq ($(run-built-tests),yes)
tests-special += \
+ $(objpfx)tst-dl-debug-tid.out \
$(objpfx)tst-oddstacklimit.out \
$(objpfx)tst-stack3-mem.out \
# tests-special
@@ -685,6 +687,11 @@ tst-stackguard1-ARGS = --command "$(host-test-program-cmd) --child"
tst-stackguard1-static-ARGS = --command "$(objpfx)tst-stackguard1-static --child"
ifeq ($(run-built-tests),yes)
+$(objpfx)tst-dl-debug-tid.out: tst-dl-debug-tid.sh $(objpfx)tst-dl-debug-tid
+ $(SHELL) $< $(common-objpfx) '$(test-wrapper)' '$(rtld-prefix)' \
+ '$(test-wrapper-env)' '$(run-program-env)' \
+ $(objpfx)tst-dl-debug-tid > $@; $(evaluate-test)
+
$(objpfx)tst-oddstacklimit.out: $(objpfx)tst-oddstacklimit $(objpfx)tst-basic1
$(test-program-prefix) $< --command '$(host-test-program-cmd)' > $@; \
$(evaluate-test)
diff --git a/nptl/allocatestack.c b/nptl/allocatestack.c
index b0f40afd3f5801f6..9e0bb8f0eeff8e88 100644
--- a/nptl/allocatestack.c
+++ b/nptl/allocatestack.c
@@ -114,6 +114,10 @@ get_cached_stack (size_t *sizep, void **memp)
/* Release the lock early. */
lll_unlock (GL (dl_stack_cache_lock), LLL_PRIVATE);
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ GLRO (dl_debug_printf) ("TLS TCB reused from cache: 0x%lx\n",
+ (unsigned long int) result);
+
/* Report size and location of the stack to the caller. */
*sizep = result->stackblock_size;
*memp = result->stackblock;
@@ -291,6 +295,12 @@ allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
stack cache nor will the memory (except the TLS memory) be freed. */
pd->user_stack = true;
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ GLRO (dl_debug_printf) (
+ "TCB for user-supplied stack created: 0x%lx, stack=0x%lx, size=%lu\n",
+ (unsigned long int) pd, (unsigned long int) pd->stackblock,
+ (unsigned long int) pd->stackblock_size);
+
/* This is at least the second thread. */
pd->header.multiple_threads = 1;
#ifndef TLS_MULTIPLE_THREADS_IN_TCB
@@ -425,6 +435,10 @@ allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
/* Don't allow setxid until cloned. */
pd->setxid_futex = -1;
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ GLRO (dl_debug_printf) ("TCB for new stack allocated: 0x%lx\n",
+ (unsigned long int) pd);
+
/* Allocate the DTV for this thread. */
if (_dl_allocate_tls (TLS_TPADJ (pd)) == NULL)
{
diff --git a/nptl/nptl-stack.c b/nptl/nptl-stack.c
index 3f33a4c20b39a4aa..4ef6527ff7e8192b 100644
--- a/nptl/nptl-stack.c
+++ b/nptl/nptl-stack.c
@@ -75,6 +75,11 @@ __nptl_free_stacks (size_t limit)
/* Account for the freed memory. */
GL (dl_stack_cache_actsize) -= curr->stackblock_size;
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ GLRO (dl_debug_printf) (
+ "TCB cache full, deallocating: TID=%ld, TCB=0x%lx\n",
+ (long int) curr->tid, (unsigned long int) curr);
+
/* Free the memory associated with the ELF TLS. */
_dl_deallocate_tls (TLS_TPADJ (curr), false);
@@ -96,6 +101,12 @@ static inline void
__attribute ((always_inline))
queue_stack (struct pthread *stack)
{
+ /* The 'stack' parameter is a pointer to the TCB (struct pthread),
+ not just the stack. */
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ GLRO (dl_debug_printf) ("TCB deallocated into cache: TID=%ld, TCB=0x%lx\n",
+ (long int) stack->tid, (unsigned long int) stack);
+
/* We unconditionally add the stack to the list. The memory may
still be in use but it will not be reused until the kernel marks
the stack as not used anymore. */
@@ -123,8 +134,16 @@ __nptl_deallocate_stack (struct pthread *pd)
if (__glibc_likely (! pd->user_stack))
(void) queue_stack (pd);
else
- /* Free the memory associated with the ELF TLS. */
- _dl_deallocate_tls (TLS_TPADJ (pd), false);
+ {
+ /* User-provided stack. We must not free it. But we must free
+ the TLS memory. */
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ GLRO (dl_debug_printf) (
+ "TCB for user-supplied stack deallocated: TID=%ld, TCB=0x%lx\n",
+ (long int) pd->tid, (unsigned long int) pd);
+ /* Free the memory associated with the ELF TLS. */
+ _dl_deallocate_tls (TLS_TPADJ (pd), false);
+ }
lll_unlock (GL (dl_stack_cache_lock), LLL_PRIVATE);
}
diff --git a/nptl/pthread_create.c b/nptl/pthread_create.c
index 109c5e3dc78c9aa2..37de54eaa570cedb 100644
--- a/nptl/pthread_create.c
+++ b/nptl/pthread_create.c
@@ -362,6 +362,10 @@ start_thread (void *arg)
goto out;
}
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ GLRO (dl_debug_printf) ("Thread starting: TID=%ld, TCB=0x%lx\n",
+ (long int) pd->tid, (unsigned long int) pd);
+
/* Initialize resolver state pointer. */
__resp = &pd->res;
diff --git a/nptl/tst-dl-debug-tid.c b/nptl/tst-dl-debug-tid.c
new file mode 100644
index 0000000000000000..231fa43516b233b3
--- /dev/null
+++ b/nptl/tst-dl-debug-tid.c
@@ -0,0 +1,69 @@
+/* Test for thread ID logging in dynamic linker.
+ 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/>. */
+
+/* This test checks that the dynamic linker correctly logs thread creation
+ and destruction. It creates a detached thread followed by a joinable
+ thread to exercise different code paths. A barrier is used to ensure
+ the detached thread has started before the joinable one is created,
+ making the test more deterministic. The tst-dl-debug-tid.sh shell script
+ wrapper then verifies the LD_DEBUG output. */
+
+#include <pthread.h>
+#include <support/xthread.h>
+#include <stdio.h>
+#include <unistd.h>
+
+static void *
+thread_function (void *arg)
+{
+ if (arg)
+ pthread_barrier_wait ((pthread_barrier_t *) arg);
+ return NULL;
+}
+
+static int
+do_test (void)
+{
+ pthread_t thread1;
+ pthread_attr_t attr;
+ pthread_barrier_t barrier;
+
+ pthread_barrier_init (&barrier, NULL, 2);
+
+ /* A detached thread.
+ * Deallocation is done by the thread itself upon exit. */
+ xpthread_attr_init (&attr);
+ xpthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
+ /* We don't need the thread handle for the detached thread. */
+ xpthread_create (&attr, thread_function, &barrier);
+ xpthread_attr_destroy (&attr);
+
+ /* Wait for the detached thread to be executed. */
+ pthread_barrier_wait (&barrier);
+ pthread_barrier_destroy (&barrier);
+
+ /* A joinable thread.
+ * Deallocation is done by the main thread in pthread_join. */
+ thread1 = xpthread_create (NULL, thread_function, NULL);
+
+ xpthread_join (thread1);
+
+ return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/nptl/tst-dl-debug-tid.sh b/nptl/tst-dl-debug-tid.sh
new file mode 100644
index 0000000000000000..93d27134a09eaca7
--- /dev/null
+++ b/nptl/tst-dl-debug-tid.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+# Test for thread ID logging in dynamic linker.
+# 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/>.
+
+# This script runs the tst-dl-debug-tid test case and verifies its
+# LD_DEBUG output. It checks for thread creation/destruction messages
+# to ensure the dynamic linker's thread-aware logging is working.
+
+set -e
+
+# Arguments are from Makefile.
+common_objpfx="$1"
+test_wrapper="$2"
+rtld_prefix="$3"
+test_wrapper_env="$4"
+run_program_env="$5"
+test_program="$6"
+
+debug_output="${common_objpfx}elf/tst-dl-debug-tid.debug"
+rm -f "${debug_output}".*
+
+# Run the test program with LD_DEBUG=tls.
+eval "${test_wrapper_env}" LD_DEBUG=tls LD_DEBUG_OUTPUT="${debug_output}" \
+ "${test_wrapper}" "${rtld_prefix}" "${test_program}"
+
+debug_output=$(ls "${debug_output}".*)
+# Check for the "Thread starting" message.
+if ! grep -q 'Thread starting: TID=' "${debug_output}"; then
+ echo "error: 'Thread starting' message not found"
+ cat "${debug_output}"
+ exit 1
+fi
+
+# Check that we have a message where the PID (from prefix) is different
+# from the TID (in the message). This indicates a worker thread log.
+if ! grep 'Thread starting: TID=' "${debug_output}" | awk -F '[ \t:]+' '{
+ sub(/,/, "", $5);
+ sub(/TID=/, "", $5);
+ if ($1 != $5)
+ exit 0;
+ exit 1
+}'; then
+ echo "error: No 'Thread starting' message from a worker thread found"
+ cat "${debug_output}"
+ exit 1
+fi
+
+# We expect messages from thread creation and destruction.
+if ! grep -q 'TCB allocated\|TCB deallocating\|TCB reused\|TCB deallocated' \
+ "${debug_output}"; then
+ echo "error: Expected TCB allocation/deallocation message not found"
+ cat "${debug_output}"
+ exit 1
+fi
+
+cat "${debug_output}"
+rm -f "${debug_output}"
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index e5860916345487e7..5080db9381c31c4f 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -560,9 +560,10 @@ struct rtld_global_ro
#define DL_DEBUG_STATISTICS (1 << 7)
#define DL_DEBUG_UNUSED (1 << 8)
#define DL_DEBUG_SCOPES (1 << 9)
-/* These two are used only internally. */
+/* DL_DEBUG_HELP is only used internally. */
#define DL_DEBUG_HELP (1 << 10)
#define DL_DEBUG_PRELINK (1 << 11)
+#define DL_DEBUG_TLS (1 << 12)
/* OS version. */
EXTERN unsigned int _dl_osversion;

107
glibc-RHEL-49785-3.patch Normal file
View File

@ -0,0 +1,107 @@
commit d4d472366ba69df7b14eba22a75f887b99855d70
Author: Frédéric Bérat <fberat@redhat.com>
Date: Thu Oct 2 19:15:29 2025 +0200
docs: Add dynamic linker environment variable docs
The Dynamic Linker chapter now includes a new section detailing
environment variables that influence its behavior.
This new section documents the `LD_DEBUG` environment variable,
explaining how to enable debugging output and listing its various
keywords like `libs`, `reloc`, `files`, `symbols`, `bindings`,
`versions`, `scopes`, `tls`, `all`, `statistics`, `unused`, and `help`.
It also documents `LD_DEBUG_OUTPUT`, which controls where the debug
output is written, allowing redirection to a file with the process ID
appended.
This provides users with essential information for controlling and
debugging the dynamic linker.
Reviewed-by: DJ Delorie <dj@redhat.com>
diff --git a/manual/dynlink.texi b/manual/dynlink.texi
index 2b35e5883c3e65c0..469340e1d4ae8a88 100644
--- a/manual/dynlink.texi
+++ b/manual/dynlink.texi
@@ -14,6 +14,8 @@ Dynamic linkers are sometimes called @dfn{dynamic loaders}.
@menu
* Dynamic Linker Invocation:: Explicit invocation of the dynamic linker.
+* Dynamic Linker Environment Variables:: Environment variables that control the
+ dynamic linker.
* Dynamic Linker Introspection:: Interfaces for querying mapping information.
@end menu
@@ -348,6 +350,70 @@ probed CPU are omitted. Nothing is printed if the system does not
support the XGETBV instruction.
@end table
+@node Dynamic Linker Environment Variables
+@section Dynamic Linker Environment Variables
+
+The behavior of the dynamic linker can be modified through various environment
+variables.
+
+@table @code
+@item LD_DEBUG
+@cindex @code{LD_DEBUG} environment variable
+The @env{LD_DEBUG} environment variable can be set to a comma-separated list
+of keywords to enable debugging output from the dynamic linker. Setting it to
+@code{help} will display a list of all available keywords. The output is
+written to standard output by default.
+
+@table @code
+@item libs
+Display library search paths.
+
+@item reloc
+Display relocation processing.
+
+@item files
+Display progress for input file processing.
+
+@item symbols
+Display symbol table processing.
+
+@item bindings
+Display information about symbol binding.
+
+@item versions
+Display version dependencies.
+
+@item scopes
+Display scope information.
+
+@item tls
+Display information about Thread-Local Storage (TLS) handling, including TCB
+allocation, deallocation, and reuse. This is useful for debugging issues
+related to thread creation and lifecycle.
+
+@item all
+All previous options combined.
+
+@item statistics
+Display relocation statistics.
+
+@item unused
+Determined unused DSOs.
+
+@item help
+Display a help message with all available options and exit.
+@end table
+
+@item LD_DEBUG_OUTPUT
+@cindex @code{LD_DEBUG_OUTPUT} environment variable
+If @env{LD_DEBUG} is set, the output is written to standard output by
+default. If @env{LD_DEBUG_OUTPUT} is set, the output is written to the
+file specified by its value, with the process ID appended. For example, if
+@env{LD_DEBUG_OUTPUT} is set to @file{/tmp/glibc.debug}, the output will be
+written to a file named @file{/tmp/glibc.debug.12345}, where @code{12345} is
+the process ID.
+@end table
+
@node Dynamic Linker Introspection
@section Dynamic Linker Introspection

70
glibc-RHEL-49785-4.patch Normal file
View File

@ -0,0 +1,70 @@
commit 20092f2ef601aef57cc184cbacd7cab39bba5a25
Author: Yury Khrustalev <yury.khrustalev@arm.com>
Date: Mon Dec 1 10:09:14 2025 +0000
nptl: tests: Fix test-wrapper use in tst-dl-debug-tid.sh
Test wrapper script was used twice: once to run the test
command and second time within the text command which
seems unnecessary and results in false errors when running
this test.
Fixes 332f8e62afef53492dd8285490bcf7aeef18c80a
Reviewed-by: Frédéric Bérat <fberat@redhat.com>
diff --git a/nptl/Makefile b/nptl/Makefile
index 7001ecd17b752dba..597311bf07223c4f 100644
--- a/nptl/Makefile
+++ b/nptl/Makefile
@@ -688,8 +688,8 @@ tst-stackguard1-static-ARGS = --command "$(objpfx)tst-stackguard1-static --child
ifeq ($(run-built-tests),yes)
$(objpfx)tst-dl-debug-tid.out: tst-dl-debug-tid.sh $(objpfx)tst-dl-debug-tid
- $(SHELL) $< $(common-objpfx) '$(test-wrapper)' '$(rtld-prefix)' \
- '$(test-wrapper-env)' '$(run-program-env)' \
+ $(SHELL) $< $(common-objpfx) '$(test-wrapper-env)' '$(rtld-prefix)' \
+ '$(run-program-env)' \
$(objpfx)tst-dl-debug-tid > $@; $(evaluate-test)
$(objpfx)tst-oddstacklimit.out: $(objpfx)tst-oddstacklimit $(objpfx)tst-basic1
diff --git a/nptl/tst-dl-debug-tid.sh b/nptl/tst-dl-debug-tid.sh
index 93d27134a09eaca7..9ee31ac4f241f0b4 100644
--- a/nptl/tst-dl-debug-tid.sh
+++ b/nptl/tst-dl-debug-tid.sh
@@ -25,18 +25,17 @@ set -e
# Arguments are from Makefile.
common_objpfx="$1"
-test_wrapper="$2"
+test_wrapper_env="$2"
rtld_prefix="$3"
-test_wrapper_env="$4"
-run_program_env="$5"
-test_program="$6"
+run_program_env="$4"
+test_program="$5"
debug_output="${common_objpfx}elf/tst-dl-debug-tid.debug"
rm -f "${debug_output}".*
# Run the test program with LD_DEBUG=tls.
eval "${test_wrapper_env}" LD_DEBUG=tls LD_DEBUG_OUTPUT="${debug_output}" \
- "${test_wrapper}" "${rtld_prefix}" "${test_program}"
+ "${rtld_prefix}" "${test_program}"
debug_output=$(ls "${debug_output}".*)
# Check for the "Thread starting" message.
@@ -49,9 +48,9 @@ fi
# Check that we have a message where the PID (from prefix) is different
# from the TID (in the message). This indicates a worker thread log.
if ! grep 'Thread starting: TID=' "${debug_output}" | awk -F '[ \t:]+' '{
- sub(/,/, "", $5);
- sub(/TID=/, "", $5);
- if ($1 != $5)
+ sub(/,/, "", $4);
+ sub(/TID=/, "", $4);
+ if ($1 != $4)
exit 0;
exit 1
}'; then

681
glibc-RHEL-49785-5.patch Normal file
View File

@ -0,0 +1,681 @@
commit 1e47dbcce4d5cf2ce71377e729014e454a3e15ae
Author: Frédéric Bérat <fberat@redhat.com>
Date: Fri Dec 12 16:19:43 2025 +0100
elf(tls): Add debug logging for TLS operations
This commit introduces extensive debug logging for thread-local storage
(TLS) operations within the dynamic linker. When `LD_DEBUG=tls` is
enabled, messages are printed for:
- TLS module assignment and release.
- DTV (Dynamic Thread Vector) resizing events.
- TLS block allocations and deallocations.
- `__tls_get_addr` slow path events (DTV updates, lazy allocations, and
static TLS usage).
The log format is standardized to use a "tls: " prefix and identifies
modules using the "modid %lu" convention. To aid in debugging
multithreaded applications, thread-specific logs include the Thread
Control Block (TCB) address to identify the context of the operation.
A new test module `tst-tls-debug-mod.c` and a corresponding shell script
`tst-tls-debug-recursive.sh` have been added. Additionally, the existing
`tst-dl-debug-tid` NPTL test has been updated to verify these TLS debug
messages in a multithreaded context.
Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
Conflicts:
elf/Makefile
(fixup context)
elf/dl-tls.c
(missing 5e249192cac7354af02a7347a0d8c984e0c88ed3 downstream)
sysdeps/x86_64/dl-tls.c
(missing 5e249192cac7354af02a7347a0d8c984e0c88ed3 downstream)
diff --git a/elf/Makefile b/elf/Makefile
index bd12a20435538fdd..2431e7032c44beb8 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -1252,6 +1252,7 @@ $(objpfx)tst-glibcelf.out: tst-glibcelf.py elf.h $(..)/scripts/glibcelf.py \
ifeq ($(run-built-tests),yes)
tests-special += $(objpfx)tst-tls-allocation-failure-static-patched.out
+tests-special += $(objpfx)tst-tls-debug-recursive.out
endif
# The test requires shared _and_ PIE because the executable
@@ -3052,3 +3053,15 @@ LDFLAGS-tst-version-hash-zero-linkmod.so = \
$(objpfx)tst-version-hash-zero-refmod.so: \
$(objpfx)tst-version-hash-zero-linkmod.so
tst-version-hash-zero-refmod.so-no-z-defs = yes
+
+ifeq ($(run-built-tests),yes)
+$(objpfx)tst-tls-debug-recursive.out: tst-tls-debug-recursive.sh \
+ $(objpfx)tst-recursive-tls \
+ $(objpfx)tst-recursive-tlsmallocmod.so \
+ $(patsubst %,$(objpfx)tst-recursive-tlsmod%.so, \
+ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15)
+ $(SHELL) $< $(common-objpfx) '$(test-wrapper-env)' \
+ '$(rtld-prefix)' '$(run_program_env)' \
+ $(objpfx)tst-recursive-tls > $@; \
+ $(evaluate-test)
+endif
diff --git a/elf/dl-close.c b/elf/dl-close.c
index 4514c53d2db261c1..8020b0b182d66f4f 100644
--- a/elf/dl-close.c
+++ b/elf/dl-close.c
@@ -79,6 +79,11 @@ remove_slotinfo (size_t idx, struct dtv_slotinfo_list *listp, size_t disp,
if (__glibc_likely (old_map != NULL))
{
/* Mark the entry as unused. These can be read concurrently. */
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf (
+ "tls: release modid %lu from %s [%ld]\n",
+ (unsigned long int) idx, DSO_FILENAME (old_map->l_name),
+ (long int) old_map->l_ns);
atomic_store_relaxed (&listp->slotinfo[idx - disp].gen,
GL(dl_tls_generation) + 1);
atomic_store_relaxed (&listp->slotinfo[idx - disp].map, NULL);
diff --git a/elf/dl-tls.c b/elf/dl-tls.c
index 20d6036b891f653c..f22d828a86539057 100644
--- a/elf/dl-tls.c
+++ b/elf/dl-tls.c
@@ -215,6 +215,12 @@ _dl_assign_tls_modid (struct link_map *l)
}
l->l_tls_modid = result;
+
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: assign modid %lu to %s [%ld]\n",
+ (unsigned long int) result,
+ DSO_FILENAME (l->l_name),
+ (long int) l->l_ns);
}
@@ -504,7 +510,7 @@ _dl_allocate_tls_storage (void)
if (result == NULL)
free (allocated);
else if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
- _dl_debug_printf ("TCB allocated: 0x%lx\n", (unsigned long int) result);
+ _dl_debug_printf ("tls: allocate TCB 0x%lx\n", (unsigned long int) result);
_dl_tls_allocate_end ();
return result;
@@ -517,13 +523,18 @@ extern dtv_t _dl_static_dtv[];
#endif
static dtv_t *
-_dl_resize_dtv (dtv_t *dtv, size_t max_modid)
+_dl_resize_dtv (dtv_t *dtv, size_t max_modid, void *tcb)
{
/* Resize the dtv. */
dtv_t *newp;
size_t newsize = max_modid + DTV_SURPLUS;
size_t oldsize = dtv[-1].counter;
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: DTV resized for TCB 0x%lx: oldsize=%lu, newsize=%lu\n",
+ (unsigned long int) tcb,
+ (unsigned long int) oldsize, (unsigned long int) newsize);
+
_dl_tls_allocate_begin ();
if (dtv == GL(dl_initial_dtv))
{
@@ -592,7 +603,7 @@ _dl_allocate_tls_init (void *result, bool main_thread)
if (dtv[-1].counter < GL(dl_tls_max_dtv_idx))
{
/* Resize the dtv. */
- dtv = _dl_resize_dtv (dtv, GL(dl_tls_max_dtv_idx));
+ dtv = _dl_resize_dtv (dtv, GL(dl_tls_max_dtv_idx), result);
/* Install this new dtv in the thread data structures. */
INSTALL_DTV (result, &dtv[-1]);
@@ -683,9 +694,14 @@ rtld_hidden_def (_dl_allocate_tls_init)
void *
_dl_allocate_tls (void *mem)
{
- return _dl_allocate_tls_init (mem == NULL
- ? _dl_allocate_tls_storage ()
- : allocate_dtv (mem), false);
+ void *result = _dl_allocate_tls_init (mem == NULL
+ ? _dl_allocate_tls_storage ()
+ : allocate_dtv (mem), false);
+ if (__glibc_unlikely (result != NULL
+ && (GLRO (dl_debug_mask) & DL_DEBUG_TLS)))
+ _dl_debug_printf ("tls: TLS initialized for TCB 0x%lx\n",
+ (unsigned long int) result);
+ return result;
}
rtld_hidden_def (_dl_allocate_tls)
@@ -694,14 +710,22 @@ void
_dl_deallocate_tls (void *tcb, bool dealloc_tcb)
{
if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
- _dl_debug_printf ("TCB deallocating: 0x%lx (dealloc_tcb=%d)\n",
+ _dl_debug_printf ("tls: deallocate TCB 0x%lx (dealloc_tcb=%d)\n",
(unsigned long int) tcb, dealloc_tcb);
dtv_t *dtv = GET_DTV (tcb);
/* We need to free the memory allocated for non-static TLS. */
for (size_t cnt = 0; cnt < dtv[-1].counter; ++cnt)
- free (dtv[1 + cnt].pointer.to_free);
+ {
+ if (dtv[1 + cnt].pointer.to_free != NULL
+ && __glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf (
+ "tls: deallocate block 0x%lx for modid %lu; TCB=0x%lx\n",
+ (unsigned long int) dtv[1 + cnt].pointer.to_free,
+ (unsigned long int) (1 + cnt), (unsigned long int) tcb);
+ free (dtv[1 + cnt].pointer.to_free);
+ }
/* The array starts with dtv[-1]. */
if (dtv != GL(dl_initial_dtv))
@@ -773,6 +797,12 @@ allocate_and_init (struct link_map *map)
(map->l_tls_align, map->l_tls_blocksize);
if (result.val == NULL)
oom ();
+ else if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: allocate block 0x%lx for modid %lu; size=%lu, TCB=0x%lx\n",
+ (unsigned long int) result.to_free,
+ (unsigned long int) map->l_tls_modid,
+ (unsigned long int) map->l_tls_blocksize,
+ (unsigned long int) THREAD_SELF);
/* Initialize the memory. */
memset (__mempcpy (result.val, map->l_tls_initimage,
@@ -874,7 +904,7 @@ _dl_update_slotinfo (unsigned long int req_modid, size_t new_gen)
continue;
/* Resizing the dtv aborts on failure: bug 16134. */
- dtv = _dl_resize_dtv (dtv, max_modid);
+ dtv = _dl_resize_dtv (dtv, max_modid, THREAD_SELF);
assert (modid <= dtv[-1].counter);
@@ -895,6 +925,12 @@ _dl_update_slotinfo (unsigned long int req_modid, size_t new_gen)
least some dynamic TLS usage by interposed mallocs. */
if (dtv[modid].pointer.to_free != NULL)
{
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf (
+ "tls: DTV update for TCB 0x%lx: modid %lu deallocated block 0x%lx\n",
+ (unsigned long int) THREAD_SELF,
+ (unsigned long int) modid,
+ (unsigned long int) dtv[modid].pointer.to_free);
_dl_tls_allocate_begin ();
free (dtv[modid].pointer.to_free);
_dl_tls_allocate_end ();
@@ -975,6 +1011,11 @@ tls_get_addr_tail (GET_ADDR_ARGS, dtv_t *dtv, struct link_map *the_map)
dtv[GET_ADDR_MODULE].pointer.to_free = NULL;
dtv[GET_ADDR_MODULE].pointer.val = p;
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: modid %lu using static TLS; TCB=0x%lx\n",
+ (unsigned long int) GET_ADDR_MODULE,
+ (unsigned long int) THREAD_SELF);
+
return (char *) p + GET_ADDR_OFFSET;
}
else
@@ -1030,18 +1071,28 @@ __tls_get_addr (GET_ADDR_ARGS)
{
if (_dl_tls_allocate_active ()
&& GET_ADDR_MODULE < _dl_tls_initial_modid_limit)
+ {
/* This is a reentrant __tls_get_addr call, but we can
satisfy it because it's an initially-loaded module ID.
These TLS slotinfo slots do not change, so the
out-of-date generation counter does not matter. However,
if not in a TLS update, still update_get_addr below, to
get off the slow path eventually. */
- ;
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: modid %lu reentrant usage; TCB=0x%lx\n",
+ (unsigned long int) GET_ADDR_MODULE,
+ (unsigned long int) THREAD_SELF);
+ }
else
{
/* Update DTV up to the global generation, see CONCURRENCY NOTES
in _dl_update_slotinfo. */
gen = atomic_load_acquire (&GL(dl_tls_generation));
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf (
+ "tls: modid %lu update DTV to generation %lu; TCB=0x%lx\n",
+ (unsigned long int) GET_ADDR_MODULE, (unsigned long int) gen,
+ (unsigned long int) THREAD_SELF);
return update_get_addr (GET_ADDR_PARAM, gen);
}
}
@@ -1049,7 +1100,13 @@ __tls_get_addr (GET_ADDR_ARGS)
void *p = dtv[GET_ADDR_MODULE].pointer.val;
if (__glibc_unlikely (p == TLS_DTV_UNALLOCATED))
- return tls_get_addr_tail (GET_ADDR_PARAM, dtv, NULL);
+ {
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: modid %lu lazy allocation; TCB=0x%lx\n",
+ (unsigned long int) GET_ADDR_MODULE,
+ (unsigned long int) THREAD_SELF);
+ return tls_get_addr_tail (GET_ADDR_PARAM, dtv, NULL);
+ }
return (char *) p + GET_ADDR_OFFSET;
}
@@ -1119,6 +1176,10 @@ _dl_tls_initial_modid_limit_setup (void)
break;
}
_dl_tls_initial_modid_limit = idx;
+
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: initial modid limit set to %lu\n",
+ (unsigned long int) idx);
}
diff --git a/elf/rtld.c b/elf/rtld.c
index ff4c160d8fafd70d..3221bc8520996097 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -1257,6 +1257,11 @@ rtld_setup_main_map (struct link_map *main_map)
/* This image gets the ID one. */
GL(dl_tls_max_dtv_idx) = main_map->l_tls_modid = 1;
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: assign modid %lu to %s [%ld]\n",
+ (unsigned long int) main_map->l_tls_modid,
+ DSO_FILENAME (main_map->l_name),
+ (long int) main_map->l_ns);
}
break;
diff --git a/elf/tst-recursive-tlsmodN.c b/elf/tst-recursive-tlsmodN.c
index bb7592aee6ed347e..921980605cf32a38 100644
--- a/elf/tst-recursive-tlsmodN.c
+++ b/elf/tst-recursive-tlsmodN.c
@@ -19,10 +19,10 @@
/* Compiled with VAR and FUNC set via -D. FUNC requires some
relocation against TLS variable VAR. */
-__thread int VAR;
+__thread char VAR[32768];
int
FUNC (void)
{
- return VAR;
+ return VAR[0];
}
diff --git a/elf/tst-tls-debug-recursive.sh b/elf/tst-tls-debug-recursive.sh
new file mode 100755
index 0000000000000000..083e716f7216ffd5
--- /dev/null
+++ b/elf/tst-tls-debug-recursive.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+# Test for TLS logging in dynamic linker.
+# 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/>.
+
+# This script runs the tst-tls-debug-recursive test case and verifies its
+# LD_DEBUG=tls output. It checks for various TLS-related messages
+# to ensure the dynamic linker's TLS logging is working correctly.
+
+set -e
+common_objpfx="$1"
+test_wrapper_env="$2"
+rtld_prefix="$3"
+run_program_env="$4"
+test_program="$5"
+
+debug_output="${common_objpfx}elf/tst-tls-debug-recursive.debug"
+rm -f "${debug_output}".*
+
+# Run the test program with LD_DEBUG=tls.
+eval "${test_wrapper_env}" LD_DEBUG=tls LD_DEBUG_OUTPUT="${debug_output}" \
+ "${rtld_prefix}" "${test_program}"
+
+debug_output=$(ls "${debug_output}".*)
+
+fail=0
+
+# Check for expected messages
+if ! grep -q 'tls: DTV resized for TCB 0x.*: oldsize' "${debug_output}"; then
+ echo "FAIL: DTV resized message not found"
+ fail=1
+fi
+
+if ! grep -q 'tls: DTV update for TCB 0x.*: modid .* deallocated block' "${debug_output}"; then
+ echo "FAIL: module deallocated during DTV update message not found"
+ fail=1
+fi
+
+if ! grep -q 'tls: assign modid .* to' "${debug_output}"; then
+ echo "FAIL: module assigned message not found"
+ fail=1
+fi
+
+if ! grep -q 'tls: allocate block .* for modid .* size=.*, TCB=0x' "${debug_output}"; then
+ echo "FAIL: module allocated message not found"
+ fail=1
+fi
+
+if ! grep -q 'tls: modid .* update DTV to generation .* TCB=0x' "${debug_output}"; then
+ echo "FAIL: update DTV message not found"
+ fail=1
+fi
+
+if ! grep -q 'tls: initial modid limit set to' "${debug_output}"; then
+ echo "FAIL: initial modid limit message not found"
+ fail=1
+fi
+
+if [ $fail -ne 0 ]; then
+ echo "Test FAILED"
+ cat "${debug_output}"
+ rm -f "${debug_output}"
+ exit 1
+fi
+
+echo "Test PASSED"
+cat "${debug_output}"
+rm -f "${debug_output}"
+exit 0
diff --git a/nptl/Makefile b/nptl/Makefile
index 597311bf07223c4f..958de6b0002f63aa 100644
--- a/nptl/Makefile
+++ b/nptl/Makefile
@@ -274,6 +274,7 @@ CFLAGS-tst-thread-exit-clobber.o = -std=gnu++11
LDLIBS-tst-thread-exit-clobber = -lstdc++
CFLAGS-tst-minstack-throw.o = -std=gnu++11
LDLIBS-tst-minstack-throw = -lstdc++
+LDLIBS-tst-dl-debug-tid = $(libdl)
tests = \
tst-attr2 \
@@ -486,6 +487,7 @@ modules-names = \
tst-compat-forwarder-mod \
tst-execstack-mod \
tst-stack4mod \
+ tst-tls-debug-mod \
tst-tls3mod \
tst-tls5mod \
tst-tls5moda \
@@ -687,7 +689,8 @@ tst-stackguard1-ARGS = --command "$(host-test-program-cmd) --child"
tst-stackguard1-static-ARGS = --command "$(objpfx)tst-stackguard1-static --child"
ifeq ($(run-built-tests),yes)
-$(objpfx)tst-dl-debug-tid.out: tst-dl-debug-tid.sh $(objpfx)tst-dl-debug-tid
+$(objpfx)tst-dl-debug-tid.out: tst-dl-debug-tid.sh $(objpfx)tst-dl-debug-tid \
+ $(objpfx)tst-tls-debug-mod.so
$(SHELL) $< $(common-objpfx) '$(test-wrapper-env)' '$(rtld-prefix)' \
'$(run-program-env)' \
$(objpfx)tst-dl-debug-tid > $@; $(evaluate-test)
diff --git a/nptl/allocatestack.c b/nptl/allocatestack.c
index 9e0bb8f0eeff8e88..6c6094e929f927cc 100644
--- a/nptl/allocatestack.c
+++ b/nptl/allocatestack.c
@@ -115,7 +115,7 @@ get_cached_stack (size_t *sizep, void **memp)
lll_unlock (GL (dl_stack_cache_lock), LLL_PRIVATE);
if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
- GLRO (dl_debug_printf) ("TLS TCB reused from cache: 0x%lx\n",
+ GLRO (dl_debug_printf) ("tls: TCB reused from cache: 0x%lx\n",
(unsigned long int) result);
/* Report size and location of the stack to the caller. */
@@ -297,9 +297,9 @@ allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
GLRO (dl_debug_printf) (
- "TCB for user-supplied stack created: 0x%lx, stack=0x%lx, size=%lu\n",
- (unsigned long int) pd, (unsigned long int) pd->stackblock,
- (unsigned long int) pd->stackblock_size);
+ "tls: TCB created (user-supplied stack); stack=0x%lx, size=%lu, TCB=0x%lx\n",
+ (unsigned long int) pd->stackblock,
+ (unsigned long int) pd->stackblock_size, (unsigned long int) pd);
/* This is at least the second thread. */
pd->header.multiple_threads = 1;
@@ -436,7 +436,7 @@ allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
pd->setxid_futex = -1;
if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
- GLRO (dl_debug_printf) ("TCB for new stack allocated: 0x%lx\n",
+ GLRO (dl_debug_printf) ("tls: TCB allocated (new stack): 0x%lx\n",
(unsigned long int) pd);
/* Allocate the DTV for this thread. */
diff --git a/nptl/nptl-stack.c b/nptl/nptl-stack.c
index 4ef6527ff7e8192b..d2d0729035a19dfe 100644
--- a/nptl/nptl-stack.c
+++ b/nptl/nptl-stack.c
@@ -77,7 +77,7 @@ __nptl_free_stacks (size_t limit)
if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
GLRO (dl_debug_printf) (
- "TCB cache full, deallocating: TID=%ld, TCB=0x%lx\n",
+ "tls: TCB deallocating from full cache; TID=%ld, TCB=0x%lx\n",
(long int) curr->tid, (unsigned long int) curr);
/* Free the memory associated with the ELF TLS. */
@@ -104,7 +104,7 @@ queue_stack (struct pthread *stack)
/* The 'stack' parameter is a pointer to the TCB (struct pthread),
not just the stack. */
if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
- GLRO (dl_debug_printf) ("TCB deallocated into cache: TID=%ld, TCB=0x%lx\n",
+ GLRO (dl_debug_printf) ("tls: TCB deallocated into cache; TID=%ld, TCB=0x%lx\n",
(long int) stack->tid, (unsigned long int) stack);
/* We unconditionally add the stack to the list. The memory may
@@ -139,7 +139,7 @@ __nptl_deallocate_stack (struct pthread *pd)
the TLS memory. */
if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
GLRO (dl_debug_printf) (
- "TCB for user-supplied stack deallocated: TID=%ld, TCB=0x%lx\n",
+ "tls: TCB deallocated (user-supplied stack); TID=%ld, TCB=0x%lx\n",
(long int) pd->tid, (unsigned long int) pd);
/* Free the memory associated with the ELF TLS. */
_dl_deallocate_tls (TLS_TPADJ (pd), false);
diff --git a/nptl/pthread_create.c b/nptl/pthread_create.c
index 37de54eaa570cedb..c69b4108f60f647d 100644
--- a/nptl/pthread_create.c
+++ b/nptl/pthread_create.c
@@ -363,7 +363,7 @@ start_thread (void *arg)
}
if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
- GLRO (dl_debug_printf) ("Thread starting: TID=%ld, TCB=0x%lx\n",
+ GLRO (dl_debug_printf) ("tls: thread starting; TID=%ld, TCB=0x%lx\n",
(long int) pd->tid, (unsigned long int) pd);
/* Initialize resolver state pointer. */
diff --git a/nptl/tst-dl-debug-tid.c b/nptl/tst-dl-debug-tid.c
index 231fa43516b233b3..f3b443f9786a9a99 100644
--- a/nptl/tst-dl-debug-tid.c
+++ b/nptl/tst-dl-debug-tid.c
@@ -27,12 +27,25 @@
#include <support/xthread.h>
#include <stdio.h>
#include <unistd.h>
+#include <dlfcn.h>
+#include <support/xdlfcn.h>
+#include <support/check.h>
static void *
thread_function (void *arg)
{
if (arg)
pthread_barrier_wait ((pthread_barrier_t *) arg);
+
+ /* Load a module with TLS to verify allocation/deallocation logs. */
+ void *h = xdlopen ("tst-tls-debug-mod.so", RTLD_NOW);
+
+ /* Call a function that accesses TLS. */
+ int (*fp) (void) = (int (*) (void)) xdlsym (h, "in_dso");
+ TEST_COMPARE (fp (), 0);
+
+ xdlclose (h);
+
return NULL;
}
diff --git a/nptl/tst-dl-debug-tid.sh b/nptl/tst-dl-debug-tid.sh
index 9ee31ac4f241f0b4..8708ac9f2ea25c9e 100644
--- a/nptl/tst-dl-debug-tid.sh
+++ b/nptl/tst-dl-debug-tid.sh
@@ -39,7 +39,7 @@ eval "${test_wrapper_env}" LD_DEBUG=tls LD_DEBUG_OUTPUT="${debug_output}" \
debug_output=$(ls "${debug_output}".*)
# Check for the "Thread starting" message.
-if ! grep -q 'Thread starting: TID=' "${debug_output}"; then
+if ! grep -q 'tls: thread starting; TID=.*, TCB=0x' "${debug_output}"; then
echo "error: 'Thread starting' message not found"
cat "${debug_output}"
exit 1
@@ -47,10 +47,10 @@ fi
# Check that we have a message where the PID (from prefix) is different
# from the TID (in the message). This indicates a worker thread log.
-if ! grep 'Thread starting: TID=' "${debug_output}" | awk -F '[ \t:]+' '{
- sub(/,/, "", $4);
- sub(/TID=/, "", $4);
- if ($1 != $4)
+if ! grep 'tls: thread starting; TID=.*, TCB=0x' "${debug_output}" | awk -F '[ \t:]+' '{
+ sub(/TID=/, "", $5);
+ sub(/,/, "", $5);
+ if ($1 != $5)
exit 0;
exit 1
}'; then
@@ -60,12 +60,33 @@ if ! grep 'Thread starting: TID=' "${debug_output}" | awk -F '[ \t:]+' '{
fi
# We expect messages from thread creation and destruction.
-if ! grep -q 'TCB allocated\|TCB deallocating\|TCB reused\|TCB deallocated' \
+if ! grep -q 'tls: allocate TCB 0x\|tls: deallocate TCB 0x\|tls: TCB reused from cache\|tls: TCB deallocated' \
"${debug_output}"; then
echo "error: Expected TCB allocation/deallocation message not found"
cat "${debug_output}"
exit 1
fi
+# Check for TLS module ID assignment.
+if ! grep -q 'tls: assign modid .* to' "${debug_output}"; then
+ echo "error: Expected 'modid ... assigned to' message not found"
+ cat "${debug_output}"
+ exit 1
+fi
+
+# Check for TLS block allocation.
+if ! grep -q 'tls: allocate block .* for modid .* size=.*, TCB=0x' "${debug_output}"; then
+ echo "error: Expected 'modid ... allocated' message not found"
+ cat "${debug_output}"
+ exit 1
+fi
+
+# TLS block deallocation might be skipped due to DTV surplus.
+if grep -q 'tls: deallocate block .* for modid .* TCB=0x' "${debug_output}"; then
+ echo "INFO: module deallocated message found"
+else
+ echo "INFO: module deallocated message not found (may be due to DTV surplus)"
+fi
+
cat "${debug_output}"
rm -f "${debug_output}"
diff --git a/nptl/tst-tls-debug-mod.c b/nptl/tst-tls-debug-mod.c
new file mode 100644
index 0000000000000000..0308c8a3e2d76fe1
--- /dev/null
+++ b/nptl/tst-tls-debug-mod.c
@@ -0,0 +1,26 @@
+/* Test for TLS logging in dynamic linker.
+ 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/>. */
+
+__thread char tls_var[32768] __attribute__ ((tls_model ("global-dynamic")));
+
+int
+in_dso (void)
+{
+ tls_var[0] = 42;
+ return tls_var[0] - 42;
+}
diff --git a/sysdeps/x86_64/dl-tls.c b/sysdeps/x86_64/dl-tls.c
index 7abfb18413b1d06c..2bd336876a6613c8 100644
--- a/sysdeps/x86_64/dl-tls.c
+++ b/sysdeps/x86_64/dl-tls.c
@@ -41,11 +41,36 @@ __tls_get_addr_slow (GET_ADDR_ARGS)
dtv_t *dtv = THREAD_DTV ();
size_t gen = atomic_load_acquire (&GL(dl_tls_generation));
- if (__glibc_unlikely (dtv[0].counter != gen)
+ if (__glibc_unlikely (dtv[0].counter != gen))
+ {
/* See comment in __tls_get_addr in elf/dl-tls.c. */
- && !(_dl_tls_allocate_active ()
- && GET_ADDR_MODULE < _dl_tls_initial_modid_limit))
- return update_get_addr (GET_ADDR_PARAM, gen);
+ if (_dl_tls_allocate_active ()
+ && GET_ADDR_MODULE < _dl_tls_initial_modid_limit)
+ {
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf (
+ "tls: modid %lu reentrant usage; TCB=0x%lx\n",
+ (unsigned long int) GET_ADDR_MODULE,
+ (unsigned long int) THREAD_SELF);
+ }
+ else
+ {
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf (
+ "tls: modid %lu update DTV to generation %lu; TCB=0x%lx\n",
+ (unsigned long int) GET_ADDR_MODULE, (unsigned long int) gen,
+ (unsigned long int) THREAD_SELF);
+ return update_get_addr (GET_ADDR_PARAM, gen);
+ }
+ }
+
+ if (__glibc_unlikely (dtv[GET_ADDR_MODULE].pointer.val == TLS_DTV_UNALLOCATED))
+ {
+ if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_TLS))
+ _dl_debug_printf ("tls: modid %lu lazy allocation; TCB=0x%lx\n",
+ (unsigned long int) GET_ADDR_MODULE,
+ (unsigned long int) THREAD_SELF);
+ }
return tls_get_addr_tail (GET_ADDR_PARAM, dtv, NULL);
}

177
glibc-RHEL-49785-6.patch Normal file
View File

@ -0,0 +1,177 @@
commit 9181dc6eb6084da95d8c14d9defe96189fd0360d
Author: Frédéric Bérat <berat.fred@gmail.com>
Date: Tue Jan 27 23:07:17 2026 +0100
feat(rtld): Allow LD_DEBUG category exclusion
Adds support for excluding specific categories from `LD_DEBUG` output.
The `LD_DEBUG` environment variable now accepts category names prefixed
with a dash (`-`) to disable their debugging output. This allows users
to enable broad categories (e.g., `all`) while suppressing verbose or
irrelevant information from specific sub-categories (e.g., `-tls`).
The `process_dl_debug` function in `rtld.c` has been updated to parse
these exclusion options and unset the corresponding bits in
`GLRO(dl_debug_mask)`. The `LD_DEBUG=help` output has also been updated
to document this new functionality. A new test `tst-dl-debug-exclude.sh`
is added to verify the correct behavior of category exclusion.
Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
diff --git a/elf/Makefile b/elf/Makefile
index 2431e7032c44beb8..ec87811c61565511 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -1253,6 +1253,7 @@ $(objpfx)tst-glibcelf.out: tst-glibcelf.py elf.h $(..)/scripts/glibcelf.py \
ifeq ($(run-built-tests),yes)
tests-special += $(objpfx)tst-tls-allocation-failure-static-patched.out
tests-special += $(objpfx)tst-tls-debug-recursive.out
+tests-special += $(objpfx)tst-dl-debug-exclude.out
endif
# The test requires shared _and_ PIE because the executable
@@ -3064,4 +3065,14 @@ $(objpfx)tst-tls-debug-recursive.out: tst-tls-debug-recursive.sh \
'$(rtld-prefix)' '$(run_program_env)' \
$(objpfx)tst-recursive-tls > $@; \
$(evaluate-test)
+
+$(objpfx)tst-dl-debug-exclude.out: tst-dl-debug-exclude.sh \
+ $(objpfx)tst-recursive-tls \
+ $(objpfx)tst-recursive-tlsmallocmod.so \
+ $(patsubst %,$(objpfx)tst-recursive-tlsmod%.so, \
+ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15)
+ $(SHELL) $< $(common-objpfx) '$(test-wrapper-env)' \
+ '$(rtld-prefix)' '$(run_program_env)' \
+ $(objpfx)tst-recursive-tls > $@; \
+ $(evaluate-test)
endif
diff --git a/elf/rtld.c b/elf/rtld.c
index 3221bc8520996097..a4290890d6bca263 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -2708,11 +2708,18 @@ process_dl_debug (struct dl_main_state *state, const char *dl_debug)
&& dl_debug[len] != ',' && dl_debug[len] != ':')
++len;
+ bool exclude = *dl_debug == '-';
+ const char *name = exclude ? dl_debug + 1 : dl_debug;
+ size_t name_len = exclude ? len - 1 : len;
+
for (cnt = 0; cnt < ndebopts; ++cnt)
- if (debopts[cnt].len == len
- && memcmp (dl_debug, debopts[cnt].name, len) == 0)
+ if (debopts[cnt].len == name_len
+ && memcmp (name, debopts[cnt].name, name_len) == 0)
{
- GLRO(dl_debug_mask) |= debopts[cnt].mask;
+ if (exclude)
+ GLRO(dl_debug_mask) &= ~debopts[cnt].mask;
+ else
+ GLRO(dl_debug_mask) |= debopts[cnt].mask;
break;
}
@@ -2754,7 +2761,8 @@ Valid options for the LD_DEBUG environment variable are:\n\n");
_dl_printf ("\n\
To direct the debugging output into a file instead of standard output\n\
-a filename can be specified using the LD_DEBUG_OUTPUT environment variable.\n");
+a filename can be specified using the LD_DEBUG_OUTPUT environment variable.\n\
+Categories can be excluded by prefixing them with a dash (-).\n");
_exit (0);
}
}
diff --git a/elf/tst-dl-debug-exclude.sh b/elf/tst-dl-debug-exclude.sh
new file mode 100644
index 0000000000000000..9837ffcea8e07933
--- /dev/null
+++ b/elf/tst-dl-debug-exclude.sh
@@ -0,0 +1,87 @@
+#!/bin/sh
+# Test for LD_DEBUG category exclusion.
+# 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/>.
+
+# This script verifies the LD_DEBUG category exclusion functionality.
+# It checks that:
+# 1. Categories can be excluded using the '-' prefix.
+# 2. Options are processed sequentially, meaning the last specified
+# option for a category (enable or exclude) takes precedence.
+
+set -e
+
+common_objpfx="$1"
+test_wrapper_env="$2"
+rtld_prefix="$3"
+run_program_env="$4"
+test_program="$5"
+
+debug_output="${common_objpfx}elf/tst-dl-debug-exclude.debug"
+rm -f "${debug_output}".*
+
+# Run the test program with LD_DEBUG=all,-tls.
+# We expect general logs but no TLS logs.
+eval "${test_wrapper_env}" LD_DEBUG=all,-tls LD_DEBUG_OUTPUT="${debug_output}" \
+ "${rtld_prefix}" "${test_program}"
+
+fail=0
+
+# 1. Check that general logs are present (e.g., file loading)
+if ! grep -q 'file=' "${debug_output}".*; then
+ echo "FAIL: 'file=' message not found (LD_DEBUG=all failed)"
+ fail=1
+fi
+
+# 2. Check that TLS logs are NOT present
+if grep -q 'tls: ' "${debug_output}".*; then
+ echo "FAIL: TLS message found (exclusion of -tls failed)"
+ fail=1
+fi
+
+rm -f "${debug_output}".*
+# 3. Check for LD_DEBUG=all,-tls,tls (ordering verification)
+# We expect TLS logs to BE present
+eval "${test_wrapper_env}" LD_DEBUG=all,-tls,tls LD_DEBUG_OUTPUT="${debug_output}" \
+ "${rtld_prefix}" "${test_program}"
+
+if ! grep -q 'tls: ' "${debug_output}".*; then
+ echo "FAIL: TLS message not found (ordering -tls,tls failed)"
+ fail=1
+fi
+
+rm -f "${debug_output}".*
+# 4. Check for LD_DEBUG=tls,-tls
+# We expect TLS logs to NOT be present
+eval "${test_wrapper_env}" LD_DEBUG=tls,-tls LD_DEBUG_OUTPUT="${debug_output}" \
+ "${rtld_prefix}" "${test_program}"
+
+if grep -q 'tls: ' "${debug_output}".*; then
+ echo "FAIL: TLS message found (ordering tls,-tls failed)"
+ fail=1
+fi
+
+if [ $fail -ne 0 ]; then
+ echo "Test FAILED"
+ cat "${debug_output}".*
+ rm -f "${debug_output}".*
+ exit 1
+fi
+
+echo "Test PASSED"
+rm -f "${debug_output}".*
+exit 0