[glibc] elf: Initialize static TLS before relocation processing (BZ 34164)

Adhemerval Zanella via Glibc-cvs <[email protected]> Mon, 25 May 2026 12:56:43 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=af34b1376a37fa27e1de9d869ed9493fc569bfa6

commit af34b1376a37fa27e1de9d869ed9493fc569bfa6
Author: Adhemerval Zanella <[email protected]>
Date:   Fri May 22 14:08:12 2026 -0300

    elf: Initialize static TLS before relocation processing (BZ 34164)
    
    An IFUNC resolver firing during dynamic linker relocation reads its
    DSO's __thread storage from a zero-filled slot: init_tls() allocates
    the static TLS block zero-filled, but .tdata is not copied in until
    the trailing _dl_allocate_tls_init at the end of dl_main, long after
    the per-object phase 2 resolvers from commit 63b31c05a8a901 have run.
    A resolver that *writes* TLS is even worse off -- the write is
    clobbered by that same trailing copy.
    
    dl_main (elf/rtld.c): populate the DTV slotinfo, bump
    dl_tls_generation, and call _dl_allocate_tls_init right after
    init_tls(), before the relocation loop.
    
    _dl_try_allocate_static_tls (elf/dl-reloc.c): drop the
    "defer-if-not-relocated" branch and always run _dl_init_static_tls
    inline, so a CHECK_STATIC_TLS allocation triggered mid-relocation
    initialises the slot before the same object's phase 2 fires.
    
    The new tests cheks some scenarios:
    
      elf/tst-ifunc-tls-init         resolver reads its DSO's IE TLS.
      elf/tst-ifunc-tls-init-dlopen  same, via dlopen.
      elf/tst-ifunc-tls-write        resolver write to TLS must survive
                                     to main.
    
    Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu.
    
    Reviewed-by: H.J. Lu <[email protected]>

Diff:
---
 elf/Makefile                          | 10 +++++
 elf/dl-reloc.c                        | 27 ++++++------
 elf/rtld.c                            | 31 +++++++++-----
 elf/tst-ifunc-tls-init-dlopen-lib.c   | 55 ++++++++++++++++++++++++
 elf/tst-ifunc-tls-init-lib-skeleton.c | 57 ++++++++++++++++++++++++
 elf/tst-ifunc-tls-init-lib1.c         |  1 +
 elf/tst-ifunc-tls-init-lib2.c         |  1 +
 elf/tst-ifunc-tls-init.c              | 81 +++++++++++++++++++++++++++++++++++
 elf/tst-ifunc-tls-write-lib.c         | 54 +++++++++++++++++++++++
 elf/tst-ifunc-tls-write.c             | 34 +++++++++++++++
 10 files changed, 327 insertions(+), 24 deletions(-)

diff --git a/elf/Makefile b/elf/Makefile
index aef13b73ca..ff42eb32d4 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -1268,6 +1268,8 @@ tests += \
   tst-ifunc-plt-bindnow \
   tst-ifunc-plt-dlopen \
   tst-ifunc-plt-dlopen-bindnow \
+  tst-ifunc-tls-init \
+  tst-ifunc-tls-write \
   # tests
 # Note: sysdeps/x86_64/ifuncmain8.c uses ifuncmain8.
 tests-internal += \
@@ -1332,6 +1334,9 @@ modules-names += \
   ifuncmod6 \
   tst-ifunc-plt-dep \
   tst-ifunc-plt-lib \
+  tst-ifunc-tls-init-lib1 \
+  tst-ifunc-tls-init-lib2 \
+  tst-ifunc-tls-write-lib \
   # modules-names
 ifeq (no,$(with-lld))
 modules-names += ifuncmod5
@@ -2468,6 +2473,11 @@ $(objpfx)tst-ifunc-plt-dlopen.out: \
 $(objpfx)tst-ifunc-plt-dlopen-bindnow.out: \
   $(objpfx)tst-ifunc-plt-lib.so $(objpfx)tst-ifunc-plt-dep.so
 
+$(objpfx)tst-ifunc-tls-init: $(objpfx)tst-ifunc-tls-init-lib1.so
+$(objpfx)tst-ifunc-tls-init.out: \
+  $(objpfx)tst-ifunc-tls-init-lib2.so
+$(objpfx)tst-ifunc-tls-write: $(objpfx)tst-ifunc-tls-write-lib.so
+
 $(objpfx)tst-unique1.out: $(objpfx)tst-unique1mod1.so \
 			  $(objpfx)tst-unique1mod2.so
 
diff --git a/elf/dl-reloc.c b/elf/dl-reloc.c
index f1a432ac09..91497b3839 100644
--- a/elf/dl-reloc.c
+++ b/elf/dl-reloc.c
@@ -107,22 +107,21 @@ _dl_try_allocate_static_tls (struct link_map *map, bool optional)
 # error "Either TLS_TCB_AT_TP or TLS_DTV_AT_TP must be defined"
 #endif
 
-  /* If the object is not yet relocated we cannot initialize the
-     static TLS region.  Delay it.  */
-  if (map->l_real->l_relocated)
-    {
+  /* Initialise the static TLS region, the map may not yet be l_relocated (a
+     TLS reloc inside the relocation loop triggered the allocation), but
+     _dl_init_static_tls only writes .tdata into the static TLS slot, which is
+     independent of relocation state.
+     Doing this inline ensures any IFUNC resolver that fires later in the same
+     object's relocation pass sees an initialised TLS slot, and the
+     post-relocation TLS init loop in dl_open_worker_begin becomes a no-op for
+     this map.  */
 #ifdef SHARED
-      /* Update the DTV of the current thread.  Note: GL(dl_load_tls_lock)
-	 is held here so normal load of the generation counter is valid.  */
-      if (__builtin_expect (THREAD_DTV()[0].counter != GL(dl_tls_generation),
-			    0))
-	(void) _dl_update_slotinfo (map->l_tls_modid, GL(dl_tls_generation));
+  /* Update the DTV of the current thread.  Note: GL(dl_load_tls_lock)
+     is held here so normal load of the generation counter is valid.  */
+  if (__glibc_unlikely (THREAD_DTV()[0].counter != GL(dl_tls_generation)))
+    _dl_update_slotinfo (map->l_tls_modid, GL(dl_tls_generation));
 #endif
-
-      _dl_init_static_tls (map);
-    }
-  else
-    map->l_need_tls_init = 1;
+  _dl_init_static_tls (map);
 
   return 0;
 }
diff --git a/elf/rtld.c b/elf/rtld.c
index e926ec73e4..12e1b4dd71 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -2263,6 +2263,26 @@ dl_main (const ElfW(Phdr) *phdr,
       rtld_timer_accum (&relocate_time, start);
   }
 
+  /* Populate the DTV slotinfo and copy each TLS module's into thestatic TLS
+     block *before* the relocation loop.  IFUNC resolvers fired during phase 2
+     of the per-object two-phase scheme therefore observe initialised TLS.  */
+  if (__rtld_tls_init_tp_called)
+    {
+      unsigned int i = main_map->l_searchlist.r_nlist;
+      while (i-- > 0)
+	{
+	  struct link_map *l = main_map->l_initfini[i];
+	  if (l->l_tls_blocksize != 0)
+	    _dl_add_to_slotinfo (l, true);
+	}
+      /* _dl_add_to_slotinfo records gen = dl_tls_generation + 1, and
+	 _dl_allocate_tls_init asserts gen <= dl_tls_generation, so bump
+	 the generation before init.  */
+      if (GL(dl_tls_max_dtv_idx) > 0)
+	++GL(dl_tls_generation);
+      _dl_allocate_tls_init (tcbp, true);
+    }
+
   RTLD_TIMING_VAR (start);
   rtld_timer_start (&start);
   {
@@ -2286,10 +2306,6 @@ dl_main (const ElfW(Phdr) *phdr,
 
 	_dl_relocate_object (l, l->l_scope, GLRO(dl_lazy) ? RTLD_LAZY : 0,
 			     consider_profiling);
-
-	/* Add object to slot information data if necessary.  */
-	if (l->l_tls_blocksize != 0 && __rtld_tls_init_tp_called)
-	  _dl_add_to_slotinfo (l, true);
       }
   }
   rtld_timer_stop (&relocate_time, start);
@@ -2310,12 +2326,7 @@ dl_main (const ElfW(Phdr) *phdr,
       || count_modids != _dl_count_modids ())
     ++GL(dl_tls_generation);
 
-  /* Now that we have completed relocation, the initializer data
-     for the TLS blocks has its final values and we can copy them
-     into the main thread's TLS area, which we allocated above.
-     Note: thread-local variables must only be accessed after completing
-     the next step.  */
-  _dl_allocate_tls_init (tcbp, true);
+  /* TLS .tdata copy moved before the relocation loop above.  */
 
   /* And finally install it for the main thread.  */
   if (! __rtld_tls_init_tp_called)
diff --git a/elf/tst-ifunc-tls-init-dlopen-lib.c b/elf/tst-ifunc-tls-init-dlopen-lib.c
new file mode 100644
index 0000000000..652e220b3f
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-dlopen-lib.c
@@ -0,0 +1,55 @@
+/* Shared library for tst-ifunc-tls-init-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/>.  */
+
+/* Same as tst-ifunc-tls-init-lib.c, but exercises the dlopen path.  */
+
+#define SENTINEL 0x5A5A1234
+
+static volatile __thread int sentinel
+  __attribute__ ((tls_model ("initial-exec"))) = 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")));
+
+/* Force a non-PLT relocation against the IFUNC symbol.  */
+int (*fptr) (void) = ifunc_tls;
diff --git a/elf/tst-ifunc-tls-init-lib-skeleton.c b/elf/tst-ifunc-tls-init-lib-skeleton.c
new file mode 100644
index 0000000000..7b8a1647a4
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-lib-skeleton.c
@@ -0,0 +1,57 @@
+/* Test that static-TLS initialisation works correctly with IFUNC resolvers.
+   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/>.  */
+
+/* The initial-exec TLS model keeps the access path to a single TP-relative
+   load, so the test is sensitive to whether the static TLS block has been
+   populated rather than to any __tls_get_addr / DTV-update timing.  */
+
+#define SENTINEL 0x5A5A1234
+
+/* The 'volatile' avoids constant fold optimization in impl_ok.  */
+static volatile __thread int sentinel
+  __attribute__ ((tls_model ("initial-exec"))) = 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")));
+
+/* Force a non-PLT relocation against the IFUNC symbol.  */
+int (*fptr) (void) = ifunc_tls;
diff --git a/elf/tst-ifunc-tls-init-lib1.c b/elf/tst-ifunc-tls-init-lib1.c
new file mode 100644
index 0000000000..ed9db110b1
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-lib1.c
@@ -0,0 +1 @@
+#include "tst-ifunc-tls-init-lib-skeleton.c"
diff --git a/elf/tst-ifunc-tls-init-lib2.c b/elf/tst-ifunc-tls-init-lib2.c
new file mode 100644
index 0000000000..ed9db110b1
--- /dev/null
+++ b/elf/tst-ifunc-tls-init-lib2.c
@@ -0,0 +1 @@
+#include "tst-ifunc-tls-init-lib-skeleton.c"
diff --git a/elf/tst-ifunc-tls-init.c b/elf/tst-ifunc-tls-init.c
new file mode 100644
index 0000000000..8b996b7c60
--- /dev/null
+++ b/elf/tst-ifunc-tls-init.c
@@ -0,0 +1,81 @@
+/* Check if static-TLS variables are correctly intialized in IFUNC resolvers.
+   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/>.  */
+
+/* Checks if a IFUNC resolver sees if a TLS blocker is properly initialized.
+   The tst-ifunc-tls-init-lib.so carries:
+
+   - a thread-local 'sentinel' initialised to SENTINEL.
+   - an IFUNC 'ifunc_tls' whose resolver picks impl_ok when sentinel reads
+     as SENTINEL and impl_bad (returning 0) otherwise.
+   - an IFUNC-backed function-pointer global 'fptr' whose initialiser
+     produces a non-PLT relocation.  */
+
+#include <support/check.h>
+#include <support/xdlfcn.h>
+
+#define SENTINEL 0x5A5A1234
+
+extern int ifunc_tls (void);
+extern int (*fptr) (void);
+extern int get_last_seen_sentinel (void);
+
+static void
+test_tls_ifunc (int (*check_sentinel)(void),
+		int (*check_fptr)(void),
+		int (*check_ifunc_tls)(void))
+{
+  /* Primary check: 'get_last_seen_sentinel' returns the value of the DSO's
+     thread-local 'sentinel' as observed by the resolver at the moment it ran
+     for the IFUNC reloc that initialised fptr.  The getter is a regular
+     PLT-resolved function in the DSO, so the read of the diagnostic global
+     does NOT go through a COPY relocation that could overwrite the resolver's
+     write.  */
+  TEST_COMPARE (check_sentinel (), SENTINEL);
+
+  /* Secondary check: fptr is set during IFUNC resolver call, then copied into
+     the exe's.  Returns SENTINEL only if the resolver picked impl_ok.  */
+  TEST_VERIFY (check_fptr != NULL);
+  TEST_COMPARE (check_fptr (), SENTINEL);
+
+  /* Sanity check: issue the ifunc.  */
+  TEST_COMPARE (check_ifunc_tls (), SENTINEL);
+}
+
+static int
+do_test (void)
+{
+  test_tls_ifunc (get_last_seen_sentinel, fptr, ifunc_tls);
+
+  /* Same as before, but check the dlopen path.  */
+  void *handle = xdlopen ("tst-ifunc-tls-init-lib2.so",
+			  RTLD_LAZY | RTLD_LOCAL);
+
+  int (*get_last_seen_sentinel_dlopen) (void)
+    = xdlsym (handle, "get_last_seen_sentinel");
+  int (**fptr_dlopen) (void) = xdlsym (handle, "fptr");
+  int (*ifunc_tls_dlopen) (void) = xdlsym (handle, "ifunc_tls");
+
+  test_tls_ifunc (get_last_seen_sentinel_dlopen, *fptr_dlopen,
+		  ifunc_tls_dlopen);
+
+  xdlclose (handle);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/tst-ifunc-tls-write-lib.c b/elf/tst-ifunc-tls-write-lib.c
new file mode 100644
index 0000000000..ef1dff90e0
--- /dev/null
+++ b/elf/tst-ifunc-tls-write-lib.c
@@ -0,0 +1,54 @@
+/* Shared library for tst-ifunc-tls-write.
+   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/>.  */
+
+/* The DSO defines an initial-exec thread-local 'counter' initialised to
+   SENTINEL, the IFUNC resolver (via the non-PLT IFUNC reloc on 'fptr')
+   overwrites counter with MARKER, and the test then reads counter back
+   through a getter.  */
+
+#define SENTINEL 0x5A5A1234u
+#define MARKER   0x32125A5Au
+
+/* The 'volatile' avoids constant fold optimization in impl_ok.  */
+static volatile __thread unsigned int counter
+  __attribute__ ((tls_model ("initial-exec"))) = SENTINEL;
+
+static unsigned int
+impl (void)
+{
+  return 0;
+}
+
+static unsigned int (*resolver (void)) (void)
+{
+  counter = MARKER;
+  return impl;
+}
+unsigned int ifunc_write (void) __attribute__ ((ifunc ("resolver")));
+
+/* Force the resolver rather than lazy bind on first call, which would
+   re-write counter after the test reads it).  Using a COPY'd fptr also lets
+   the test verify the resolver ran without making a PLT call that would
+   itself fire the resolver again.  */
+unsigned int (*fptr) (void) = ifunc_write;
+
+unsigned int
+get_counter (void)
+{
+  return counter;
+}
diff --git a/elf/tst-ifunc-tls-write.c b/elf/tst-ifunc-tls-write.c
new file mode 100644
index 0000000000..8c3711c161
--- /dev/null
+++ b/elf/tst-ifunc-tls-write.c
@@ -0,0 +1,34 @@
+/* Check if static-TLS variables are correctly intialized in IFUNC resolvers.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <support/check.h>
+
+#define MARKER   0x32125A5Au
+
+extern unsigned int (*fptr) (void);
+extern unsigned int get_counter (void);
+
+static int
+do_test (void)
+{
+  TEST_VERIFY (fptr != NULL);
+  TEST_COMPARE (get_counter (), MARKER);
+  return 0;
+}
+
+#include <support/test-driver.c>