[glibc] elf: Set up TLS slotinfo for dlopen'd modules before relocation (BZ 34170)

Adhemerval Zanella via Glibc-cvs <[email protected]> Fri, 5 Jun 2026 19:21:28 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=f9a11a380d1e18199444670c0e264e3e6f4182b2

commit f9a11a380d1e18199444670c0e264e3e6f4182b2
Author: Adhemerval Zanella <[email protected]>
Date:   Mon May 25 15:15:01 2026 -0300

    elf: Set up TLS slotinfo for dlopen'd modules before relocation (BZ 34170)
    
    An IFUNC resolver in a DSO that is being loaded by dlopen is allowed
    to read its own TLS storage during the resolver call.  After
    af34b1376a3 ("elf: Initialize static TLS before relocation processing
    (BZ 34164)") that works for the initial-exec model on every supported
    architecture.
    
    However, it does not work for the dynamic-TLS path (on both -mtls-dialect mode,
    if the ABI supports both).  Both lookup paths index the calling thread's
    DTV by the new module's l_tls_modid and, on miss, walk
    GL(dl_tls_dtv_slotinfo_list) to discover the module and lazily allocate
    its TLS block.  The just-loaded DSO is however not yet in that list when
    its resolver fires, so the lookup faults inside dlopen.  This is the
    direct dlopen analog of BZ 34164.
    
    The solution is to reorder dl_open_worker_begin so the slotinfo install
    happens before the relocation pass.  The new order is:
    
      1. resize_scopes, resize_tls_slotinfo, add_to_global_resize
         (unchanged, still recoverable).
      2. update_tls_slotinfo: register the new modules in slotinfo, bump
         dl_tls_generation, initialise their static TLS images.
      3. Relocate the new objects.  IFUNC resolvers can now read their
         own DSO's __thread storage via any TLS model.
      4. Demarcation point.
      5. update_scopes, _dl_find_object_update.
    
    Checked on aarch64-linux-gnu and x86_64-linux-gnu.
    
    Reviewed-by: H.J. Lu <[email protected]>

Diff:
---
 elf/Makefile                                | 24 ++++++++
 elf/dl-open.c                               | 51 ++++++++---------
 elf/tst-ifunc-tls-init-gd-global-lib.c      |  3 +
 elf/tst-ifunc-tls-init-gd-ld-lib-skeleton.c | 69 ++++++++++++++++++++++
 elf/tst-ifunc-tls-init-gd-ld.c              | 89 +++++++++++++++++++++++++++++
 elf/tst-ifunc-tls-init-gd-lib.c             |  2 +
 elf/tst-ifunc-tls-init-ld-lib.c             |  2 +
 elf/tst-ifunc-tls-init-tlsdesc-lib.c        |  3 +
 elf/tst-ifunc-tls-init-tlsdesc.c            | 69 ++++++++++++++++++++++
 9 files changed, 286 insertions(+), 26 deletions(-)

diff --git a/elf/Makefile b/elf/Makefile
index bdf9a786d5..5ede78c590 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -1296,6 +1296,7 @@ tests += \
   tst-ifunc-plt-dlopen-bindnow \
   tst-ifunc-resolver-protector \
   tst-ifunc-tls-init \
+  tst-ifunc-tls-init-gd-ld \
   tst-ifunc-tls-write \
   tst-tls-tdata-reloc \
   # tests
@@ -1363,11 +1364,20 @@ modules-names += \
   tst-ifunc-plt-dep \
   tst-ifunc-plt-lib \
   tst-ifunc-resolver-protector-mod \
+  tst-ifunc-tls-init-gd-global-lib \
+  tst-ifunc-tls-init-gd-lib \
+  tst-ifunc-tls-init-ld-lib \
   tst-ifunc-tls-init-lib1 \
   tst-ifunc-tls-init-lib2 \
   tst-ifunc-tls-write-lib \
   tst-tls-tdata-reloc-lib \
   # modules-names
+ifneq (no,$(have-test-mtls-descriptor))
+tests += tst-ifunc-tls-init-tlsdesc
+modules-names += tst-ifunc-tls-init-tlsdesc-lib
+CFLAGS-tst-ifunc-tls-init-tlsdesc-lib.c += \
+  -mtls-dialect=$(have-test-mtls-descriptor)
+endif
 ifeq (no,$(with-lld))
 modules-names += ifuncmod5
 endif
@@ -2522,6 +2532,20 @@ $(objpfx)tst-ifunc-tls-init.out: \
 $(objpfx)tst-ifunc-tls-write: $(objpfx)tst-ifunc-tls-write-lib.so
 $(objpfx)tst-tls-tdata-reloc: $(objpfx)tst-tls-tdata-reloc-lib.so
 
+$(objpfx)tst-ifunc-tls-init-gd-ld: $(shared-thread-library)
+$(objpfx)tst-ifunc-tls-init-gd-ld.out: \
+  $(objpfx)tst-ifunc-tls-init-gd-lib.so \
+  $(objpfx)tst-ifunc-tls-init-gd-global-lib.so \
+  $(objpfx)tst-ifunc-tls-init-ld-lib.so
+tst-ifunc-tls-init-gd-ld-TUNABLES = glibc.rtld.optional_static_tls=0
+
+ifneq (no,$(have-test-mtls-descriptor))
+$(objpfx)tst-ifunc-tls-init-tlsdesc: $(shared-thread-library)
+$(objpfx)tst-ifunc-tls-init-tlsdesc.out: \
+  $(objpfx)tst-ifunc-tls-init-tlsdesc-lib.so
+tst-ifunc-tls-init-tlsdesc-TUNABLES = glibc.rtld.optional_static_tls=0
+endif # $(have-test-mtls-descriptor)
+
 $(objpfx)tst-unique1.out: $(objpfx)tst-unique1mod1.so \
 			  $(objpfx)tst-unique1mod2.so
 
diff --git a/elf/dl-open.c b/elf/dl-open.c
index 0a34b1b334..cf4749694f 100644
--- a/elf/dl-open.c
+++ b/elf/dl-open.c
@@ -658,6 +658,31 @@ dl_open_worker_begin (void *a)
 
   bool relocation_in_progress = false;
 
+  /* This only performs the memory allocations.  The actual update of
+     the scopes happens below, after failure is impossible.  */
+  resize_scopes (new);
+
+  /* Increase the size of the GL (dl_tls_dtv_slotinfo_list) data
+     structure.  */
+  bool any_tls = resize_tls_slotinfo (new);
+
+  /* Perform the necessary allocations for adding new global objects
+     to the global scope below.  */
+  if (mode & RTLD_GLOBAL)
+    add_to_global_resize (new);
+
+  /* Install the new modules in the DTV slotinfo and initialise their
+     static TLS *before* relocation, so an IFUNC resolver firing during
+     the relocation loop below can reach its DSO's __thread storage via
+     __tls_get_addr / TLSDESC.  Without this, the resolver's TLS access
+     for a just-loaded module would index into an unallocated DTV slot
+     and crash.  If relocation later fails, the subsequent _dl_close_worker
+     cleans up these slotinfo entries via remove_slotinfo.  */
+  if (any_tls)
+    /* FIXME: This calls _dl_update_slotinfo, which aborts the process
+       on memory allocation failure.  See bug 16134.  */
+    update_tls_slotinfo (new);
+
   /* Perform relocation.  This can trigger lazy binding in IFUNC
      resolvers.  For NODELETE mappings, these dependencies are not
      recorded because the flag has not been applied to the newly
@@ -682,19 +707,6 @@ dl_open_worker_begin (void *a)
     _dl_open_relocate_one_object (args, r, new->l_initfini[i], reloc_mode,
 				  &relocation_in_progress);
 
-  /* This only performs the memory allocations.  The actual update of
-     the scopes happens below, after failure is impossible.  */
-  resize_scopes (new);
-
-  /* Increase the size of the GL (dl_tls_dtv_slotinfo_list) data
-     structure.  */
-  bool any_tls = resize_tls_slotinfo (new);
-
-  /* Perform the necessary allocations for adding new global objects
-     to the global scope below.  */
-  if (mode & RTLD_GLOBAL)
-    add_to_global_resize (new);
-
   /* Demarcation point: After this, no recoverable errors are allowed.
      All memory allocations for new objects must have happened
      before.  */
@@ -716,19 +728,6 @@ dl_open_worker_begin (void *a)
     _dl_signal_error (ENOMEM, new->l_libname->name, NULL,
 		      N_ ("cannot allocate address lookup data"));
 
-  /* FIXME: It is unclear whether the order here is correct.
-     Shouldn't new objects be made available for binding (and thus
-     execution) only after there TLS data has been set up fully?
-     Fixing bug 16134 will likely make this distinction less
-     important.  */
-
-  /* Second stage after resize_tls_slotinfo: Update the slotinfo data
-     structures.  */
-  if (any_tls)
-    /* FIXME: This calls _dl_update_slotinfo, which aborts the process
-       on memory allocation failure.  See bug 16134.  */
-    update_tls_slotinfo (new);
-
   /* Notify the debugger all new objects have been relocated.  */
   if (relocation_in_progress)
     LIBC_PROBE (reloc_complete, 3, args->nsid, r, new);
diff --git a/elf/tst-ifunc-tls-init-gd-global-lib.c b/elf/tst-ifunc-tls-init-gd-global-lib.c
new file mode 100644
index 0000000000..9e91ab4c9b
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-gd-global-lib.c
@@ -0,0 +1,3 @@
+#define TLS_MODEL "global-dynamic"
+#define SENTINEL_STORAGE /* empty */
+#include "tst-ifunc-tls-init-gd-ld-lib-skeleton.c"
diff --git a/elf/tst-ifunc-tls-init-gd-ld-lib-skeleton.c b/elf/tst-ifunc-tls-init-gd-ld-lib-skeleton.c
new file mode 100644
index 0000000000..b6fd6cf06e
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-gd-ld-lib-skeleton.c
@@ -0,0 +1,69 @@
+/* Shared-library skeleton for tst-ifunc-tls-init-gd-ld.
+   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/>.  */
+
+/* Unlike tst-ifunc-tls-init-lib-skeleton.c, which uses initial-exec and
+   resolves via a single TP-relative load, this skeleton's TLS_MODEL is
+   either "global-dynamic" or "local-dynamic" so the resolver's read of
+   'sentinel' must traverse __tls_get_addr (or the architecture's
+   TLSDESC equivalent).  That dynamic-TLS access can lazily allocate a
+   per-module TLS block, which is the path being exercised.  */
+
+#ifndef TLS_MODEL
+# error "tst-ifunc-tls-init-gd-ld-lib-skeleton.c needs TLS_MODEL defined"
+#endif
+
+/* Without static the relocation against the SENTINEL goes through the regular
+   global-symbol lookup path; combined with TLS_MODEL="global-dynamic" this
+   exercises the "global GD" variant rather than the file-local one.  */
+#ifndef SENTINEL_STORAGE
+# define SENTINEL_STORAGE static
+#endif
+
+#define SENTINEL 0x5A5A1234
+
+SENTINEL_STORAGE volatile __thread int sentinel
+  __attribute__ ((tls_model (TLS_MODEL))) = SENTINEL;
+static volatile int last_seen_sentinel;
+
+static int
+impl_ok (void)
+{
+  return SENTINEL;
+}
+
+static int
+impl_bad (void)
+{
+  return 0;
+}
+
+int
+get_last_seen_sentinel (void)
+{
+  return last_seen_sentinel;
+}
+
+static int (*resolver (void)) (void)
+{
+  int s = sentinel;
+  last_seen_sentinel = s;
+  return s == SENTINEL ? impl_ok : impl_bad;
+}
+int ifunc_tls (void) __attribute__ ((ifunc ("resolver")));
+
+int (*fptr) (void) = ifunc_tls;
diff --git a/elf/tst-ifunc-tls-init-gd-ld.c b/elf/tst-ifunc-tls-init-gd-ld.c
new file mode 100644
index 0000000000..20ebe4d993
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-gd-ld.c
@@ -0,0 +1,89 @@
+/* Check if dynamic-TLS variables (global-dynamic, local-dynamic) are
+   correctly initialised in IFUNC resolvers reached via dlopen.
+   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 test dlopens three modules covering the dynamic-TLS access variants:
+
+     gd-lib         file-local 'sentinel' + tls_model("global-dynamic")
+     ld-lib         file-local 'sentinel' + tls_model("local-dynamic")
+     gd-global-lib  external 'sentinel' + tls_model("global-dynamic")
+
+   In each case the IFUNC resolver's read of 'sentinel' must traverse the
+   dynamic-TLS resolution path at the moment the resolver fires during
+   dlopen-time relocation.
+
+   Each variant additionally calls the resolved IFUNC from a thread spawned
+   after dlopen, to verify per-thread DTV propagation for the newly-loaded
+   module.  */
+
+#include <support/check.h>
+#include <support/xdlfcn.h>
+#include <support/xthread.h>
+
+#define SENTINEL 0x5A5A1234
+
+struct ifunc_handles
+{
+  int (*get_last_seen_sentinel) (void);
+  int (**fptr) (void);
+  int (*ifunc_tls) (void);
+};
+
+static void *
+ifunc_caller (void *arg)
+{
+  struct ifunc_handles *h = arg;
+  TEST_COMPARE ((*h->fptr) (), SENTINEL);
+  TEST_COMPARE (h->ifunc_tls (), SENTINEL);
+  return NULL;
+}
+
+static void
+test_lib (const char *soname)
+{
+  void *handle = xdlopen (soname, RTLD_LAZY | RTLD_LOCAL);
+
+  struct ifunc_handles h;
+  h.get_last_seen_sentinel = xdlsym (handle, "get_last_seen_sentinel");
+  h.fptr = xdlsym (handle, "fptr");
+  h.ifunc_tls = xdlsym (handle, "ifunc_tls");
+
+  TEST_COMPARE (h.get_last_seen_sentinel (), SENTINEL);
+
+  TEST_VERIFY (*h.fptr != NULL);
+  TEST_COMPARE ((*h.fptr) (), SENTINEL);
+  TEST_COMPARE (h.ifunc_tls (), SENTINEL);
+
+  /* From a thread spawned *after* the dlopen, which exercises DTV propagation
+     for the new module into a fresh TCB.  */
+  pthread_t consumer = xpthread_create (NULL, ifunc_caller, &h);
+  xpthread_join (consumer);
+
+  xdlclose (handle);
+}
+
+static int
+do_test (void)
+{
+  test_lib ("tst-ifunc-tls-init-gd-lib.so");
+  test_lib ("tst-ifunc-tls-init-ld-lib.so");
+  test_lib ("tst-ifunc-tls-init-gd-global-lib.so");
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/tst-ifunc-tls-init-gd-lib.c b/elf/tst-ifunc-tls-init-gd-lib.c
new file mode 100644
index 0000000000..3d940f2af3
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-gd-lib.c
@@ -0,0 +1,2 @@
+#define TLS_MODEL "global-dynamic"
+#include "tst-ifunc-tls-init-gd-ld-lib-skeleton.c"
diff --git a/elf/tst-ifunc-tls-init-ld-lib.c b/elf/tst-ifunc-tls-init-ld-lib.c
new file mode 100644
index 0000000000..41e9f30298
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-ld-lib.c
@@ -0,0 +1,2 @@
+#define TLS_MODEL "local-dynamic"
+#include "tst-ifunc-tls-init-gd-ld-lib-skeleton.c"
diff --git a/elf/tst-ifunc-tls-init-tlsdesc-lib.c b/elf/tst-ifunc-tls-init-tlsdesc-lib.c
new file mode 100644
index 0000000000..9e91ab4c9b
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-tlsdesc-lib.c
@@ -0,0 +1,3 @@
+#define TLS_MODEL "global-dynamic"
+#define SENTINEL_STORAGE /* empty */
+#include "tst-ifunc-tls-init-gd-ld-lib-skeleton.c"
diff --git a/elf/tst-ifunc-tls-init-tlsdesc.c b/elf/tst-ifunc-tls-init-tlsdesc.c
new file mode 100644
index 0000000000..dfa99a4248
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-tlsdesc.c
@@ -0,0 +1,69 @@
+/* Check that an IFUNC resolver in a dlopen'd DSO can read .tdata-initialised
+   __thread storage when the TLS access is compiled as a TLSDESC sequence.
+   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/>.  */
+
+/* Sibling of tst-ifunc-tls-init-gd-ld, but the TLS access in the loaded
+   DSO is compiled with the TLSDESC dialect (x86 gnu2, aarch64 desc).  */
+
+#include <support/check.h>
+#include <support/xdlfcn.h>
+#include <support/xthread.h>
+
+#define SENTINEL 0x5A5A1234
+
+struct ifunc_handles
+{
+  int (*get_last_seen_sentinel) (void);
+  int (**fptr) (void);
+  int (*ifunc_tls) (void);
+};
+
+static void *
+ifunc_caller (void *arg)
+{
+  struct ifunc_handles *h = arg;
+  TEST_COMPARE ((*h->fptr) (), SENTINEL);
+  TEST_COMPARE (h->ifunc_tls (), SENTINEL);
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  void *handle = xdlopen ("tst-ifunc-tls-init-tlsdesc-lib.so",
+			  RTLD_LAZY | RTLD_LOCAL);
+
+  struct ifunc_handles h;
+  h.get_last_seen_sentinel = xdlsym (handle, "get_last_seen_sentinel");
+  h.fptr = xdlsym (handle, "fptr");
+  h.ifunc_tls = xdlsym (handle, "ifunc_tls");
+
+  TEST_COMPARE (h.get_last_seen_sentinel (), SENTINEL);
+
+  TEST_VERIFY (*h.fptr != NULL);
+  TEST_COMPARE ((*h.fptr) (), SENTINEL);
+  TEST_COMPARE (h.ifunc_tls (), SENTINEL);
+
+  pthread_t consumer = xpthread_create (NULL, ifunc_caller, &h);
+  xpthread_join (consumer);
+
+  xdlclose (handle);
+  return 0;
+}
+
+#include <support/test-driver.c>