[PATCH] elf: Release dl_load_lock before running dlopen constructors (BZ 15686)

Andreas K. Huettel dilfridge@gentoo.org
Sun Jul 12 02:11:32 GMT 2026


Am Sonntag, 12. Juli 2026, 05:25:32 Japanische Normalzeit schrieb temap@mail.ru:
> From: Artem Proskurnev <temap@mail.ru>
> 
> This addresses one instance of the long-standing class of deadlocks
> described in BZ #15686: ELF constructors and destructors invoked by
> the dynamic loader run with dl_load_lock held, so any code path in
> those constructors that itself needs dl_load_lock deadlocks.

In any case this is post release stuff.

> 
> dl_open_worker holds dl_load_lock across the entire _dl_open call,
> including the call to call_dl_init that runs the new objects'
> constructors.  If one of those constructors spawns a thread whose
> first access to a thread_local object triggers
> __cxa_thread_atexit_impl, the new thread blocks trying to acquire
> dl_load_lock -- which is held by the dlopen thread -- deadlocking
> the process.  The same deadlock arises when the spawned thread calls
> a function that triggers NSS module loading through _dl_open, or any
> other code path that needs dl_load_lock.
> 
> The blocking site is __cxa_thread_atexit_impl at
> stdlib/cxa_thread_atexit_impl.c.  BZ #28357 was a partial fix for
> the wider BZ #15686 problem: it moved dl_open_worker_begin and
> _dl_close_worker to the finer-grained dl_load_tls_lock (commit
> 024a7640ab) and used that new lock in pthread_create and
> __tls_get_addr.  __cxa_thread_atexit_impl, however, still takes
> dl_load_lock to protect its DSO lookup (_dl_find_dso_for_object)
> against a racing dlclose, and that path is not covered by the
> BZ #28357 fix.  Moving it to dl_load_tls_lock is not straightforward
> because _dl_find_dso_for_object walks _ns_loaded, which is protected
> by dl_load_lock rather than dl_load_tls_lock.
> 
> This patch takes the alternative approach of releasing dl_load_lock
> during constructor execution.  At the point where call_dl_init runs,
> the following invariants hold:
> 
>   * All link_map structures for the newly loaded DSO and its
>     dependencies are fully initialized and immutable.
>   * The DSO has l_direct_opencount == 1 (incremented in
>     dl_open_worker_begin), so a concurrent dlclose cannot unload it:
>     _dl_close_worker short-circuits when the count is non-zero.
>   * Implicit dependencies are protected by the l_map_used marking in
>     _dl_close_worker, which transitively marks the l_initfini chain
>     of any map with non-zero l_direct_opencount.
>   * dl_iterate_phdr uses dl_load_write_lock rather than dl_load_lock
>     and is unaffected by the unlock.  Other threads calling
>     dl_iterate_phdr during the constructor may observe the DSO before
>     its constructor has run; this is consistent with POSIX, which
>     does not guarantee atomic appearance of dlopen'd objects, and is
>     equivalent to dlsym from inside a constructor observing
>     partially-initialized main-executable symbols.
>   * Recursive dlopen from a constructor re-acquires dl_load_lock
>     normally in _dl_open and proceeds serially.
> 
> The lock is re-acquired immediately after constructors complete,
> before add_to_global_update and the lock/unlock pairing expected by
> _dl_open.
> 
> Exception safety: call_dl_init is invoked via
> _dl_catch_exception (NULL, ...) so that lazy binding failures are
> fatal (the process exits through _dl_fatal_printf); therefore the
> re-lock is not required on the error path.  C++ exceptions thrown
> from constructors are a separate, pre-existing concern: dl exception
> handling uses setjmp/longjmp rather than C++ unwinding, so a thrown
> exception may leave locks in any state regardless of this patch.
> Releasing the lock is strictly safer than holding it in that case.
> 
> Minimal reproducer: a DSO whose constructor calls
> gdk_pixbuf_new_from_file on a system where the glycin image loader
> is wired in via gdk-pixbuf reaches a sandboxed loader process spawn,
> which in turn calls std::thread::spawn; the spawned thread's first
> thread_local access (__cxa_thread_atexit_impl) blocks on dl_load_lock
> held by the dlopen caller.  The same hang reproduces with any
> constructor that spawns a thread touching thread_local state or
> triggering NSS module loads.
> 
> A regression test is added in sysdeps/pthread/tst-create2.c with its
> DSO in tst-create2mod.c.  The DSO constructor spawns a worker thread
> that calls __cxa_thread_atexit_impl and then joins it; under the
> pre-fix locking model the join deadlocks and the test framework
> times out.  The test follows the layout of tst-create1 (BZ #28357),
> which covers the pthread_create leg of the same bug class.
> 
> Tested on x86_64-linux-gnu. Verified both directions of the regression
> test: sysdeps/pthread/tst-create2 deadlocks (times out after 10 s)
> when run against the unpatched tree, and passes (exit 0) with this
> patch applied. No regressions in the full glibc test suite (make check).
> 
> Co-authored-by: Alexander Pevzner <pzz@apevzner.com>
> Signed-off-by: Artem Proskurnev <temap@mail.ru>
> Signed-off-by: Alexander Pevzner <pzz@apevzner.com>
> ---
>  elf/dl-open.c                    | 26 ++++++++++++++
>  sysdeps/pthread/Makefile         |  7 ++++
>  sysdeps/pthread/tst-create2.c    | 48 +++++++++++++++++++++++++
>  sysdeps/pthread/tst-create2mod.c | 62 ++++++++++++++++++++++++++++++++
>  4 files changed, 143 insertions(+)
>  create mode 100644 sysdeps/pthread/tst-create2.c
>  create mode 100644 sysdeps/pthread/tst-create2mod.c
> 
> diff --git a/elf/dl-open.c b/elf/dl-open.c
> index cf4749694f..2e824d86bc 100644
> --- a/elf/dl-open.c
> +++ b/elf/dl-open.c
> @@ -792,11 +792,37 @@ dl_open_worker (void *a)
>    int mode = args->mode;
>    struct link_map *new = args->map;
>  
> +  /* The link_map structures are fully initialized at this point.  We
> +     can release dl_load_lock so that constructors can safely spawn
> +     threads without deadlocking on dl_load_lock (e.g. if the new thread
> +     accesses a thread_local variable and calls __cxa_thread_atexit_impl,
> +     or performs operations that trigger NSS module loading).
> +
> +     The DSO has l_direct_opencount == 1 at this point, so it cannot be
> +     unloaded by a concurrent dlclose -- _dl_close_worker skips objects
> +     with nonzero opencount, and l_map_used marking transitively protects
> +     implicit dependencies.  Concurrent dlopen and dlclose in other
> +     threads are not blocked while the lock is released, but they are
> +     safe: our DSO's link_map structures are fully initialized and
> +     immutable at this point.
> +
> +     Notably, dl_iterate_phdr (which uses dl_load_write_lock, not
> +     dl_load_lock) can observe the DSO before its constructor has run.
> +     This is consistent with POSIX, which does not guarantee that a
> +     dlopen'd DSO appears atomically -- and is no different from dlsym
> +     inside a constructor observing partially-initialized symbols from
> +     the main executable.  */
> +  __rtld_lock_unlock_recursive (GL(dl_load_lock));
> +
>    /* Run the initializer functions of new objects.  Temporarily
>       disable the exception handler, so that lazy binding failures are
>       fatal.  */
>    _dl_catch_exception (NULL, call_dl_init, args);
>  
> +  /* Re-acquire dl_load_lock for the final global scope update and for
> +     the lock/unlock pairing expected by _dl_open.  */
> +  __rtld_lock_lock_recursive (GL(dl_load_lock));
> +
>    /* Now we can make the new map available in the global scope.  */
>    if (mode & RTLD_GLOBAL)
>      add_to_global_update (new);
> diff --git a/sysdeps/pthread/Makefile b/sysdeps/pthread/Makefile
> index d0f3cd59ac..9451a5c25c 100644
> --- a/sysdeps/pthread/Makefile
> +++ b/sysdeps/pthread/Makefile
> @@ -349,6 +349,7 @@ tests += \
>    tst-atfork3 \
>    tst-atfork4 \
>    tst-create1 \
> +  tst-create2 \
>    tst-fini1 \
>    tst-pt-tls4 \
>    # tests
> @@ -365,6 +366,7 @@ modules-names += \
>    tst-atfork3mod \
>    tst-atfork4mod \
>    tst-create1mod \
> +  tst-create2mod \
>    tst-fini1mod \
>    tst-stack2-mod \
>    tst-tls4moda \
> @@ -377,6 +379,7 @@ tst-atfork2mod.so-no-z-defs = yes
>  tst-atfork3mod.so-no-z-defs = yes
>  tst-atfork4mod.so-no-z-defs = yes
>  tst-create1mod.so-no-z-defs = yes
> +tst-create2mod.so-no-z-defs = yes
>  
>  ifeq ($(build-shared),yes)
>  # Build all the modules even when not actually running test programs.
> @@ -540,6 +543,10 @@ LDFLAGS-tst-create1 = -Wl,-export-dynamic
>  $(objpfx)tst-create1: $(shared-thread-library)
>  $(objpfx)tst-create1.out: $(objpfx)tst-create1mod.so
>  
> +LDFLAGS-tst-create2 = -Wl,-export-dynamic
> +$(objpfx)tst-create2: $(shared-thread-library)
> +$(objpfx)tst-create2.out: $(objpfx)tst-create2mod.so
> +
>  $(objpfx)tst-stack2.out: $(objpfx)tst-stack2-mod.so
>  $(objpfx)tst-stack2-mod.so: $(shared-thread-library)
>  LDFLAGS-tst-stack2-mod.so = -Wl,-z,execstack
> diff --git a/sysdeps/pthread/tst-create2.c b/sysdeps/pthread/tst-create2.c
> new file mode 100644
> index 0000000000..d7df8aafc5
> --- /dev/null
> +++ b/sysdeps/pthread/tst-create2.c
> @@ -0,0 +1,48 @@
> +/* Verify that a thread spawned by a dlopen constructor can register a
> +   TLS destructor via __cxa_thread_atexit_impl without deadlocking on
> +   dl_load_lock held by the dlopen caller (BZ 15686).
> +   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/>.  */
> +
> +/* Reproducer for one instance of the deadlock class described in
> +   BZ 15686.
> +
> +   thread 1: dlopen -> ctor -> pthread_create(worker) -> pthread_join(worker)
> +   thread 2 (worker): __cxa_thread_atexit_impl -> tries to lock dl_load_lock
> +
> +   Before the fix in elf/dl-open.c, dl_load_lock is held across
> +   call_dl_init, so thread 2 blocks on a lock that thread 1 will only
> +   release after pthread_join returns -- a deadlock that the
> +   test-driver timeout surfaces as a failure.  After the fix,
> +   dl_load_lock is released before constructors run and reacquired
> +   afterwards, so thread 2 makes progress and the dlopen call returns.  */
> +
> +#include <stdio.h>
> +#include <support/xdlfcn.h>
> +
> +static int
> +do_test (void)
> +{
> +  dprintf (1, "main: dlopen tst-create2mod.so\n");
> +  void *h = xdlopen ("tst-create2mod.so", RTLD_NOW);
> +  dprintf (1, "main: dlopen done\n");
> +  xdlclose (h);
> +  dprintf (1, "main: dlclose done\n");
> +  return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/sysdeps/pthread/tst-create2mod.c b/sysdeps/pthread/tst-create2mod.c
> new file mode 100644
> index 0000000000..d8254f729f
> --- /dev/null
> +++ b/sysdeps/pthread/tst-create2mod.c
> @@ -0,0 +1,62 @@
> +/* Verify that a thread spawned by a dlopen constructor can register a
> +   TLS destructor via __cxa_thread_atexit_impl without deadlocking on
> +   dl_load_lock held by the dlopen caller (BZ 15686).
> +   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 <pthread.h>
> +#include <stdlib.h>
> +#include <dso_handle.h>
> +
> +typedef struct { void *val; } A;
> +
> +static void
> +A_dtor (void *obj)
> +{
> +  ((A *) obj)->val = obj;
> +}
> +
> +/* Acquire dl_load_lock via __cxa_thread_atexit_impl.  Called from the
> +   worker thread spawned by the constructor below; if the constructor
> +   runs with dl_load_lock held, this blocks and pthread_join in the
> +   constructor never returns.  */
> +static void
> +reg_dtor (void)
> +{
> +  static __thread A b;
> +  __cxa_thread_atexit_impl (A_dtor, &b, __dso_handle);
> +}
> +
> +static void *
> +worker (void *arg)
> +{
> +  reg_dtor ();
> +  return NULL;
> +}
> +
> +static void __attribute__ ((constructor))
> +do_init (void)
> +{
> +  pthread_t t;
> +  if (pthread_create (&t, NULL, worker, NULL) != 0)
> +    abort ();
> +  /* Blocks until worker has completed its __cxa_thread_atexit_impl
> +     call; under the pre-fix locking model that call deadlocks on
> +     dl_load_lock held by the dlopen caller running this ctor.  */
> +  if (pthread_join (t, NULL) != 0)
> +    abort ();
> +}
> 


-- 
PD Dr. Andreas K. Hüttel
dilfridge@gentoo.org
Gentoo Linux developer 
(council, comrel, toolchain, base-system, perl, libreoffice)
https://wiki.gentoo.org/wiki/User:Dilfridge




More information about the Libc-alpha mailing list