diff --git a/elf/dl-tls.c b/elf/dl-tls.c index aacd80cbe5..7d815e102f 100644 --- a/elf/dl-tls.c +++ b/elf/dl-tls.c @@ -537,6 +537,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; @@ -725,6 +727,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 299f8dd60e..5ea5383eb6 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..bba47b79e7 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->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..231fa43516 --- /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 + . */ + +/* 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 +#include +#include +#include + +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 diff --git a/nptl/tst-dl-debug-tid.sh b/nptl/tst-dl-debug-tid.sh new file mode 100644 index 0000000000..93d27134a0 --- /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 +# . + +# 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 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;