[PATCH 1/2] tls: Add debug logging for TLS and TCB management

Frédéric Bérat fberat@redhat.com
Wed Oct 15 14:23:05 GMT 2025


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.
---
 elf/dl-tls.c               |  6 ++++
 elf/rtld.c                 |  4 ++-
 nptl/Makefile              |  7 ++++
 nptl/allocatestack.c       | 14 ++++++++
 nptl/nptl-stack.c          | 19 +++++++++--
 nptl/pthread_create.c      |  4 +++
 nptl/tst-dl-debug-tid.c    | 56 +++++++++++++++++++++++++++++++
 nptl/tst-dl-debug-tid.sh   | 68 ++++++++++++++++++++++++++++++++++++++
 sysdeps/generic/ldsodefs.h |  3 +-
 9 files changed, 177 insertions(+), 4 deletions(-)
 create mode 100644 nptl/tst-dl-debug-tid.c
 create mode 100644 nptl/tst-dl-debug-tid.sh

diff --git a/elf/dl-tls.c b/elf/dl-tls.c
index a083a82933..ffc5798fb8 100644
--- a/elf/dl-tls.c
+++ b/elf/dl-tls.c
@@ -535,6 +535,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;
@@ -723,6 +725,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 753ce6690b..480e11557d 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -2428,10 +2428,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 e6481d5694..881b9f98cc 100644
--- a/nptl/Makefile
+++ b/nptl/Makefile
@@ -375,6 +375,7 @@ tests-container =  tst-pthread-getattr
 tests-internal := \
   tst-barrier5 \
   tst-cond22 \
+  tst-dl-debug-tid \
   tst-mutex8 \
   tst-mutex8-static \
   tst-mutexpi8 \
@@ -573,6 +574,7 @@ xtests-static += tst-setuid1-static
 
 ifeq ($(run-built-tests),yes)
 tests-special += \
+  $(objpfx)tst-dl-debug-tid.out \
   $(objpfx)tst-oddstacklimit.out \
   # tests-special
 ifeq ($(build-shared),yes)
@@ -710,6 +712,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 99f56b94df..94afd02ba4 100644
--- a/nptl/allocatestack.c
+++ b/nptl/allocatestack.c
@@ -116,6 +116,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;
@@ -430,6 +434,12 @@ allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
 	 stack cache nor will the memory (except the TLS memory) be freed.  */
       pd->stack_mode = ALLOCATE_GUARD_USER;
 
+      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;
 
@@ -550,6 +560,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 c049c5133c..c8f2b3505f 100644
--- a/nptl/nptl-stack.c
+++ b/nptl/nptl-stack.c
@@ -77,6 +77,10 @@ __nptl_free_stacks (size_t limit)
 
 	  /* Free the memory associated with the ELF TLS.  */
 	  _dl_deallocate_tls (TLS_TPADJ (curr), false);
+	  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);
 
 	  /* Remove this block.  This should never fail.  If it does
 	     something is really wrong.  */
@@ -99,6 +103,9 @@ queue_stack (struct pthread *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.  */
+  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);
   __nptl_stack_list_add (&stack->list, &GL (dl_stack_cache));
 
   GL (dl_stack_cache_actsize) += stack->stackblock_size;
@@ -123,8 +130,16 @@ __nptl_deallocate_stack (struct pthread *pd)
   if (__glibc_likely (pd->stack_mode != ALLOCATE_GUARD_USER))
     (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 e1033d4ee6..19e4ec8064 100644
--- a/nptl/pthread_create.c
+++ b/nptl/pthread_create.c
@@ -364,6 +364,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 0000000000..b0befe9e1a
--- /dev/null
+++ b/nptl/tst-dl-debug-tid.c
@@ -0,0 +1,56 @@
+/* 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/>.  */
+
+#include <pthread.h>
+#include <support/xthread.h>
+#include <stdio.h>
+#include <unistd.h>
+
+static void *
+thread_function (void *arg)
+{
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  pthread_t thread1;
+  pthread_attr_t attr;
+
+  /* A joinable thread.
+   * Deallocation is done by the main thread in pthread_join.  */
+  thread1 = xpthread_create (NULL, thread_function, NULL);
+
+  /* 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, NULL);
+  xpthread_attr_destroy (&attr);
+
+  /* Give the detached thread time to finish.  */
+  sleep (1);
+
+  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 0000000000..eeedb9aa3b
--- /dev/null
+++ b/nptl/tst-dl-debug-tid.sh
@@ -0,0 +1,68 @@
+#!/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/>.
+
+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 31e9a6b600..cb318ade7b 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -523,8 +523,9 @@ 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_TLS        (1 << 11)
 
   /* Platform name.  */
   EXTERN const char *_dl_platform;
-- 
2.51.0



More information about the Libc-alpha mailing list