[PATCH] elf: Initialize static TLS before relocation processing
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Thu May 21 12:01:07 GMT 2026
On 20/05/26 18:09, H.J. Lu wrote:
> On Thu, May 21, 2026 at 4:07 AM Adhemerval Zanella
> <adhemerval.zanella@linaro.org> wrote:
>>
>> 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.
>
> Is there a bug report? Is this a regression on x86?
It is https://sourceware.org/bugzilla/show_bug.cgi?id=34164 now and afaik it is
not a regression. The testcase on the bug report can be reproduced on old
glibcs (it shows on 2.39).
>
>> 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 scenariosn:
> Spelling?
Ack.
>
>>
>> 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.
>> ---
>> elf/Makefile | 10 ++++
>> elf/dl-reloc.c | 27 +++++----
>> elf/rtld.c | 31 ++++++----
>> elf/tst-ifunc-tls-init-dlopen-lib.c | 56 ++++++++++++++++++
>> 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, 328 insertions(+), 24 deletions(-)
>> create mode 100644 elf/tst-ifunc-tls-init-dlopen-lib.c
>> create mode 100644 elf/tst-ifunc-tls-init-lib-skeleton.c
>> create mode 100644 elf/tst-ifunc-tls-init-lib1.c
>> create mode 100644 elf/tst-ifunc-tls-init-lib2.c
>> create mode 100644 elf/tst-ifunc-tls-init.c
>> create mode 100644 elf/tst-ifunc-tls-write-lib.c
>> create mode 100644 elf/tst-ifunc-tls-write.c
>>
>> diff --git a/elf/Makefile b/elf/Makefile
>> index aef13b73ca5..ff42eb32d41 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 f1a432ac099..67299400cad 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 laterin 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 e926ec73e49..12e1b4dd71f 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 00000000000..b3e587a3f40
>> --- /dev/null
>> +++ b/elf/tst-ifunc-tls-init-dlopen-lib.c
>> @@ -0,0 +1,56 @@
>> +/* 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)
>
> 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 00000000000..d8b2a4f0f10
>> --- /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 00000000000..ed9db110b1a
>> --- /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 00000000000..ed9db110b1a
>> --- /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 00000000000..8b996b7c60c
>> --- /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 00000000000..ef1dff90e01
>> --- /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 00000000000..8c3711c161c
>> --- /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>
>> --
>> 2.43.0
>>
>
>
More information about the Libc-alpha
mailing list