@@ -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;
@@ -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.
@@ -595,3 +611,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
new file mode 100644
@@ -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>
new file mode 100644
@@ -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);
+}
new file mode 100644
@@ -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>
new file mode 100644
@@ -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);
+}
new file mode 100644
@@ -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;
new file mode 100644
@@ -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>
new file mode 100644
@@ -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);
+}
new file mode 100644
@@ -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;
new file mode 100644
@@ -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>
new file mode 100644
@@ -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;