[PATCH v7 2/4] elf: Release dl_load_lock before running dlclose destructors (BZ 15686)
[email protected] Mon, 3 Aug 2026 23:03:24 +0300
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Message-ID | <[email protected]> |
From: Artem Proskurnev <[email protected]> This closes the remaining half of BZ #15686 for the destructor side. _dl_fini (the exit-time finalizer) already releases dl_load_lock before calling _dl_call_fini (dl-fini.c), but _dl_close_worker retains dl_load_lock across the _dl_catch_exception (NULL, _dl_call_fini, imap) call at dl-close.c. The same destructor therefore observes different locking contexts depending on whether it fires from process exit or from an explicit dlclose -- a historical inconsistency rather than a deliberate design choice, since _dl_fini is written more carefully (it pins l_direct_opencount on every entry in its private maps[] array at dl-fini.c before releasing the lock). The practical consequence is that a destructor which acquires a user lock while another thread holds that user lock and calls dlopen deadlocks: thread A: dlclose -> dtor -> acquire user mutex (holds dl_load_lock) thread B: hold user mutex -> dlopen (waits for dl_load_lock) The constructor-side companion patch (1/2) in this series addresses the same deadlock shape for dlopen constructors; this patch addresses the dlclose destructor side, which is the only remaining path that holds dl_load_lock across ELF destructor execution. The fix mirrors what _dl_fini already does: release dl_load_lock around the destructor call, reacquire it before continuing cleanup. The glibc.rtld.strict_init_order tunable (introduced by the 1/2 patch) controls the behaviour: when set to 1, dl_load_lock is held across the destructor as before, for diagnostic use. Two safety points. First, imap->l_removed is moved from after the destructor call to before it. This prevents concurrent code paths from binding to this link_map once the destructor is running under the released lock. _dl_map_object_from_fd skips l_removed maps (dl-load.c), so a concurrent dlopen of the same file gets a fresh link_map instead of binding to the one about to be unmapped; do_lookup_x in dl-lookup.c skips l_removed maps (dl-lookup.c), so concurrent symbol resolution does not bind against the dying DSO. The destructor itself is unaffected because its calls into its own DSO are direct calls resolved at link time, not dynamic lookups. Second, concurrent entry to _dl_close_worker via the static dl_close_state variable (dl-close.c). Two cases are distinct. A recursive dlclose (the destructor calls dlclose itself) sees dl_close_state == pending, sets rerun, and returns; the outer _dl_close_worker runs goto retry at the end (dl-close.c) and picks up the recursively-closed DSO. This is the case dl_close_state was designed for. A cross-thread dlclose of an unrelated DSO while this thread's destructor is running under the released lock also sees dl_close_state == pending and takes the early return. This is a behaviour change relative to the pre-patch model, where the cross-thread dlclose would block on dl_load_lock until the destructor finished and then proceed normally. After this patch, the cross-thread dlclose decrements the opencount of its target and returns success; the actual teardown (destructor and unmap) is performed by this thread's goto retry pass, which rebuilds maps[] from _ns_loaded and picks up any DSO whose opencount has reached zero. POSIX does not require dlclose to complete teardown synchronously, and the DSO is eventually cleaned up, but application code that assumed dlclose (Y) returns only once Y's destructor has run will see a delay when racing with an in-flight dlclose on another thread. The link_map being destructed is not freed until later in _dl_close_worker (long after the lock is reacquired), so the imap pointer stays valid across the unlock window. Other link_maps in the maps[] array are also stable: they all have non-zero reference counts or are themselves on the unload list and stay mapped until the unmap phase later in this function. A regression test is added in sysdeps/pthread/tst-create10.c with its DSO in tst-create10mod.c. The main thread acquires a user mutex, spawns a worker that calls dlclose on the DSO, and waits for the DSO's destructor to signal it is about to block on the mutex. The main thread then calls dlopen; if dl_load_lock is still held by the worker's dlclose, the dlopen blocks forever and the test-driver timeout surfaces the failure. After the fix, dl_load_lock was released for the destructor, so the main thread's dlopen completes, the mutex is released, and the destructor finishes. Tested on x86_64-linux-gnu. With the 1/2 patch applied alone, tst-create10 deadlocks (times out after 10 s); with both 1/2 and this patch applied, it passes. Signed-off-by: Artem Proskurnev <[email protected]> --- elf/dl-close.c | 66 +++++++++++++-- sysdeps/pthread/Makefile | 59 +++++++++++++ sysdeps/pthread/tst-create10.c | 119 ++++++++++++++++++++++++++ sysdeps/pthread/tst-create10mod.c | 59 +++++++++++++ sysdeps/pthread/tst-create7.c | 129 +++++++++++++++++++++++++++++ sysdeps/pthread/tst-create7mod-a.c | 46 ++++++++++ sysdeps/pthread/tst-create7mod-b.c | 26 ++++++ sysdeps/pthread/tst-create8.c | 97 ++++++++++++++++++++++ sysdeps/pthread/tst-create8mod-a.c | 55 ++++++++++++ sysdeps/pthread/tst-create8mod-b.c | 27 ++++++ sysdeps/pthread/tst-create9.c | 113 +++++++++++++++++++++++++ sysdeps/pthread/tst-create9mod.c | 37 +++++++++ 12 files changed, 827 insertions(+), 6 deletions(-) create mode 100644 sysdeps/pthread/tst-create10.c create mode 100644 sysdeps/pthread/tst-create10mod.c create mode 100644 sysdeps/pthread/tst-create7.c create mode 100644 sysdeps/pthread/tst-create7mod-a.c create mode 100644 sysdeps/pthread/tst-create7mod-b.c create mode 100644 sysdeps/pthread/tst-create8.c create mode 100644 sysdeps/pthread/tst-create8mod-a.c create mode 100644 sysdeps/pthread/tst-create8mod-b.c create mode 100644 sysdeps/pthread/tst-create9.c create mode 100644 sysdeps/pthread/tst-create9mod.c diff --git a/elf/dl-close.c b/elf/dl-close.c index 8b6e654791..8e0b504a52 100644 --- a/elf/dl-close.c +++ b/elf/dl-close.c @@ -33,6 +33,7 @@ #include <tls.h> #include <stap-probe.h> #include <dl-find_object.h> +#include <dl-tunables.h> #include <dl-unmap-segments.h> @@ -118,7 +119,15 @@ _dl_close_worker (struct link_map *map, bool force) /* If _dl_close is called recursively (some destructor call dlclose), just record that the parent _dl_close will need to do garbage collection - again and return. */ + again and return. + + Accessed under dl_load_lock. The BZ 15686 destructor fix releases + dl_load_lock around _dl_call_fini below, so a different thread can + enter _dl_close_worker while the original sleeps in the destructor; + that cross-thread entrant takes dl_load_lock on entry to _dl_close + before reading dl_close_state, which pairs with the original + thread's store-via-release-of-dl_load_lock to provide + happens-before. All other accesses below are also under the lock. */ static enum { not_pending, pending, rerun } dl_close_state; if (map->l_direct_opencount > 0 || map->l_type != lt_loaded @@ -262,11 +271,59 @@ _dl_close_worker (struct link_map *map, bool force) { assert (imap->l_type == lt_loaded && !imap->l_nodelete_active); + /* Mark this object as removed *before* running its destructor. + Once dl_load_lock is released for the destructor (see below), + a concurrent dlopen of the same file must not reuse this + link_map - which is about to be unmapped - and must get a + fresh instance instead. Both _dl_map_object_from_fd and + _dl_lookup_map skip objects with l_removed set. */ + imap->l_removed = 1; + /* Call its termination function. Do not do it for half-cooked objects. Temporarily disable exception - handling, so that errors are fatal. */ + handling, so that errors are fatal. + + BZ 15686: Release dl_load_lock while running the destructor + so that it can safely call dlopen, dlsym, or any other dl* + function without deadlocking. This mirrors what _dl_fini + already does for exit-time destructors: it builds a local + array of maps, releases dl_load_lock, and then calls + _dl_call_fini. Previously dlclose was the only path that + held the lock across destructor execution, creating an + asymmetry that could deadlock when a destructor acquired + a user lock held by a thread that was itself waiting for + dl_load_lock (e.g. in dlopen). + + Safety: + - l_removed (set above) prevents concurrent dlopen from + binding to this link_map. + - Concurrent _dl_close_worker is serialized by the static + dl_close_state variable (checked under dl_load_lock at + the top of this function); a recursive dlclose from the + destructor or from another thread sets dl_close_state = + rerun and defers. + - The link_map is not freed until later in this function + (long after the lock is reacquired), so imap stays valid + across the unlock window. + + Setting glibc.rtld.strict_init_order=1 disables the unlock + and reverts to the pre-BZ-15686 model where dl_load_lock + is held across destructor execution. */ if (imap->l_init_called) - _dl_catch_exception (NULL, _dl_call_fini, imap); + { + bool release_lock_for_fini + = (TUNABLE_GET (glibc, rtld, strict_init_order, + int32_t, NULL) + == 0); + + if (release_lock_for_fini) + __rtld_lock_unlock_recursive (GL (dl_load_lock)); + + _dl_catch_exception (NULL, _dl_call_fini, imap); + + if (release_lock_for_fini) + __rtld_lock_lock_recursive (GL (dl_load_lock)); + } #ifdef SHARED /* Auditing checkpoint: we will start deleting objects. @@ -279,9 +336,6 @@ _dl_close_worker (struct link_map *map, bool force) _dl_audit_objclose (imap); #endif - /* This object must not be used anymore. */ - imap->l_removed = 1; - /* We indeed have an object to remove. */ unload_any = true; diff --git a/sysdeps/pthread/Makefile b/sysdeps/pthread/Makefile index 09b46e6d1c..a616092810 100644 --- a/sysdeps/pthread/Makefile +++ b/sysdeps/pthread/Makefile @@ -354,6 +354,10 @@ tests += \ tst-create4 \ tst-create5 \ tst-create6 \ + tst-create7 \ + tst-create8 \ + tst-create9 \ + tst-create10 \ tst-fini1 \ tst-pt-tls4 \ # tests @@ -369,12 +373,18 @@ modules-names += \ tst-atfork2mod \ tst-atfork3mod \ tst-atfork4mod \ + tst-create10mod \ tst-create1mod \ tst-create2mod \ tst-create3mod \ tst-create4mod-a \ tst-create4mod-b \ tst-create6mod \ + tst-create7mod-a \ + tst-create7mod-b \ + tst-create8mod-a \ + tst-create8mod-b \ + tst-create9mod \ tst-fini1mod \ tst-stack2-mod \ tst-tls4moda \ @@ -390,6 +400,12 @@ tst-create1mod.so-no-z-defs = yes tst-create2mod.so-no-z-defs = yes tst-create4mod-a.so-no-z-defs = yes tst-create4mod-b.so-no-z-defs = yes +tst-create7mod-a.so-no-z-defs = yes +tst-create7mod-b.so-no-z-defs = yes +tst-create8mod-a.so-no-z-defs = yes +tst-create8mod-b.so-no-z-defs = yes +tst-create9mod.so-no-z-defs = yes +tst-create10mod.so-no-z-defs = yes ifeq ($(build-shared),yes) # Build all the modules even when not actually running test programs. @@ -594,3 +610,46 @@ $(objpfx)tst-create5: $(shared-thread-library) $(objpfx)tst-create6: $(shared-thread-library) $(objpfx)tst-create6mod.so: $(shared-thread-library) $(objpfx)tst-create6.out: $(objpfx)tst-create6mod.so + +# tst-create7 verifies that dlclose of an unrelated DSO concurrent +# with an in-flight dlclose destructor (BZ 15686) is eventually +# completed via the goto retry pass of _dl_close_worker. Worker A +# dlclose's mod-a, whose destructor sleeps to keep dl_load_lock +# released; the main thread then dlclose's the unrelated mod-b and +# polls RTLD_NOLOAD until mod-b is observed unloaded. Without the +# retry pass the poll would never see NULL and the test would time +# out. +LDFLAGS-tst-create7 = -Wl,-export-dynamic +$(objpfx)tst-create7: $(shared-thread-library) +$(objpfx)tst-create7.out: $(objpfx)tst-create7mod-a.so $(objpfx)tst-create7mod-b.so + +# tst-create8 verifies the original dl_close_state = rerun case: +# a destructor that recursively calls dlclose on a different DSO. +# mod-a's constructor dlopens mod-b; mod-a's destructor dlcloses +# mod-b. The recursive _dl_close_worker(mod-b) takes the +# early-return with dl_close_state = rerun, and the outer worker's +# goto retry pass picks up mod-b for teardown. +LDFLAGS-tst-create8 = -Wl,-export-dynamic +$(objpfx)tst-create8: $(shared-thread-library) +$(objpfx)tst-create8mod-a.so: $(shared-thread-library) +$(objpfx)tst-create8.out: $(objpfx)tst-create8mod-a.so $(objpfx)tst-create8mod-b.so + +# tst-create9 covers the refcounted-plugin pattern: two threads +# concurrently dlclose the same handle (opencount = 2). Exactly +# one decrement reaches 0 and runs the destructor; the other +# returns success having decremented 2 -> 1. The test asserts +# the destructor ran exactly once and the DSO is fully unloaded. +LDFLAGS-tst-create9 = -Wl,-export-dynamic +$(objpfx)tst-create9: $(shared-thread-library) +$(objpfx)tst-create9mod.so: $(shared-thread-library) +$(objpfx)tst-create9.out: $(objpfx)tst-create9mod.so + +# tst-create10 verifies the BZ 15686 destructor fix: dlclose must +# release dl_load_lock while running DT_FINI destructors, mirroring +# what _dl_fini already does at process exit. The module's destructor +# blocks on a user mutex; the main thread holds that mutex and calls +# dlopen (which needs dl_load_lock). Without the fix, this deadlocks. +LDFLAGS-tst-create10 = -Wl,-export-dynamic +$(objpfx)tst-create10: $(shared-thread-library) +$(objpfx)tst-create10mod.so: $(shared-thread-library) +$(objpfx)tst-create10.out: $(objpfx)tst-create10mod.so diff --git a/sysdeps/pthread/tst-create10.c b/sysdeps/pthread/tst-create10.c new file mode 100644 index 0000000000..cfa821abd3 --- /dev/null +++ b/sysdeps/pthread/tst-create10.c @@ -0,0 +1,119 @@ +/* Verify that a dlclose destructor does not hold dl_load_lock (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/>. */ + +/* WHAT THIS TEST CHECKS + + A worker thread calls dlclose on a DSO whose destructor tries to + acquire a user mutex (tst_create10_mutex). The main thread holds + that mutex and then calls dlopen, which needs dl_load_lock. + + If dlclose holds dl_load_lock across the destructor (the + pre-BZ-15686 dlclose behaviour), we get a classic lock-ordering + deadlock: + + worker: holds dl_load_lock, waits for mutex + main: holds mutex, waits for dl_load_lock + + The BZ 15686 fix releases dl_load_lock for the duration of the + destructor in _dl_close_worker, mirroring what _dl_fini already + does for exit-time destructors. After the fix, the main thread's + dlopen acquires dl_load_lock (now free), completes, and then + releases the mutex so the destructor can finish. + + This test specifically exercises the DESTRUCTOR path in dlclose. + It complements tst-create2 (which tests the CONSTRUCTOR path) + and would hang on a glibc that has only the constructor half of + the fix. */ + +#include <pthread.h> +#include <stdatomic.h> +#include <sched.h> +#include <stdio.h> +#include <support/check.h> +#include <support/xdlfcn.h> +#include <support/xthread.h> + +/* Exported via -rdynamic so the module can find them. */ +pthread_mutex_t tst_create10_mutex = PTHREAD_MUTEX_INITIALIZER; +atomic_int tst_create10_dtor_running = 0; +atomic_int tst_create10_dtor_done = 0; + +static void * +worker (void *arg) +{ + void *h = arg; + + /* dlclose runs the module's DT_FINI_ARRAY destructor. Under the + old dlclose locking model, dl_load_lock is held here for the + entire destructor. The destructor blocks on tst_create10_mutex + (held by main), and if main's dlopen waits for dl_load_lock, + the two threads deadlock. */ + xdlclose (h); + return NULL; +} + +static int +do_test (void) +{ + /* Load the module so we have a handle to dlclose. */ + void *h = xdlopen ("tst-create10mod.so", RTLD_NOW); + TEST_VERIFY_EXIT (h != NULL); + + /* Lock the mutex before spawning the worker. The module's + destructor will try to acquire it and block. */ + TEST_COMPARE (pthread_mutex_lock (&tst_create10_mutex), 0); + + pthread_t t = xpthread_create (0, worker, h); + + /* Wait until the destructor signals it is about to block on the + mutex. This ensures the destructor is actually running inside + the dlclose, not still in the earlier _dl_close_worker + bookkeeping. */ + while (!atomic_load_explicit (&tst_create10_dtor_running, + memory_order_acquire)) + sched_yield (); + + /* The destructor is now blocked on tst_create10_mutex. Call + dlopen - if dl_load_lock is still held by the worker's + dlclose, this call blocks forever (deadlock). With the + BZ 15686 fix, dl_load_lock was released for the destructor, + so this succeeds. */ + printf ("main: calling dlopen while destructor is blocked\n"); + void *h2 = xdlopen ("tst-create10mod.so", RTLD_NOW); + TEST_VERIFY_EXIT (h2 != NULL); + printf ("main: dlopen succeeded - dl_load_lock was released\n"); + + /* Release the new handle. This will defer actual unloading + (dl_close_state == pending) but that is fine; the rerun + mechanism in _dl_close_worker will clean it up. */ + xdlclose (h2); + + /* Release the mutex so the worker's destructor can proceed. */ + TEST_COMPARE (pthread_mutex_unlock (&tst_create10_mutex), 0); + + xpthread_join (t); + + /* Verify the destructor ran to completion. */ + TEST_COMPARE (atomic_load_explicit (&tst_create10_dtor_done, + memory_order_acquire), 1); + + printf ("PASS: destructor completed without deadlock\n"); + return 0; +} + +#include <support/test-driver.c> diff --git a/sysdeps/pthread/tst-create10mod.c b/sysdeps/pthread/tst-create10mod.c new file mode 100644 index 0000000000..e162b4e85e --- /dev/null +++ b/sysdeps/pthread/tst-create10mod.c @@ -0,0 +1,59 @@ +/* DSO for tst-create10: destructor acquires a user mutex that the + main thread holds while calling dlopen. If dlclose still holds + dl_load_lock during the destructor (the pre-BZ-15686 dlclose + behaviour), the main thread's dlopen deadlocks waiting for that + lock. + 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 <stdatomic.h> + +/* Defined in the main executable, exported via -rdynamic. */ + +/* Mutex held by the main thread while the destructor runs. The + destructor blocks here, simulating a real-world plugin cleanup + that removes entries from a registry protected by a mutex. */ +extern pthread_mutex_t tst_create10_mutex; + +/* Set to 1 just before the destructor blocks on the mutex, so the + main thread knows the destructor has started. */ +extern atomic_int tst_create10_dtor_running; + +/* Set to 1 after the destructor has acquired and released the + mutex, confirming it ran to completion. */ +extern atomic_int tst_create10_dtor_done; + +static void __attribute__ ((destructor)) +fini (void) +{ + /* Signal that we have entered the destructor. The main thread + polls this before calling dlopen, so it knows the destructor + is about to block on the mutex. */ + atomic_store_explicit (&tst_create10_dtor_running, 1, + memory_order_release); + + /* Block on the mutex held by the main thread. If dl_load_lock + is still held by our dlclose caller, and the main thread's + dlopen is waiting for dl_load_lock, we have a classic + lock-ordering deadlock. */ + pthread_mutex_lock (&tst_create10_mutex); + pthread_mutex_unlock (&tst_create10_mutex); + + atomic_store_explicit (&tst_create10_dtor_done, 1, + memory_order_release); +} diff --git a/sysdeps/pthread/tst-create7.c b/sysdeps/pthread/tst-create7.c new file mode 100644 index 0000000000..059a9e3776 --- /dev/null +++ b/sysdeps/pthread/tst-create7.c @@ -0,0 +1,129 @@ +/* Verify that concurrent dlclose of an unrelated DSO is eventually + completed via the goto retry pass of _dl_close_worker (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/>. */ + +/* WHAT THIS TEST CHECKS + + When worker A is inside a dlclose destructor with dl_load_lock + released (per the BZ 15686 destructor fix), a concurrent dlclose + of an UNRELATED DSO on another thread (here: the main thread) + takes the early return in _dl_close_worker: it sees + dl_close_state == pending, decrements its target's + l_direct_opencount, marks dl_close_state = rerun, and returns + success without running the destructor or unmapping the DSO. + + Worker A's _dl_close_worker eventually reaches the goto retry at + the end of the function, rebuilds maps[] from _ns_loaded, and + picks up the unrelated DSO for teardown. + + This test verifies that the deferred teardown actually completes: + after the main thread's dlclose returns, the unrelated DSO must + be unloaded within a reasonable bound. Without the retry pass + picking it up, the DSO would stay loaded indefinitely and the + RTLD_NOLOAD probe would never return NULL. + + The destructor of tst-create7mod-a sleeps briefly to widen the + window in which the main thread can race into _dl_close_worker + while dl_load_lock is released. */ + +#include <pthread.h> +#include <stdatomic.h> +#include <sched.h> +#include <stdio.h> +#include <time.h> +#include <support/check.h> +#include <support/xdlfcn.h> +#include <support/xthread.h> + +/* Set to 1 by tst-create7mod-a destructor when it has entered. + The main thread polls this to know the race window is open. */ +atomic_int tst_create7mod_a_dtor_running = 0; + +/* Set to 1 by tst-create7mod-a destructor after it has finished + sleeping. The main thread checks this after joining worker A. */ +atomic_int tst_create7mod_a_dtor_done = 0; + +static void * +worker_a (void *arg) +{ + void *h = arg; + /* dlclose runs tst-create7mod-a's DT_FINI_ARRAY destructor, which + sleeps to keep dl_load_lock released long enough for the main + thread to race in. */ + xdlclose (h); + return NULL; +} + +static int +do_test (void) +{ + void *ha = xdlopen ("tst-create7mod-a.so", RTLD_NOW); + TEST_VERIFY_EXIT (ha != NULL); + void *hb = xdlopen ("tst-create7mod-b.so", RTLD_NOW); + TEST_VERIFY_EXIT (hb != NULL); + + /* Spawn worker A first; its destructor will sleep, opening the + race window for the main thread. */ + pthread_t ta = xpthread_create (0, worker_a, ha); + + /* Wait until A's destructor has started. */ + while (!atomic_load_explicit (&tst_create7mod_a_dtor_running, + memory_order_acquire)) + sched_yield (); + + /* Main thread dlclose's an unrelated DSO while A is in its + destructor with dl_load_lock released. With the fix, this + takes the early return in _dl_close_worker, decrements + l_direct_opencount, sets dl_close_state = rerun, and returns + success without running the destructor or unmapping. */ + printf ("main: dlclose unrelated DSO while A is in destructor\n"); + xdlclose (hb); + printf ("main: dlclose returned; teardown deferred\n"); + + /* Poll for unload completion. Worker A's _dl_close_worker will + eventually reach goto retry, rebuild maps[] from _ns_loaded, + and pick up mod-b for teardown. Without that retry pass this + loop would never observe RTLD_NOLOAD returning NULL. + + NB: dlopen() with RTLD_NOLOAD on an already-loaded DSO returns + a handle AND increments l_direct_opencount, so each probe must + be matched by dlclose() to avoid pinning the DSO and defeating + the test. */ + for (int i = 0; i < 50; ++i) + { + void *h = dlopen ("tst-create7mod-b.so", RTLD_NOW | RTLD_NOLOAD); + if (h == NULL) + break; + dlclose (h); + struct timespec ts = { .tv_nsec = 100000000 }; /* 100 ms */ + nanosleep (&ts, NULL); + } + + TEST_VERIFY (dlopen ("tst-create7mod-b.so", RTLD_NOW | RTLD_NOLOAD) + == NULL); + printf ("main: mod-b unloaded via worker A retry pass\n"); + + /* Let worker A finish and verify its destructor ran to completion. */ + xpthread_join (ta); + TEST_COMPARE (atomic_load_explicit (&tst_create7mod_a_dtor_done, + memory_order_acquire), 1); + + return 0; +} + +#include <support/test-driver.c> diff --git a/sysdeps/pthread/tst-create7mod-a.c b/sysdeps/pthread/tst-create7mod-a.c new file mode 100644 index 0000000000..1f63c2a34b --- /dev/null +++ b/sysdeps/pthread/tst-create7mod-a.c @@ -0,0 +1,46 @@ +/* DSO for tst-create7: destructor sleeps to widen the race window + in which the main thread can dlclose an unrelated DSO while + dl_load_lock is released (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 <stdatomic.h> +#include <time.h> + +/* Defined in the main executable, exported via -rdynamic. */ +extern atomic_int tst_create7mod_a_dtor_running; +extern atomic_int tst_create7mod_a_dtor_done; + +/* How long (nanoseconds) the destructor sleeps to keep dl_load_lock + released long enough for the main thread to race in. */ +#define TST_CREATE7_DTOR_SLEEP_NS 200000000 /* 200 ms */ + +static void __attribute__ ((destructor)) +fini_a (void) +{ + /* Signal that we have entered the destructor. The main thread + polls this before dlclose'ing the unrelated DSO. */ + atomic_store_explicit (&tst_create7mod_a_dtor_running, 1, + memory_order_release); + + /* Sleep to widen the race window. */ + struct timespec ts = { .tv_nsec = TST_CREATE7_DTOR_SLEEP_NS }; + nanosleep (&ts, NULL); + + atomic_store_explicit (&tst_create7mod_a_dtor_done, 1, + memory_order_release); +} diff --git a/sysdeps/pthread/tst-create7mod-b.c b/sysdeps/pthread/tst-create7mod-b.c new file mode 100644 index 0000000000..fb3441c85f --- /dev/null +++ b/sysdeps/pthread/tst-create7mod-b.c @@ -0,0 +1,26 @@ +/* Trivial DSO for tst-create7: exists only so the main thread can + dlopen/dlclose it concurrently with worker A's destructor on an + unrelated module. No constructors or destructors; the test + verifies only the deferred teardown of this DSO via worker A's + goto retry pass in _dl_close_worker (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/>. */ + +/* A single exported symbol so the .so is not entirely empty. Never + read by the test; the test only checks load/unload state via + RTLD_NOLOAD. */ +int tst_create7mod_b_marker = 0; diff --git a/sysdeps/pthread/tst-create8.c b/sysdeps/pthread/tst-create8.c new file mode 100644 index 0000000000..56a151d4e6 --- /dev/null +++ b/sysdeps/pthread/tst-create8.c @@ -0,0 +1,97 @@ +/* Verify that a recursive dlclose issued from a destructor is + eventually completed via the goto retry pass of _dl_close_worker + (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/>. */ + +/* WHAT THIS TEST CHECKS + + The static dl_close_state variable in _dl_close_worker was + originally introduced for the case where a destructor calls + dlclose on another DSO. Pre-BZ-15686, dl_load_lock was held + across the destructor, so the recursive dlclose saw + dl_close_state == pending, set dl_close_state = rerun, returned, + and the outer worker's goto retry pass picked up the + recursively-closed DSO. + + The BZ 15686 destructor fix releases dl_load_lock around + _dl_call_fini. The recursive-dlclose-from-destructor pattern + must still work: the recursive call still sees dl_close_state + == pending (the outer worker set it at the retry: label before + entering the unload loop), still takes the early-return, and + the outer worker still does goto retry. This test exercises + that path. + + Setup: tst-create8mod-a's constructor dlopens tst-create8mod-b + and stashes the handle; tst-create8mod-a's destructor dlclose's + that handle. The main test dlclose's mod-a, which triggers the + recursive dlclose, then polls RTLD_NOLOAD until both DSOs are + gone. Without the goto retry pass picking up mod-b, the poll + would never observe NULL. */ + +#include <stdio.h> +#include <time.h> +#include <support/check.h> +#include <support/xdlfcn.h> + +static int +do_test (void) +{ + void *ha = xdlopen ("tst-create8mod-a.so", RTLD_NOW); + TEST_VERIFY_EXIT (ha != NULL); + + /* The constructor of mod-a dlopen'd mod-b. Sanity-check that + mod-b is resident before we tear anything down. */ + void *hb_probe = dlopen ("tst-create8mod-b.so", RTLD_NOW | RTLD_NOLOAD); + TEST_VERIFY_EXIT (hb_probe != NULL); + dlclose (hb_probe); + + /* dlclose(mod-a) triggers mod-a's destructor, which dlclose's + mod-b recursively. With the BZ 15686 destructor fix, the + outer _dl_close_worker has released dl_load_lock for the + destructor; the recursive _dl_close_worker(mod-b) takes the + early-return and sets dl_close_state = rerun; the outer + worker's goto retry then picks up mod-b. */ + printf ("main: dlclose(mod-a), destructor will recursively dlclose(mod-b)\n"); + xdlclose (ha); + printf ("main: dlclose returned; mod-b teardown is deferred to retry\n"); + + /* Poll for mod-b unload completion. Each RTLD_NOLOAD probe must + be matched by dlclose to avoid pinning the DSO (see the + NB comment in tst-create7.c). */ + for (int i = 0; i < 50; ++i) + { + void *h = dlopen ("tst-create8mod-b.so", RTLD_NOW | RTLD_NOLOAD); + if (h == NULL) + break; + dlclose (h); + struct timespec ts = { .tv_nsec = 100000000 }; /* 100 ms */ + nanosleep (&ts, NULL); + } + + TEST_VERIFY (dlopen ("tst-create8mod-b.so", RTLD_NOW | RTLD_NOLOAD) == NULL); + printf ("main: mod-b unloaded via outer worker's goto retry pass\n"); + + /* mod-a was the original target of dlclose, so it must already + be gone. */ + TEST_VERIFY (dlopen ("tst-create8mod-a.so", RTLD_NOW | RTLD_NOLOAD) == NULL); + printf ("main: mod-a unloaded\n"); + + return 0; +} + +#include <support/test-driver.c> diff --git a/sysdeps/pthread/tst-create8mod-a.c b/sysdeps/pthread/tst-create8mod-a.c new file mode 100644 index 0000000000..aa813e9980 --- /dev/null +++ b/sysdeps/pthread/tst-create8mod-a.c @@ -0,0 +1,55 @@ +/* DSO for tst-create8: constructor dlopens mod-b and stores the + handle; destructor dlclose's that handle, recursing into + _dl_close_worker while mod-a's own _dl_close_worker is still in + flight with dl_load_lock released (BZ 15686 destructor fix). + + The recursive dlclose(mod-b) sees dl_close_state == pending, takes + the early-return, sets dl_close_state = rerun, and returns. The + outer _dl_close_worker for mod-a eventually reaches the goto retry + pass, rebuilds maps[] from _ns_loaded, and unloads mod-b. + + 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 <dlfcn.h> + +/* Held across the constructor / destructor lifetime so the static + handle below is stable. */ +static void *mod_b_handle; + +static void __attribute__ ((constructor)) +init_a (void) +{ + /* RTLD_NOW so relocations for mod-b complete before anyone uses + it. Keep the handle for the destructor to close. */ + mod_b_handle = dlopen ("tst-create8mod-b.so", RTLD_NOW); +} + +static void __attribute__ ((destructor)) +fini_a (void) +{ + /* Recursive dlclose from a destructor: the outer _dl_close_worker + for mod-a is currently inside its goto retry / unload phase with + dl_load_lock released (per the BZ 15686 destructor fix). This + call enters _dl_close_worker for mod-b, which sees + dl_close_state == pending, decrements mod-b's opencount, + records dl_close_state = rerun, and returns without running + mod-b's destructor or unmapping it. The outer worker's goto + retry pass picks up mod-b for teardown. */ + if (mod_b_handle != NULL) + dlclose (mod_b_handle); +} diff --git a/sysdeps/pthread/tst-create8mod-b.c b/sysdeps/pthread/tst-create8mod-b.c new file mode 100644 index 0000000000..434ed53d00 --- /dev/null +++ b/sysdeps/pthread/tst-create8mod-b.c @@ -0,0 +1,27 @@ +/* DSO for tst-create8: loaded by tst-create8mod-a's constructor and + recursively dlclose'd by tst-create8mod-a's destructor. Exists to + exercise the dl_close_state = rerun path in _dl_close_worker + (BZ 15686): the destructor of mod-a runs while mod-a's + _dl_close_worker has dl_close_state == pending, so the recursive + dlclose(mod-b) takes the early-return, sets dl_close_state = rerun, + and the outer worker picks up mod-b via goto retry. + + 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/>. */ + +/* A single exported symbol so the .so is not entirely empty. */ +int tst_create8mod_b_marker = 0; diff --git a/sysdeps/pthread/tst-create9.c b/sysdeps/pthread/tst-create9.c new file mode 100644 index 0000000000..6f7921bc96 --- /dev/null +++ b/sysdeps/pthread/tst-create9.c @@ -0,0 +1,113 @@ +/* Verify that two threads concurrently calling dlclose on the same + handle (with opencount = 2) race safely: one thread decrements to + 1 and returns, the other decrements to 0 and runs the destructor + (BZ 15686 regression coverage for refcounted-plugin pattern). + 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/>. */ + +/* WHAT THIS TEST CHECKS + + This is a regression test for the BZ 15686 destructor fix under + the common refcounted-plugin pattern: a plugin is referenced by + N users, each user calls dlclose when done, and the last dlclose + triggers the actual destructor + unmap. The test dlopens the + module twice (opencount = 2, same handle returned) and then has + two threads call dlclose on that handle concurrently. + + Expected behaviour: + - Exactly one thread runs the destructor (it observed + opencount 1 -> 0 inside _dl_close_worker). + - The other thread observed opencount 2 -> 1 and returned + without running the destructor. + - The destructor runs exactly once (tst_create9mod_dtor_done + ends up == 1, not 2). + - After both threads return, the DSO is fully unloaded + (RTLD_NOLOAD returns NULL). + - No use-after-free, no double-unmap, no deadlock. + + Under the BZ 15686 fix, the thread that runs the destructor + releases dl_load_lock around _dl_call_fini. The other thread + may have already returned by then (it took the early-return at + the top of _dl_close_worker before opencount reached 0), or it + may be waiting on dl_load_lock in _dl_close - both paths must + converge to the same safe end state. + + This test does NOT try to exercise the buggy "two dlclose calls + on a single-reference handle" pattern (which would error out + with "shared object not open"); that is a documented + application bug, not a glibc regression vector. */ + +#include <stdatomic.h> +#include <pthread.h> +#include <stdio.h> +#include <time.h> +#include <support/check.h> +#include <support/xdlfcn.h> +#include <support/xthread.h> + +/* Set to 1 by tst-create9mod destructor when it has run. Read by + the main thread after both workers have joined. */ +atomic_int tst_create9mod_dtor_done = 0; + +static void * +worker (void *arg) +{ + void *h = arg; + /* dlclose returns 0 on success. Both callers must succeed: one + decrements 2 -> 1, the other 1 -> 0 (and runs the destructor). + xdlclose aborts on failure, so a non-zero return from either + thread would fail the test loudly. */ + xdlclose (h); + return NULL; +} + +static int +do_test (void) +{ + /* Two dlopens of the same name: opencount goes to 2, and dlopen + returns the same handle both times. */ + void *h1 = xdlopen ("tst-create9mod.so", RTLD_NOW); + TEST_VERIFY_EXIT (h1 != NULL); + void *h2 = xdlopen ("tst-create9mod.so", RTLD_NOW); + TEST_VERIFY_EXIT (h2 != NULL); + TEST_VERIFY (h1 == h2); + + printf ("main: spawning two threads that will both dlclose the same handle\n"); + + /* Both threads get the same handle. Whichever wins the + 1 -> 0 race runs the destructor. */ + pthread_t t1 = xpthread_create (0, worker, h1); + pthread_t t2 = xpthread_create (0, worker, h2); + + xpthread_join (t1); + xpthread_join (t2); + + printf ("main: both dlclose calls returned successfully\n"); + + /* Exactly one decrement reached 0, so the destructor ran exactly + once. */ + TEST_COMPARE (atomic_load_explicit (&tst_create9mod_dtor_done, + memory_order_acquire), 1); + + /* The DSO must be fully unloaded after both dlcloses. */ + TEST_VERIFY (dlopen ("tst-create9mod.so", RTLD_NOW | RTLD_NOLOAD) == NULL); + printf ("main: DSO unloaded; destructor ran exactly once\n"); + + return 0; +} + +#include <support/test-driver.c> diff --git a/sysdeps/pthread/tst-create9mod.c b/sysdeps/pthread/tst-create9mod.c new file mode 100644 index 0000000000..00329c9399 --- /dev/null +++ b/sysdeps/pthread/tst-create9mod.c @@ -0,0 +1,37 @@ +/* DSO for tst-create9: exists to be dlopen'd twice (so opencount = 2) + and then dlclose'd concurrently from two threads. The destructor + signals completion so the main thread can verify the unload + actually happened. Exercises the basic refcounting path that + refcounted plugin managers depend on, under the BZ 15686 changed + dl_load_lock release timing. + + 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 <stdatomic.h> + +/* Defined in the main executable, exported via -rdynamic. */ +extern atomic_int tst_create9mod_dtor_done; + +static void __attribute__ ((destructor)) +fini (void) +{ + atomic_store_explicit (&tst_create9mod_dtor_done, 1, memory_order_release); +} + +/* A single exported symbol so the .so is not entirely empty. */ +int tst_create9mod_marker = 0; -- 2.51.0