[v3] elf: Avoid re-initializing already allocated TLS in dlopen (bug 31717)

Message ID 87a5kpolfw.fsf@oldenburg.str.redhat.com
State Superseded
Delegated to: Simon Chopin
Headers
Series [v3] elf: Avoid re-initializing already allocated TLS in dlopen (bug 31717) |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Testing passed
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Testing passed
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Testing passed
linaro-tcwg-bot/tcwg_glibc_check--master-arm success Testing passed

Commit Message

Florian Weimer May 16, 2024, 2:55 p.m. UTC
  The old code used l_init_called as an indicator for whether TLS
initialization was complete.  However, it is possible that
TLS for an object is initialized, written to, and then dlopen
for this object is called again, and l_init_called is not true at
this point.  Previously, this resulted in TLS being initialized
twice, discarding any interim writes (technically introducing a
use-after-free bug even).

This commit introduces an explicit per-object flag, l_tls_in_slotinfo.
It indicates whether _dl_add_to_slotinfo has been called for this
object.  This flag is used to avoid double-initialization of TLS.
In update_tls_slotinfo, the first_static_tls micro-optimization
is removed because preserving the initalization flag for subsequent
use by the second loop for static TLS is a bit complicated, and
another per-object flag does not seem to be worth it.  Furthermore,
the l_init_called flag is dropped from the second loop (for static
TLS initialization) because l_need_tls_init on its own prevents
double-initialization.

The remaining l_init_called usage in resize_scopes and update_scopes
is just an optimization due to the use of scope_has_map, so it is
not changed in this commit.

Only the dlopen-based test case, elf/tst-dlopen-tlsreinit, fails
before the fix.  The other two (elf/tst-dlopen-tlsreinit2 for
direct linking, elf/tst-dlopen-tlsreinit3 for direct linking
with auditing) are only added for completeness.

Reported-by: Ben Woodard <woodard@redhat.com>

---
v3: Reworded _dl_add_to_slotinfo function comment.

 elf/Makefile                   | 27 ++++++++++++
 elf/dl-open.c                  | 35 +++------------
 elf/dl-tls.c                   |  8 +++-
 elf/rtld.c                     |  2 +-
 elf/tst-dlopen-tlsreinit.c     | 36 ++++++++++++++++
 elf/tst-dlopen-tlsreinit2.c    | 35 +++++++++++++++
 elf/tst-dlopen-tlsreinit3.c    |  2 +
 elf/tst-dlopen-tlsreinitmod1.c | 20 +++++++++
 elf/tst-dlopen-tlsreinitmod2.c | 30 +++++++++++++
 elf/tst-dlopen-tlsreinitmod3.c | 96 ++++++++++++++++++++++++++++++++++++++++++
 include/link.h                 |  1 +
 sysdeps/generic/ldsodefs.h     | 26 +++++++++---
 12 files changed, 280 insertions(+), 38 deletions(-)


base-commit: 1dbf2bef7934cee9829d875f11968d6ff1fee77f
  

Comments

Simon Chopin June 11, 2024, 2:03 p.m. UTC | #1
Hi,

On jeu. 16 mai 2024 16:55:47, Florian Weimer wrote:
> The old code used l_init_called as an indicator for whether TLS
> initialization was complete.  However, it is possible that
> TLS for an object is initialized, written to, and then dlopen
> for this object is called again, and l_init_called is not true at
> this point.  Previously, this resulted in TLS being initialized
> twice, discarding any interim writes (technically introducing a
> use-after-free bug even).
>
> This commit introduces an explicit per-object flag, l_tls_in_slotinfo.
> It indicates whether _dl_add_to_slotinfo has been called for this
> object.  This flag is used to avoid double-initialization of TLS.
> In update_tls_slotinfo, the first_static_tls micro-optimization
> is removed because preserving the initalization flag for subsequent
> use by the second loop for static TLS is a bit complicated, and
> another per-object flag does not seem to be worth it.  Furthermore,
> the l_init_called flag is dropped from the second loop (for static
> TLS initialization) because l_need_tls_init on its own prevents
> double-initialization.
>
> The remaining l_init_called usage in resize_scopes and update_scopes
> is just an optimization due to the use of scope_has_map, so it is
> not changed in this commit.
>
> Only the dlopen-based test case, elf/tst-dlopen-tlsreinit, fails
> before the fix.  The other two (elf/tst-dlopen-tlsreinit2 for
> direct linking, elf/tst-dlopen-tlsreinit3 for direct linking
> with auditing) are only added for completeness.
>
> Reported-by: Ben Woodard <woodard@redhat.com>

Overall looks good to me, I'm just unclear on the barrier bit in
tst-dlopen-tlsreinitmod3.c?

>
> ---
> v3: Reworded _dl_add_to_slotinfo function comment.
>
>  elf/Makefile                   | 27 ++++++++++++
>  elf/dl-open.c                  | 35 +++------------
>  elf/dl-tls.c                   |  8 +++-
>  elf/rtld.c                     |  2 +-
>  elf/tst-dlopen-tlsreinit.c     | 36 ++++++++++++++++
>  elf/tst-dlopen-tlsreinit2.c    | 35 +++++++++++++++
>  elf/tst-dlopen-tlsreinit3.c    |  2 +
>  elf/tst-dlopen-tlsreinitmod1.c | 20 +++++++++
>  elf/tst-dlopen-tlsreinitmod2.c | 30 +++++++++++++
>  elf/tst-dlopen-tlsreinitmod3.c | 96 ++++++++++++++++++++++++++++++++++++++++++
>  include/link.h                 |  1 +
>  sysdeps/generic/ldsodefs.h     | 26 +++++++++---
>  12 files changed, 280 insertions(+), 38 deletions(-)
>
> diff --git a/elf/Makefile b/elf/Makefile
> index 280e777c19..4cecb4f382 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -416,6 +416,9 @@ tests += \
>    tst-dlmopen4 \
>    tst-dlopen-self \
>    tst-dlopen-tlsmodid \
> +  tst-dlopen-tlsreinit \
> +  tst-dlopen-tlsreinit2 \
> +  tst-dlopen-tlsreinit3 \
>    tst-dlopenfail \
>    tst-dlopenfail-2 \
>    tst-dlopenrpath \
> @@ -846,6 +849,9 @@ modules-names += \
>    tst-dlmopen-twice-mod1 \
>    tst-dlmopen-twice-mod2 \
>    tst-dlmopen1mod \
> +  tst-dlopen-tlsreinitmod1 \
> +  tst-dlopen-tlsreinitmod2 \
> +  tst-dlopen-tlsreinitmod3 \
>    tst-dlopenfaillinkmod \
>    tst-dlopenfailmod1 \
>    tst-dlopenfailmod2 \
> @@ -3083,3 +3089,24 @@ CFLAGS-tst-gnu2-tls2mod0.c += -mtls-dialect=$(have-mtls-descriptor)
>  CFLAGS-tst-gnu2-tls2mod1.c += -mtls-dialect=$(have-mtls-descriptor)
>  CFLAGS-tst-gnu2-tls2mod2.c += -mtls-dialect=$(have-mtls-descriptor)
>  endif
> +
> +# Order matters here.  The test needs the constructor for
> +# tst-dlopen-tlsreinitmod2.so to be called first.
> +LDFLAGS-tst-dlopen-tlsreinitmod1.so = -Wl,--no-as-needed
> +$(objpfx)tst-dlopen-tlsreinitmod1.so: \
> +  $(objpfx)tst-dlopen-tlsreinitmod3.so $(objpfx)tst-dlopen-tlsreinitmod2.so
> +LDFLAGS-tst-dlopen-tlsreinit2 = -Wl,--no-as-needed
> +$(objpfx)tst-dlopen-tlsreinit2: \
> +  $(objpfx)tst-dlopen-tlsreinitmod3.so $(objpfx)tst-dlopen-tlsreinitmod2.so
> +LDFLAGS-tst-dlopen-tlsreinit3 = -Wl,--no-as-needed
> +$(objpfx)tst-dlopen-tlsreinit3: \
> +  $(objpfx)tst-dlopen-tlsreinitmod3.so $(objpfx)tst-dlopen-tlsreinitmod2.so
> +# tst-dlopen-tlsreinitmod2.so is underlinked and refers to
> +# tst-dlopen-tlsreinitmod3.so.  The dependency is provided via
> +# $(objpfx)tst-dlopen-tlsreinitmod1.so.
> +tst-dlopen-tlsreinitmod2.so-no-z-defs = yes
> +$(objpfx)tst-dlopen-tlsreinit.out: $(objpfx)tst-dlopen-tlsreinitmod1.so \
> +  $(objpfx)tst-dlopen-tlsreinitmod2.so $(objpfx)tst-dlopen-tlsreinitmod3.so
> +$(objpfx)tst-dlopen-tlsreinit3.out: $(objpfx)tst-auditmod1.so
> +# Reuse an audit module which provides ample debug logging.
> +tst-dlopen-tlsreinit3-ENV = LD_AUDIT=$(objpfx)tst-auditmod1.so
> diff --git a/elf/dl-open.c b/elf/dl-open.c
> index c378da16c0..8556e7bd2f 100644
> --- a/elf/dl-open.c
> +++ b/elf/dl-open.c
> @@ -363,17 +363,8 @@ resize_tls_slotinfo (struct link_map *new)
>  {
>    bool any_tls = false;
>    for (unsigned int i = 0; i < new->l_searchlist.r_nlist; ++i)
> -    {
> -      struct link_map *imap = new->l_searchlist.r_list[i];
> -
> -      /* Only add TLS memory if this object is loaded now and
> -	 therefore is not yet initialized.  */
> -      if (! imap->l_init_called && imap->l_tls_blocksize > 0)
> -	{
> -	  _dl_add_to_slotinfo (imap, false);
> -	  any_tls = true;
> -	}
> -    }
> +    if (_dl_add_to_slotinfo (new->l_searchlist.r_list[i], false))
> +      any_tls = true;

OK, l_init_called check removal addressed in the commit log,
l_tls_blocksize now checked in _dl_add_to_slotinfo.

>    return any_tls;
>  }
>
> @@ -383,22 +374,8 @@ resize_tls_slotinfo (struct link_map *new)
>  static void
>  update_tls_slotinfo (struct link_map *new)
>  {
> -  unsigned int first_static_tls = new->l_searchlist.r_nlist;
>    for (unsigned int i = 0; i < new->l_searchlist.r_nlist; ++i)
> -    {
> -      struct link_map *imap = new->l_searchlist.r_list[i];
> -
> -      /* Only add TLS memory if this object is loaded now and
> -	 therefore is not yet initialized.  */
> -      if (! imap->l_init_called && imap->l_tls_blocksize > 0)
> -	{
> -	  _dl_add_to_slotinfo (imap, true);
> -
> -	  if (imap->l_need_tls_init
> -	      && first_static_tls == new->l_searchlist.r_nlist)
> -	    first_static_tls = i;
> -	}
> -    }
> +    _dl_add_to_slotinfo (new->l_searchlist.r_list[i], true);

OK, first_static_tls only use removed below.

>
>    size_t newgen = GL(dl_tls_generation) + 1;
>    if (__glibc_unlikely (newgen == 0))
> @@ -410,13 +387,11 @@ TLS generation counter wrapped!  Please report this."));
>    /* We need a second pass for static tls data, because
>       _dl_update_slotinfo must not be run while calls to
>       _dl_add_to_slotinfo are still pending.  */
> -  for (unsigned int i = first_static_tls; i < new->l_searchlist.r_nlist; ++i)
> +  for (unsigned int i = 0; i < new->l_searchlist.r_nlist; ++i)

OK, adressed in commit log.

>      {
>        struct link_map *imap = new->l_searchlist.r_list[i];
>
> -      if (imap->l_need_tls_init
> -	  && ! imap->l_init_called
> -	  && imap->l_tls_blocksize > 0)
> +      if (imap->l_need_tls_init && imap->l_tls_blocksize > 0)

OK, l_init_called adressed in log.

>  	{
>  	  /* For static TLS we have to allocate the memory here and
>  	     now, but we can delay updating the DTV.  */
> diff --git a/elf/dl-tls.c b/elf/dl-tls.c
> index 7b3dd9ab60..4d55ddd6c9 100644
> --- a/elf/dl-tls.c
> +++ b/elf/dl-tls.c
> @@ -1012,9 +1012,12 @@ _dl_tls_get_addr_soft (struct link_map *l)
>  }
>
>
> -void
> +bool
>  _dl_add_to_slotinfo (struct link_map *l, bool do_add)
>  {
> +  if (l->l_tls_blocksize == 0 || l->l_tls_in_slotinfo)
> +    return false;
> +

OK

>    /* Now that we know the object is loaded successfully add
>       modules containing TLS data to the dtv info table.  We
>       might have to increase its size.  */
> @@ -1068,7 +1071,10 @@ cannot create TLS data structures"));
>        atomic_store_relaxed (&listp->slotinfo[idx].map, l);
>        atomic_store_relaxed (&listp->slotinfo[idx].gen,
>  			    GL(dl_tls_generation) + 1);
> +      l->l_tls_in_slotinfo = true;
>      }
> +
> +  return true;

OK

>  }
>
>  #if PTHREAD_IN_LIBC
> diff --git a/elf/rtld.c b/elf/rtld.c
> index e9525ea987..0b0fbcdbfb 100644
> --- a/elf/rtld.c
> +++ b/elf/rtld.c
> @@ -2313,7 +2313,7 @@ dl_main (const ElfW(Phdr) *phdr,
>  			       consider_profiling);
>
>  	/* Add object to slot information data if necessasy.  */
> -	if (l->l_tls_blocksize != 0 && __rtld_tls_init_tp_called)
> +	if (__rtld_tls_init_tp_called)

OK, now checked within _dl_add_to_slotinfo.

>  	  _dl_add_to_slotinfo (l, true);
>        }
>    }
> diff --git a/elf/tst-dlopen-tlsreinit.c b/elf/tst-dlopen-tlsreinit.c
> new file mode 100644
> index 0000000000..7b8611fbe3
> --- /dev/null
> +++ b/elf/tst-dlopen-tlsreinit.c
> @@ -0,0 +1,36 @@
> +/* Test that dlopen preserves already accessed TLS (bug 31717).
> +   Copyright (C) 2024 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 <stdbool.h>
> +#include <support/check.h>
> +#include <support/xdlfcn.h>
> +
> +static int
> +do_test (void)
> +{
> +  void *handle = xdlopen ("tst-dlopen-tlsreinitmod1.so", RTLD_NOW);
> +
> +  bool *tlsreinitmod3_tested = xdlsym (handle, "tlsreinitmod3_tested");
> +  TEST_VERIFY (*tlsreinitmod3_tested);
> +
> +  xdlclose (handle);
> +
> +  return 0;
> +}
> +
> +#include <support/test-driver.c>

OK, dlopen(mod1) brings in mod2 and mod3, mod3 self-reopens

> diff --git a/elf/tst-dlopen-tlsreinit2.c b/elf/tst-dlopen-tlsreinit2.c
> new file mode 100644
> index 0000000000..7d32c35e4f
> --- /dev/null
> +++ b/elf/tst-dlopen-tlsreinit2.c
> @@ -0,0 +1,35 @@
> +/* Test that dlopen preserves already accessed TLS (bug 31717).
> +   Variant with initially-linked modules.
> +   Copyright (C) 2024 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 <stdbool.h>
> +#include <support/check.h>
> +#include <support/xdlfcn.h>
> +
> +
> +static int
> +do_test (void)
> +{
> +  /* Defined in tst-dlopen-tlsreinitmod3.so.  */
> +  extern bool tlsreinitmod3_tested;
> +  TEST_VERIFY (tlsreinitmod3_tested);
> +
> +  return 0;
> +}

OK, standard library loading of mod2 and mod3, mod3 self-reopens

> +
> +#include <support/test-driver.c>
> diff --git a/elf/tst-dlopen-tlsreinit3.c b/elf/tst-dlopen-tlsreinit3.c
> new file mode 100644
> index 0000000000..344c9211ab
> --- /dev/null
> +++ b/elf/tst-dlopen-tlsreinit3.c
> @@ -0,0 +1,2 @@
> +/* Same code, but run with LD_AUDIT=tst-auditmod1.so.  */
> +#include "tst-dlopen-tlsreinit2.c"

OK

> diff --git a/elf/tst-dlopen-tlsreinitmod1.c b/elf/tst-dlopen-tlsreinitmod1.c
> new file mode 100644
> index 0000000000..354cc3de51
> --- /dev/null
> +++ b/elf/tst-dlopen-tlsreinitmod1.c
> @@ -0,0 +1,20 @@
> +/* Test that dlopen preserves already accessed TLS (bug 31717), module 1.
> +   Copyright (C) 2024 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 module triggers loading of tst-dlopen-tlsreinitmod2.so and
> +   tst-dlopen-tlsreinitmod3.so.  */

OK

> diff --git a/elf/tst-dlopen-tlsreinitmod2.c b/elf/tst-dlopen-tlsreinitmod2.c
> new file mode 100644
> index 0000000000..677e69bd35
> --- /dev/null
> +++ b/elf/tst-dlopen-tlsreinitmod2.c
> @@ -0,0 +1,30 @@
> +/* Test that dlopen preserves already accessed TLS (bug 31717), module 2.
> +   Copyright (C) 2024 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 <stdio.h>
> +
> +/* Defined in tst-dlopen-tlsreinitmod3.so.  This an underlinked symbol
> +   dependency.  */
> +extern void call_tlsreinitmod3 (void);
> +
> +static void __attribute__ ((constructor))
> +tlsreinitmod2_init (void)
> +{
> +  puts ("info: constructor of tst-dlopen-tlsreinitmod2.so invoked");
> +  call_tlsreinitmod3 ();
> +}

OK

> diff --git a/elf/tst-dlopen-tlsreinitmod3.c b/elf/tst-dlopen-tlsreinitmod3.c
> new file mode 100644
> index 0000000000..1a5fda0a14
> --- /dev/null
> +++ b/elf/tst-dlopen-tlsreinitmod3.c
> @@ -0,0 +1,96 @@
> +/* Test that dlopen preserves already accessed TLS (bug 31717), module 3.
> +   Copyright (C) 2024 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 <dlfcn.h>
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <unistd.h>
> +
> +/* Used to verify from the main program that the test ran.  */
> +bool tlsreinitmod3_tested;
> +
> +/* This TLS variable must not revert back to the initial state after
> +   dlopen.  */
> +static __thread int tlsreinitmod3_state = 1;
> +
> +/* Set from the ELF constructor during dlopen.  */
> +static bool tlsreinitmod3_constructed;
> +
> +/* Second half of test, behind a compiler barrier.  */
> +void call_tlsreinitmod3_tail (void *self) __attribute__ ((weak));

I'm a bit confused about what constitutes the compiler barrier here? Is
that just because of the weak attribute?
Also, I'm not clear on the need for a barrier in the first place, what
are the operations that we fear would be reordered here?

> +
> +/* Called from tst-dlopen-tlsreinitmod2.so.  */
> +void
> +call_tlsreinitmod3 (void)
> +{
> +  printf ("info: call_tlsreinitmod3 invoked (state=%d)\n",
> +          tlsreinitmod3_state);
> +
> +  if (tlsreinitmod3_constructed)
> +    {
> +      puts ("error: call_tlsreinitmod3 called after ELF constructor");
> +      /* Cannot rely on test harness due to dynamic linking.  */
> +      _exit (1);
> +    }
> +
> +  tlsreinitmod3_state = 2;
> +
> +  /* Self-dlopen.  This will run the ELF constructor.   */
> +  void *self = dlopen ("tst-dlopen-tlsreinitmod3.so", RTLD_NOW);
> +  if (self == NULL)
> +    {
> +      printf ("error: dlopen: %s\n", dlerror ());
> +      /* Cannot rely on test harness due to dynamic linking.  */
> +      _exit (1);
> +    }
> +
> +  call_tlsreinitmod3_tail (self);
> +}
> +
> +void
> +call_tlsreinitmod3_tail (void *self)
> +{
> +  printf ("info: dlopen returned in tlsreinitmod3 (state=%d)\n",
> +          tlsreinitmod3_state);
> +
> +  if (!tlsreinitmod3_constructed)
> +    {
> +      puts ("error: dlopen did not call tlsreinitmod3 ELF constructor");
> +      /* Cannot rely on test harness due to dynamic linking.  */
> +      _exit (1);
> +    }
> +
> +  if (tlsreinitmod3_state != 2)
> +    {
> +      puts ("error: TLS state reverted in tlsreinitmod3");
> +      /* Cannot rely on test harness due to dynamic linking.  */
> +      _exit (1);
> +    }
> +
> +  dlclose (self);
> +
> +  /* Signal test completion to the main program.  */
> +  tlsreinitmod3_tested = true;
> +}
> +
> +static void __attribute__ ((constructor))
> +tlsreinitmod3_init (void)
> +{
> +  puts ("info: constructor of tst-dlopen-tlsreinitmod3.so invoked");
> +  tlsreinitmod3_constructed = true;
> +}

OK

> diff --git a/include/link.h b/include/link.h
> index cb0d7d8e2f..5ed445d5a6 100644
> --- a/include/link.h
> +++ b/include/link.h
> @@ -212,6 +212,7 @@ struct link_map
>      unsigned int l_find_object_processed:1; /* Zero if _dl_find_object_update
>  					       needs to process this
>  					       lt_library map.  */
> +    unsigned int l_tls_in_slotinfo:1; /* TLS slotinfo updated in dlopen.  */

OK

>
>      /* NODELETE status of the map.  Only valid for maps of type
>         lt_loaded.  Lazy binding sets l_nodelete_active directly,
> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index 50f58a60e3..e027aec137 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -1242,12 +1242,26 @@ extern int _dl_scope_free (void *) attribute_hidden;
>
>
>  /* Add module to slot information data.  If DO_ADD is false, only the
> -   required memory is allocated.  Must be called with GL
> -   (dl_load_tls_lock) acquired.  If the function has already been called
> -   for the link map L with !do_add, then this function will not raise
> -   an exception, otherwise it is possible that it encounters a memory
> -   allocation failure.  */
> -extern void _dl_add_to_slotinfo (struct link_map *l, bool do_add)
> +   required memory is allocated.  Must be called with
> +   GL (dl_load_tls_lock) acquired.  If the function has already been
> +   called for the link map L with !DO_ADD, then this function will not
> +   raise an exception, otherwise it is possible that it encounters a
> +   memory allocation failure.
> +
> +   Return false if L has already been added to the slotinfo data, or
> +   if L has no TLS data.  If the returned value is true, L has been
> +   added with this call (DO_ADD), or has been added in a previous call
> +   (!DO_ADD).
> +
> +   The expected usage is as follows: Call _dl_add_to_slotinfo for
> +   several link maps with DO_ADD set to false, and record if any calls
> +   result in a true result.  If there was a true result, call
> +   _dl_add_to_slotinfo again, this time with DO_ADD set to true.  (For
> +   simplicity, it's possible to call the function for link maps where
> +   the previous result was false.)  The return value from the second
> +   round of calls can be ignored.  If there was true result initially,
> +   call _dl_update_slotinfo to update the TLS generation counter.  */
> +extern bool _dl_add_to_slotinfo (struct link_map *l, bool do_add)
>    attribute_hidden;
>
>  /* Update slot information data for at least the generation of the

OK, thanks for the added paragraph on expected usage.

>
> base-commit: 1dbf2bef7934cee9829d875f11968d6ff1fee77f
>
  
Florian Weimer June 14, 2024, 1:24 p.m. UTC | #2
* Simon Chopin:

>> +/* Second half of test, behind a compiler barrier.  */
>> +void call_tlsreinitmod3_tail (void *self) __attribute__ ((weak));
>
> I'm a bit confused about what constitutes the compiler barrier here? Is
> that just because of the weak attribute?

Yes, it indicates to the compiler that the symbol can be interposed from
somewhere else.  Otherwise, you have to use noinline and noipa, and not
all GCC versions support the latter.

> Also, I'm not clear on the need for a barrier in the first place, what
> are the operations that we fear would be reordered here?

It's about the implicit __tls_get_addr call to implement dynamic TLS.
I'm going to add a comment.

Thanks,
Florian
  

Patch

diff --git a/elf/Makefile b/elf/Makefile
index 280e777c19..4cecb4f382 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -416,6 +416,9 @@  tests += \
   tst-dlmopen4 \
   tst-dlopen-self \
   tst-dlopen-tlsmodid \
+  tst-dlopen-tlsreinit \
+  tst-dlopen-tlsreinit2 \
+  tst-dlopen-tlsreinit3 \
   tst-dlopenfail \
   tst-dlopenfail-2 \
   tst-dlopenrpath \
@@ -846,6 +849,9 @@  modules-names += \
   tst-dlmopen-twice-mod1 \
   tst-dlmopen-twice-mod2 \
   tst-dlmopen1mod \
+  tst-dlopen-tlsreinitmod1 \
+  tst-dlopen-tlsreinitmod2 \
+  tst-dlopen-tlsreinitmod3 \
   tst-dlopenfaillinkmod \
   tst-dlopenfailmod1 \
   tst-dlopenfailmod2 \
@@ -3083,3 +3089,24 @@  CFLAGS-tst-gnu2-tls2mod0.c += -mtls-dialect=$(have-mtls-descriptor)
 CFLAGS-tst-gnu2-tls2mod1.c += -mtls-dialect=$(have-mtls-descriptor)
 CFLAGS-tst-gnu2-tls2mod2.c += -mtls-dialect=$(have-mtls-descriptor)
 endif
+
+# Order matters here.  The test needs the constructor for
+# tst-dlopen-tlsreinitmod2.so to be called first.
+LDFLAGS-tst-dlopen-tlsreinitmod1.so = -Wl,--no-as-needed
+$(objpfx)tst-dlopen-tlsreinitmod1.so: \
+  $(objpfx)tst-dlopen-tlsreinitmod3.so $(objpfx)tst-dlopen-tlsreinitmod2.so
+LDFLAGS-tst-dlopen-tlsreinit2 = -Wl,--no-as-needed
+$(objpfx)tst-dlopen-tlsreinit2: \
+  $(objpfx)tst-dlopen-tlsreinitmod3.so $(objpfx)tst-dlopen-tlsreinitmod2.so
+LDFLAGS-tst-dlopen-tlsreinit3 = -Wl,--no-as-needed
+$(objpfx)tst-dlopen-tlsreinit3: \
+  $(objpfx)tst-dlopen-tlsreinitmod3.so $(objpfx)tst-dlopen-tlsreinitmod2.so
+# tst-dlopen-tlsreinitmod2.so is underlinked and refers to
+# tst-dlopen-tlsreinitmod3.so.  The dependency is provided via
+# $(objpfx)tst-dlopen-tlsreinitmod1.so.
+tst-dlopen-tlsreinitmod2.so-no-z-defs = yes
+$(objpfx)tst-dlopen-tlsreinit.out: $(objpfx)tst-dlopen-tlsreinitmod1.so \
+  $(objpfx)tst-dlopen-tlsreinitmod2.so $(objpfx)tst-dlopen-tlsreinitmod3.so
+$(objpfx)tst-dlopen-tlsreinit3.out: $(objpfx)tst-auditmod1.so
+# Reuse an audit module which provides ample debug logging.
+tst-dlopen-tlsreinit3-ENV = LD_AUDIT=$(objpfx)tst-auditmod1.so
diff --git a/elf/dl-open.c b/elf/dl-open.c
index c378da16c0..8556e7bd2f 100644
--- a/elf/dl-open.c
+++ b/elf/dl-open.c
@@ -363,17 +363,8 @@  resize_tls_slotinfo (struct link_map *new)
 {
   bool any_tls = false;
   for (unsigned int i = 0; i < new->l_searchlist.r_nlist; ++i)
-    {
-      struct link_map *imap = new->l_searchlist.r_list[i];
-
-      /* Only add TLS memory if this object is loaded now and
-	 therefore is not yet initialized.  */
-      if (! imap->l_init_called && imap->l_tls_blocksize > 0)
-	{
-	  _dl_add_to_slotinfo (imap, false);
-	  any_tls = true;
-	}
-    }
+    if (_dl_add_to_slotinfo (new->l_searchlist.r_list[i], false))
+      any_tls = true;
   return any_tls;
 }
 
@@ -383,22 +374,8 @@  resize_tls_slotinfo (struct link_map *new)
 static void
 update_tls_slotinfo (struct link_map *new)
 {
-  unsigned int first_static_tls = new->l_searchlist.r_nlist;
   for (unsigned int i = 0; i < new->l_searchlist.r_nlist; ++i)
-    {
-      struct link_map *imap = new->l_searchlist.r_list[i];
-
-      /* Only add TLS memory if this object is loaded now and
-	 therefore is not yet initialized.  */
-      if (! imap->l_init_called && imap->l_tls_blocksize > 0)
-	{
-	  _dl_add_to_slotinfo (imap, true);
-
-	  if (imap->l_need_tls_init
-	      && first_static_tls == new->l_searchlist.r_nlist)
-	    first_static_tls = i;
-	}
-    }
+    _dl_add_to_slotinfo (new->l_searchlist.r_list[i], true);
 
   size_t newgen = GL(dl_tls_generation) + 1;
   if (__glibc_unlikely (newgen == 0))
@@ -410,13 +387,11 @@  TLS generation counter wrapped!  Please report this."));
   /* We need a second pass for static tls data, because
      _dl_update_slotinfo must not be run while calls to
      _dl_add_to_slotinfo are still pending.  */
-  for (unsigned int i = first_static_tls; i < new->l_searchlist.r_nlist; ++i)
+  for (unsigned int i = 0; i < new->l_searchlist.r_nlist; ++i)
     {
       struct link_map *imap = new->l_searchlist.r_list[i];
 
-      if (imap->l_need_tls_init
-	  && ! imap->l_init_called
-	  && imap->l_tls_blocksize > 0)
+      if (imap->l_need_tls_init && imap->l_tls_blocksize > 0)
 	{
 	  /* For static TLS we have to allocate the memory here and
 	     now, but we can delay updating the DTV.  */
diff --git a/elf/dl-tls.c b/elf/dl-tls.c
index 7b3dd9ab60..4d55ddd6c9 100644
--- a/elf/dl-tls.c
+++ b/elf/dl-tls.c
@@ -1012,9 +1012,12 @@  _dl_tls_get_addr_soft (struct link_map *l)
 }
 
 
-void
+bool
 _dl_add_to_slotinfo (struct link_map *l, bool do_add)
 {
+  if (l->l_tls_blocksize == 0 || l->l_tls_in_slotinfo)
+    return false;
+
   /* Now that we know the object is loaded successfully add
      modules containing TLS data to the dtv info table.  We
      might have to increase its size.  */
@@ -1068,7 +1071,10 @@  cannot create TLS data structures"));
       atomic_store_relaxed (&listp->slotinfo[idx].map, l);
       atomic_store_relaxed (&listp->slotinfo[idx].gen,
 			    GL(dl_tls_generation) + 1);
+      l->l_tls_in_slotinfo = true;
     }
+
+  return true;
 }
 
 #if PTHREAD_IN_LIBC
diff --git a/elf/rtld.c b/elf/rtld.c
index e9525ea987..0b0fbcdbfb 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -2313,7 +2313,7 @@  dl_main (const ElfW(Phdr) *phdr,
 			       consider_profiling);
 
 	/* Add object to slot information data if necessasy.  */
-	if (l->l_tls_blocksize != 0 && __rtld_tls_init_tp_called)
+	if (__rtld_tls_init_tp_called)
 	  _dl_add_to_slotinfo (l, true);
       }
   }
diff --git a/elf/tst-dlopen-tlsreinit.c b/elf/tst-dlopen-tlsreinit.c
new file mode 100644
index 0000000000..7b8611fbe3
--- /dev/null
+++ b/elf/tst-dlopen-tlsreinit.c
@@ -0,0 +1,36 @@ 
+/* Test that dlopen preserves already accessed TLS (bug 31717).
+   Copyright (C) 2024 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 <stdbool.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+
+static int
+do_test (void)
+{
+  void *handle = xdlopen ("tst-dlopen-tlsreinitmod1.so", RTLD_NOW);
+
+  bool *tlsreinitmod3_tested = xdlsym (handle, "tlsreinitmod3_tested");
+  TEST_VERIFY (*tlsreinitmod3_tested);
+
+  xdlclose (handle);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/tst-dlopen-tlsreinit2.c b/elf/tst-dlopen-tlsreinit2.c
new file mode 100644
index 0000000000..7d32c35e4f
--- /dev/null
+++ b/elf/tst-dlopen-tlsreinit2.c
@@ -0,0 +1,35 @@ 
+/* Test that dlopen preserves already accessed TLS (bug 31717).
+   Variant with initially-linked modules.
+   Copyright (C) 2024 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 <stdbool.h>
+#include <support/check.h>
+#include <support/xdlfcn.h>
+
+
+static int
+do_test (void)
+{
+  /* Defined in tst-dlopen-tlsreinitmod3.so.  */
+  extern bool tlsreinitmod3_tested;
+  TEST_VERIFY (tlsreinitmod3_tested);
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/tst-dlopen-tlsreinit3.c b/elf/tst-dlopen-tlsreinit3.c
new file mode 100644
index 0000000000..344c9211ab
--- /dev/null
+++ b/elf/tst-dlopen-tlsreinit3.c
@@ -0,0 +1,2 @@ 
+/* Same code, but run with LD_AUDIT=tst-auditmod1.so.  */
+#include "tst-dlopen-tlsreinit2.c"
diff --git a/elf/tst-dlopen-tlsreinitmod1.c b/elf/tst-dlopen-tlsreinitmod1.c
new file mode 100644
index 0000000000..354cc3de51
--- /dev/null
+++ b/elf/tst-dlopen-tlsreinitmod1.c
@@ -0,0 +1,20 @@ 
+/* Test that dlopen preserves already accessed TLS (bug 31717), module 1.
+   Copyright (C) 2024 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 module triggers loading of tst-dlopen-tlsreinitmod2.so and
+   tst-dlopen-tlsreinitmod3.so.  */
diff --git a/elf/tst-dlopen-tlsreinitmod2.c b/elf/tst-dlopen-tlsreinitmod2.c
new file mode 100644
index 0000000000..677e69bd35
--- /dev/null
+++ b/elf/tst-dlopen-tlsreinitmod2.c
@@ -0,0 +1,30 @@ 
+/* Test that dlopen preserves already accessed TLS (bug 31717), module 2.
+   Copyright (C) 2024 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 <stdio.h>
+
+/* Defined in tst-dlopen-tlsreinitmod3.so.  This an underlinked symbol
+   dependency.  */
+extern void call_tlsreinitmod3 (void);
+
+static void __attribute__ ((constructor))
+tlsreinitmod2_init (void)
+{
+  puts ("info: constructor of tst-dlopen-tlsreinitmod2.so invoked");
+  call_tlsreinitmod3 ();
+}
diff --git a/elf/tst-dlopen-tlsreinitmod3.c b/elf/tst-dlopen-tlsreinitmod3.c
new file mode 100644
index 0000000000..1a5fda0a14
--- /dev/null
+++ b/elf/tst-dlopen-tlsreinitmod3.c
@@ -0,0 +1,96 @@ 
+/* Test that dlopen preserves already accessed TLS (bug 31717), module 3.
+   Copyright (C) 2024 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 <dlfcn.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <unistd.h>
+
+/* Used to verify from the main program that the test ran.  */
+bool tlsreinitmod3_tested;
+
+/* This TLS variable must not revert back to the initial state after
+   dlopen.  */
+static __thread int tlsreinitmod3_state = 1;
+
+/* Set from the ELF constructor during dlopen.  */
+static bool tlsreinitmod3_constructed;
+
+/* Second half of test, behind a compiler barrier.  */
+void call_tlsreinitmod3_tail (void *self) __attribute__ ((weak));
+
+/* Called from tst-dlopen-tlsreinitmod2.so.  */
+void
+call_tlsreinitmod3 (void)
+{
+  printf ("info: call_tlsreinitmod3 invoked (state=%d)\n",
+          tlsreinitmod3_state);
+
+  if (tlsreinitmod3_constructed)
+    {
+      puts ("error: call_tlsreinitmod3 called after ELF constructor");
+      /* Cannot rely on test harness due to dynamic linking.  */
+      _exit (1);
+    }
+
+  tlsreinitmod3_state = 2;
+
+  /* Self-dlopen.  This will run the ELF constructor.   */
+  void *self = dlopen ("tst-dlopen-tlsreinitmod3.so", RTLD_NOW);
+  if (self == NULL)
+    {
+      printf ("error: dlopen: %s\n", dlerror ());
+      /* Cannot rely on test harness due to dynamic linking.  */
+      _exit (1);
+    }
+
+  call_tlsreinitmod3_tail (self);
+}
+
+void
+call_tlsreinitmod3_tail (void *self)
+{
+  printf ("info: dlopen returned in tlsreinitmod3 (state=%d)\n",
+          tlsreinitmod3_state);
+
+  if (!tlsreinitmod3_constructed)
+    {
+      puts ("error: dlopen did not call tlsreinitmod3 ELF constructor");
+      /* Cannot rely on test harness due to dynamic linking.  */
+      _exit (1);
+    }
+
+  if (tlsreinitmod3_state != 2)
+    {
+      puts ("error: TLS state reverted in tlsreinitmod3");
+      /* Cannot rely on test harness due to dynamic linking.  */
+      _exit (1);
+    }
+
+  dlclose (self);
+
+  /* Signal test completion to the main program.  */
+  tlsreinitmod3_tested = true;
+}
+
+static void __attribute__ ((constructor))
+tlsreinitmod3_init (void)
+{
+  puts ("info: constructor of tst-dlopen-tlsreinitmod3.so invoked");
+  tlsreinitmod3_constructed = true;
+}
diff --git a/include/link.h b/include/link.h
index cb0d7d8e2f..5ed445d5a6 100644
--- a/include/link.h
+++ b/include/link.h
@@ -212,6 +212,7 @@  struct link_map
     unsigned int l_find_object_processed:1; /* Zero if _dl_find_object_update
 					       needs to process this
 					       lt_library map.  */
+    unsigned int l_tls_in_slotinfo:1; /* TLS slotinfo updated in dlopen.  */
 
     /* NODELETE status of the map.  Only valid for maps of type
        lt_loaded.  Lazy binding sets l_nodelete_active directly,
diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
index 50f58a60e3..e027aec137 100644
--- a/sysdeps/generic/ldsodefs.h
+++ b/sysdeps/generic/ldsodefs.h
@@ -1242,12 +1242,26 @@  extern int _dl_scope_free (void *) attribute_hidden;
 
 
 /* Add module to slot information data.  If DO_ADD is false, only the
-   required memory is allocated.  Must be called with GL
-   (dl_load_tls_lock) acquired.  If the function has already been called
-   for the link map L with !do_add, then this function will not raise
-   an exception, otherwise it is possible that it encounters a memory
-   allocation failure.  */
-extern void _dl_add_to_slotinfo (struct link_map *l, bool do_add)
+   required memory is allocated.  Must be called with
+   GL (dl_load_tls_lock) acquired.  If the function has already been
+   called for the link map L with !DO_ADD, then this function will not
+   raise an exception, otherwise it is possible that it encounters a
+   memory allocation failure.
+
+   Return false if L has already been added to the slotinfo data, or
+   if L has no TLS data.  If the returned value is true, L has been
+   added with this call (DO_ADD), or has been added in a previous call
+   (!DO_ADD).
+
+   The expected usage is as follows: Call _dl_add_to_slotinfo for
+   several link maps with DO_ADD set to false, and record if any calls
+   result in a true result.  If there was a true result, call
+   _dl_add_to_slotinfo again, this time with DO_ADD set to true.  (For
+   simplicity, it's possible to call the function for link maps where
+   the previous result was false.)  The return value from the second
+   round of calls can be ignored.  If there was true result initially,
+   call _dl_update_slotinfo to update the TLS generation counter.  */
+extern bool _dl_add_to_slotinfo (struct link_map *l, bool do_add)
   attribute_hidden;
 
 /* Update slot information data for at least the generation of the