From patchwork Mon Aug 3 20:03:23 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Artem Proskurnev X-Patchwork-Id: 140541 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 68C2A4BB24D1 for ; Mon, 3 Aug 2026 20:04:17 +0000 (GMT) X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from send278.i.mail.ru (send278.i.mail.ru [95.163.59.117]) by sourceware.org (Postfix) with ESMTPS id BE2614BB1C32 for ; Mon, 3 Aug 2026 20:03:43 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org BE2614BB1C32 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=mail.ru Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=mail.ru ARC-Filter: OpenARC Filter v1.0.0 sourceware.org BE2614BB1C32 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=95.163.59.117 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1785787424; cv=none; b=eFfYJ1QaP1TPigjf9SZyP7+ztHEivqaCP0+BofZ7TyXpLIQOlAL7S4s67pZ7h5y48qD7U2+NF8Q5iC0jDYpSqv5x3JLcHA2KJfq5Xws3v4fr86aSXnDDyhzH47nuIQXLwDyVFGc0aIlviVY7+f1TBubg/0taVuXTp11Qk9QSjrw= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1785787424; c=relaxed/simple; bh=5yJUoDf7moWGW130rjwGSoENP8AxzI+jWKEhmhspETA=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=WhgcHfNqv6FC3NtkQz72oqdQ2KPKcT1RplGyt1xYaN8yC7F5lr/2tpmYkrBAf94b6fm/gMXZxisbhNUjkHjHz7W+sc4nA9nk/PI9/6QIHZSXLYkGJnuHA4RYo7iZYGt8o9xw1BVAB7JEeeQc9BIbPiEjYKQ7n217WYvFG3U2uwo= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=mail.ru header.i=@mail.ru header.a=rsa-sha256 header.s=mail4 header.b=MQxyynCe DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org BE2614BB1C32 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=mail.ru header.i=@mail.ru header.a=rsa-sha256 header.s=mail4 header.b=MQxyynCe DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=mail.ru; s=mail4; h=Content-Transfer-Encoding:MIME-Version:References:In-Reply-To: Message-ID:Date:Subject:Cc:To:From:From:Sender:Reply-To:To:Cc:Content-Type: Content-Transfer-Encoding:Content-ID:Content-Description:Resent-Date: Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Id: List-Help:List-Unsubscribe:List-Subscribe:List-Post:List-Owner:List-Archive: X-Cloud-Ids:Disposition-Notification-To; bh=fYhAkLN8mvlxAFyfFpDRT2gFOGNvTM/61DytEp+4STo=; t=1785787423; x=1785877423; b=MQxyynCesgraavRXx9Whh5yvUU8XTYBr1bJvrdz+reVvHhphNyAhW4yR8Hd4Xg9CRvxNrKkEV3R /0ZA7i03KyeAH6H4CUGxGnEK77B8s94NHQxCuJzSgaYSR4aTJKn11PBVKk2CuikWZd3N72n25apbF wyftO8zVmrM5gI69MeOkat4UDl2rsfASznUV/tLnrLd3iuyMo5NUwmhkzSU4T8zwqLn+9m4XdJcT5 W0EXislQuNEhsmt6ig4j5WYI6WKtgO16NR+nsKJiVtwNRiPUgeIDrDVg7s6F0+Xvfg2v8VjLhyY8x 7Uqi+p5YXO4AbQamkfKeekVtKMKsUdH9HitQ==; Received: by exim-smtp-7f4897b4d8-jnp6d with esmtpa (envelope-from ) id 1wqysu-00000000ODN-43St; Mon, 03 Aug 2026 23:03:41 +0300 From: temap@mail.ru To: libc-alpha@sourceware.org Cc: fweimer@redhat.com, carlos@redhat.com, adhemerval.zanella@linaro.org, pzz@apevzner.com, m.novosyolov@rosa.ru, Artem Proskurnev Subject: [PATCH v7 1/4] elf: Release dl_load_lock before running dlopen constructors (BZ 15686) Date: Mon, 3 Aug 2026 23:03:23 +0300 Message-ID: <20260803200326.477666-2-temap@mail.ru> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260803200326.477666-1-temap@mail.ru> References: <20260801074707.2565716-1-temap@mail.ru> <20260803200326.477666-1-temap@mail.ru> MIME-Version: 1.0 Authentication-Results: exim-smtp-7f4897b4d8-jnp6d; auth=pass smtp.auth=temap@mail.ru smtp.mailfrom=temap@mail.ru X-Mailru-Src: smtp X-7564579A: 646B95376F6C166E X-77F55803: 4F1203BC0FB41BD9339FDC13DC05525FA3E69A597A0C3F9E896BE79D88F1EF4D1867C24CE74E72BB5FE18E9CBFBE7C07244E6DC7A003FE228F5BD48EAA943B3B4A3C5E9655FBDD13C129AD43B0A46749ADAB024B4846C063 X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE7D43AAFBA4462143CEA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F7900637AC83A81C8FD4AD23D82A6BABE6F325AC2E85FA5F3EDFCBAA7353EFBB553375665029C9298A93E94DE44A816D93168476D2DC56DD8BB9F3EB87483647498E9CC08EEF46B7454FC60B9742502CCDD46D0DBBEA9FB9FD75E40EF6B57BC7E64490618DEB871D839B73339E8FC8737B5C2249B737A621A50BC793CC7F00164DA146DAFE8445B8C89999729449624AB7ADAF37F6B57BC7E64490611E7FA7ABCAF51C92176DF2183F8FC7C0DCF4F0DC832992758941B15DA834481F9449624AB7ADAF37BA3038C0950A5D3613377AFFFEAFD269176DF2183F8FC7C0A85FC399AC366E1B7B076A6E789B0E97A8DF7F3B2552694AD5FFEEA1DED7F25D49FD398EE364050F9647ADFADE5905B19F804269016115C9B3661434B16C20ACC84D3B47A649675FE827F84554CEF5019E625A9149C048EE33AC447995A7AD182BEBFE083D3B9BA73A03B725D353964B0B7D0EA88DDEDAC722CA9DD8327EE4930A3850AC1BE2E735C96613F75B7D048DC4224003CC83647689D4C264860C145E X-C1DE0DAB: 0D63561A33F958A53AB2F1C0E4BB28DA5002B1117B3ED69691E7B8BFB01E093F19AC5B239BAD43353610D81D389A125CDE35189EBF2DEA28FEA14CD2CD220BB99C5DF10A05D560A9880EC71AF561E0AAD9143641EC25BB392E9E69EEB1A181A2F36E2E0160E5C55395B8A2A0B6518DF68C46860778A80D54AF47762AB4810619 X-C8649E89: 1C3962B70DF3F0AD73CAD6646DEDE1918E10F71CB4DF9F9677DD89D51EBB774225B6776AC983F447FC0B9F89525902EE6F57B2FD27647F25E66C117BDB76D6591F2F362E90375985FE3CE55F97D41B10D7738E03092E0E35F417ED9ABAB58E5751E1B14BBD47F79DB8341EE9D5BE9A0AAE74FA999B2ABCCB248AF0251BFDC22D88045001AA9763E4DABE3362BFED2FD64C41F94D744909CE8FFD5CA72B28909428BE7793689043A537E69C174A41D00C X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu53w8ahmwBjZKM/YPHZyZHvz5uv+WouB9+ObcCpyrx6l7KImUglyhkEat/+ysWwi0gdhEs0JGjl6ggRWTy1haxBpVdbIX1nthFXMZebaIdHP2ghjoIc/363UZI6Kf1ptIMVYrk7BQKFwEt7pUaW8IEKm+bJ/64Qnb7lw== X-Mailru-Sender: 583F1D7ACE8F49BDC25C0C59A06B2F964B21268047A1E0B4B951B70A5BD4BD8E631BC214E58A262932D3F34D8EB99555981BBF36307557118FCA44E9AC9C8EC2EEE2A91DED5447003DDE9B364B0DF289AE208404248635DF X-Mras: Ok X-Spam-Status: No, score=-9.9 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, FREEMAIL_FROM, GIT_PATCH_0, KAM_SHORT, SPF_HELO_PASS, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org From: Artem Proskurnev 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. 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. Releasing the lock introduces a data race on l_init_called in call_init (elf/dl-init.c): two threads performing concurrent dlopen of the same DSO could both pass the l_init_called check and run the constructor in parallel. This is addressed by adding per-DSO init serialisation using an l_init_once field in struct link_map. call_init uses atomic_compare_and_exchange_bool_acq to claim the right to run the constructor (0 -> 1); the winning thread proceeds, while losing threads block via lll_futex_wait (or __lll_wait on Hurd) until the winner signals completion (1 -> 2) via lll_futex_wake (or __lll_wake). Different DSOs can initialise concurrently because each has its own l_init_once. The CAS / futex_wait / futex_wake dance is only needed when other threads may be waiting, so single-threaded processes skip it via RTLD_SINGLE_THREAD_P - but l_init_once is always set to 2 after the constructor finishes, so a later call_init from another thread (e.g. via a transitive dlopen once the process goes multi-threaded) for a DSO initialised at startup does not wait on a value that was never going to change. Only the futex wake is conditional on RTLD_SINGLE_THREAD_P; without the unconditional store this bug hung intl/tst-gettext4 and tst-gettext5 in the full make check, because ld.so and libc.so initialise single-threaded at startup but later receive transitive call_init from gconv/NSS loads. A second field, l_init_owner, records the kernel TID of the thread that won the l_init_once CAS and is currently running the constructor. It serves two purposes. First, it closes a return-before-completion race: l_init_called is set at the top of call_init, before the constructor runs, so a concurrent caller that arrives while the constructor is still in progress sees l_init_called set and returns immediately -- even though l_init_once is still 1 and the constructor's writes have not yet been published. With l_init_owner, such a caller compares l_init_owner against its own TID; if they differ, it waits on l_init_once via futex until the winner signals completion. Second, l_init_owner lets a recursive call_init that originates from inside the constructor itself (e.g. via a transitive dlopen of a circular dependency) recognise itself and return immediately, instead of waiting for its own constructor and deadlocking. A third field, l_init_pending, closes a narrow race window that l_init_owner alone does not cover. l_init_called is set only inside call_init, which runs after dl_load_lock has been released and after the CAS has been won; a concurrent dlopen caller that takes the already-loaded early-return path (in dl_open_worker_begin when new->l_searchlist.r_list != NULL, or in _dl_open via is_already_fully_open) could observe l_init_called == 0 and l_init_once == 0 and return before the constructor has been scheduled, defeating the wait that l_init_owner was meant to enforce. l_init_pending is set to 1 in dl_open_worker while it still holds dl_load_lock, just before releasing it to run the constructor, and is cleared by call_init once it has won the l_init_once CAS and become the init owner. Both early-return paths then wait on l_init_once whenever (l_init_pending || l_init_called) is set, l_init_once != 2, and l_init_owner differs from the current TID, releasing dl_load_lock around the wait. A second lock must be released around that same wait in dl_open_worker_begin: dl_load_tls_lock. dl_open_worker calls _dl_catch_exception (..., dl_open_worker_begin, ...) while holding dl_load_tls_lock across the worker, so the early-return wait runs with dl_load_tls_lock held. Releasing only dl_load_lock is not enough - the constructor we are waiting for may spawn a thread, and thread creation takes dl_load_tls_lock in allocatestack -> _dl_allocate_tls_init. Holding dl_load_tls_lock across the futex wait re-introduces the very BZ 15686 deadlock the patch fixes for the single-lock case, in the shape the patch was meant to enable: waiter: holds dl_load_tls_lock, waits on l_init_once futex loader: in ctor -> pthread_create -> _dl_allocate_tls_init waits for dl_load_tls_lock The loader's ctor typically joins the spawned thread, so the ctor never completes, l_init_once never reaches 2, and the waiter never wakes. The __tls_get_addr slow path for dynamically loaded TLS also takes dl_load_tls_lock, so a spawned thread's first dynamic TLS access deadlocks the same way. Fix: release dl_load_tls_lock together with dl_load_lock before the futex wait, and re-acquire them afterwards in the canonical nesting order - dl_load_lock first, then dl_load_tls_lock (the same order _dl_open and dl_open_worker take them). The unlock/relock pairs are balanced by recursion count, so dl_open_worker's own dl_load_tls_lock unlock after dl_open_worker_begin returns stays consistent. The wait is safe with both locks released: the link map is pinned by the l_direct_opencount increment at the top of dl_open_worker_begin, and the futex wait touches only l_init_once. Finally, the _dl_open fast path (is_already_fully_open) was tightened to increment l_direct_opencount BEFORE entering the wait loop, matching the existing increment in dl_open_worker_begin. Without the increment first, the loader thread that ran the constructor could complete, dlsym the DSO, dlclose it (driving opencount to 0 and unloading the DSO), and free the link_map while other threads were still blocked in the wait loop - a use-after-free that surfaced in tst-create3 as segfaults in do_lookup_x with a poisoned scope pointer (0x2a2a2a2a2a2a2a2a) on cleanup, after the test had already printed PASS. A second regression test, tst-create3/tst-create3mod, exercises concurrent dlopen of the same DSO with NTHREADS=8 callers and verifies two invariants: 1. The constructor runs EXACTLY ONCE. This catches the case where two threads both think they lost the l_init_once CAS but proceed anyway. 2. No dlopen caller returns before the constructor has finished. The constructor sleeps 200 ms and publishes a "done" magic as its final write; each caller checks the magic immediately after dlopen returns. This catches the l_init_called short-circuit race that l_init_owner closes -- an earlier version of the test that only checked ctor_count == 1 did NOT catch it. A third test, tst-create4/tst-create4mod-a/tst-create4mod-b, verifies that a long-running constructor does not block a concurrent dlopen of an unrelated library. Thread A's constructor blocks on a barrier; once it has started, thread B dlopen's a different library. Before the BZ 15686 fix, thread B would deadlock because dl_load_lock was still held by thread A across the constructor. After the fix, dl_load_lock is released during the constructor, so thread B's dlopen succeeds in parallel. The test is deterministic: it spins until the constructor has confirmed it started, then creates thread B; no probabilistic interleaving is involved. A fourth regression test, tst-create5, checks a subtle regression in the main-executable path of the BZ 15686 fix. The main executable's constructors are run by the startup code rather than by call_init, so an earlier version of this patch left l_init_once at 0 for the executable. A later multi-threaded dlopen(NULL) / __RTLD_OPENEXEC then took the already-loaded early-return path in dl_open_worker_begin, saw l_init_called == 1 but l_init_once != 2, and waited forever for a constructor that would never complete. tst-create5 spawns a worker thread that calls dlopen(NULL, RTLD_NOW); without the fix the call deadlocks and the test-driver times out, while the fix (setting l_init_once = 2 for the executable in call_init, with a futex wake when multi-threaded) makes dlopen(NULL) return immediately. A fifth regression test, tst-create6/tst-create6mod, exercises the dl_load_tls_lock release around the early-return wait described above. It combines the two ingredients that tst-create2 and tst-create3 cover separately: the module constructor spawns and joins a thread (thread creation takes dl_load_tls_lock in _dl_allocate_tls_init), and two threads concurrently dlopen the same DSO. The constructor sleeps 200 ms before pthread_create so the second caller reliably reaches the early-return wait first. Without the dl_load_tls_lock release around the wait, the constructor's pthread_create blocks on it and the test deadlocks; with the fix, both dlopen calls return only after the constructor published its done magic, and the constructor ran exactly once. To aid diagnosis of such applications, a new tunable glibc.rtld.strict_init_order (default 0) reverts to the pre-BZ-15686 locking model: dl_load_lock is held across constructor execution, giving the old strict total order of dlopen calls across threads at the cost of reintroducing the BZ 15686 deadlock. The tunable is intended as a temporary escape hatch, not a long-term solution, and is documented in manual/tunables.texi. It is enabled at run time through GLIBC_TUNABLES, e.g.: GLIBC_TUNABLES=glibc.rtld.strict_init_order=1 ./your-app Because the tunable is evaluated in dl_open_worker on every dlopen, it can be set on a per-process basis without rebuilding, and cleared again once the downstream application has been fixed. Tested on x86_64-linux-gnu. Both directions of the main regression test verified: sysdeps/pthread/tst-create2 deadlocks (times out after 10 s) on the unpatched tree and passes (exit 0) with this patch. tst-create3 covers both the ctor-runs-once invariant and the visibility invariant that l_init_owner + l_init_pending preserve, and ran 15/15 stable once the opencount-before-wait fix was applied to the _dl_open fast path. tst-create4 is the positive counterpart: it demonstrates that the lock release enables concurrent dlopen of independent libraries, with a barrier-based long-running constructor that would deadlock the unpatched tree but passes deterministically after the fix. tst-create5 was checked by building the rest of this patch without the main-executable fix: that intermediate build fails nptl/tst-create5 (timeout), while the full patch passes it. tst-create6 was verified the same way against the dl_load_tls_lock release: an intermediate build with only the dl_load_lock release but without the dl_load_tls_lock release around the early-return wait hangs (test-driver timeout), while the full patch passes it. The real-world trigger was also verified end-to-end with a minimal reproducer: a DSO whose constructor calls gdk_pixbuf_new_from_file on a PNG, reaching the glycin sandbox loader via gdk-pixbuf and spawning a Rust std::thread whose first thread_local access hits __cxa_thread_atexit_impl. Against the unpatched tree the reproducer hangs (timeout 10 s); against the patched tree it loads the image and exits 0. Full glibc test suite (make check): the only difference between the intermediate build (without the main-executable fix) and the final patched tree is nptl/tst-create5, which moves from FAIL to PASS. In the test runs used here the intermediate build produced 6877 PASS / 8 FAIL and the final tree produced 6878 PASS / 7 FAIL. All seven remaining FAILs on the patched tree are environmental (missing capabilities or test-root permissions at install time) and reproduce identically on the intermediate build. The expected output of elf/tst-rtld-list-tunables was updated for the new glibc.rtld.strict_init_order line. Co-authored-by: Alexander Pevzner Signed-off-by: Artem Proskurnev Signed-off-by: Alexander Pevzner --- elf/dl-init.c | 99 ++++++++++++++++++++- elf/dl-open.c | 133 ++++++++++++++++++++++++++++- elf/dl-tunables.list | 6 ++ elf/tst-rtld-list-tunables.exp | 1 + include/link.h | 22 +++++ manual/tunables.texi | 46 ++++++++++ sysdeps/pthread/Makefile | 45 ++++++++++ sysdeps/pthread/tst-create2.c | 67 +++++++++++++++ sysdeps/pthread/tst-create2mod.c | 74 ++++++++++++++++ sysdeps/pthread/tst-create3.c | 112 ++++++++++++++++++++++++ sysdeps/pthread/tst-create3.h | 27 ++++++ sysdeps/pthread/tst-create3mod.c | 68 +++++++++++++++ sysdeps/pthread/tst-create4.c | 100 ++++++++++++++++++++++ sysdeps/pthread/tst-create4.h | 37 ++++++++ sysdeps/pthread/tst-create4mod-a.c | 30 +++++++ sysdeps/pthread/tst-create4mod-b.c | 22 +++++ sysdeps/pthread/tst-create5.c | 63 ++++++++++++++ sysdeps/pthread/tst-create6.c | 111 ++++++++++++++++++++++++ sysdeps/pthread/tst-create6.h | 26 ++++++ sysdeps/pthread/tst-create6mod.c | 72 ++++++++++++++++ 20 files changed, 1155 insertions(+), 6 deletions(-) create mode 100644 sysdeps/pthread/tst-create2.c create mode 100644 sysdeps/pthread/tst-create2mod.c create mode 100644 sysdeps/pthread/tst-create3.c create mode 100644 sysdeps/pthread/tst-create3.h create mode 100644 sysdeps/pthread/tst-create3mod.c create mode 100644 sysdeps/pthread/tst-create4.c create mode 100644 sysdeps/pthread/tst-create4.h create mode 100644 sysdeps/pthread/tst-create4mod-a.c create mode 100644 sysdeps/pthread/tst-create4mod-b.c create mode 100644 sysdeps/pthread/tst-create5.c create mode 100644 sysdeps/pthread/tst-create6.c create mode 100644 sysdeps/pthread/tst-create6.h create mode 100644 sysdeps/pthread/tst-create6mod.c diff --git a/elf/dl-init.c b/elf/dl-init.c index bd85bacdc1..072f71e237 100644 --- a/elf/dl-init.c +++ b/elf/dl-init.c @@ -20,6 +20,25 @@ #include #include #include +#include + +/* Per-DSO once-initialization for constructor execution. + l_init_once is an int used as a low-level lock (LLL): + 0 = uninitialized, 1 = initializing, 2 = initialized. + The lock is always private to the process. */ + +/* Platform-specific wait/wake primitives for once-initialization. */ +#ifdef __linux__ +# define DL_INIT_ONCE_WAIT(futexp, val, private) \ + lll_futex_wait (futexp, val, private) +# define DL_INIT_ONCE_WAKE(futexp, nr, private) \ + lll_futex_wake (futexp, nr, private) +#else +# define DL_INIT_ONCE_WAIT(futexp, val, private) \ + __lll_wait (futexp, val, private) +# define DL_INIT_ONCE_WAKE(futexp, nr, private) \ + __lll_wake (futexp, private) +#endif static void @@ -35,8 +54,31 @@ call_init (struct link_map *l, int argc, char **argv, char **env) assert (l->l_relocated || l->l_type == lt_executable); if (l->l_init_called) - /* This object is all done. */ - return; + { + /* call_init has already been invoked for this map. In a + single-threaded context this means the constructor has run + (or we are inside it via a recursive call). In a multi- + threaded context, distinguish three cases: + + (a) Constructor already completed (l_init_once == 2): done. + (b) Recursive call from the same thread that is currently + running the constructor (l_init_owner == current TID): + return immediately, otherwise we would wait for our own + constructor to finish and deadlock. + (c) Concurrent call from another thread that is currently + running the constructor: wait for completion. Without + this wait, our caller's dlopen() would return before the + constructor finishes - a regression from the pre-BZ-15686 + model where dl_load_lock serialised the whole _dl_open. */ + if (!RTLD_SINGLE_THREAD_P + && atomic_load_acquire (&l->l_init_once) != 2 + && l->l_init_owner != THREAD_GETMEM (THREAD_SELF, tid)) + { + while (atomic_load_acquire (&l->l_init_once) != 2) + DL_INIT_ONCE_WAIT (&l->l_init_once, 1, LLL_PRIVATE); + } + return; + } /* Avoid handling this constructor again in case we have a circular dependency. */ @@ -45,7 +87,49 @@ call_init (struct link_map *l, int argc, char **argv, char **env) /* Check for object which constructors we do not run here. */ if (__builtin_expect (l->l_name[0], 'a') == '\0' && l->l_type == lt_executable) - return; + { + /* The main executable's constructors are run by the startup code, + not here. Nevertheless we must mark the map as fully + initialized so that a later multi-threaded dlopen(NULL) / + __RTLD_OPENEXEC caller does not wait forever on l_init_once + in dl_open_worker. */ + atomic_store_release (&l->l_init_once, 2); + if (!RTLD_SINGLE_THREAD_P) + DL_INIT_ONCE_WAKE (&l->l_init_once, INT_MAX, LLL_PRIVATE); + return; + } + + /* When single-threaded (startup, or dlopen before any threads exist), + run the constructor inline. When multi-threaded, use per-DSO + serialisation: the first caller runs the constructor; any concurrent + caller blocks until it completes. Different DSOs can initialise + concurrently. */ + if (!RTLD_SINGLE_THREAD_P) + { + /* Fast path: already initialized. */ + if (atomic_load_acquire (&l->l_init_once) == 2) + return; + + /* Try to acquire the lock (CAS 0 -> 1). */ + if (atomic_compare_and_exchange_bool_acq (&l->l_init_once, 1, 0) != 0) + { + /* Another thread is initializing. Wait until it finishes. */ + while (atomic_load_acquire (&l->l_init_once) != 2) + DL_INIT_ONCE_WAIT (&l->l_init_once, 1, LLL_PRIVATE); + return; + } + + /* We won the CAS - record our TID so a recursive call_init from + inside the constructor (e.g. via a transitive dlopen) can + recognise itself and return without deadlocking. */ + l->l_init_owner = THREAD_GETMEM (THREAD_SELF, tid); + + /* We are now the init owner; l_init_pending has done its job of + signalling "init is scheduled" to early-return waiters in + dl_open_worker_begin. Clear it so they don't keep spinning + on it after init completes. */ + atomic_store_release (&l->l_init_pending, 0); + } /* Print a debug message if wanted. */ if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS)) @@ -73,6 +157,15 @@ call_init (struct link_map *l, int argc, char **argv, char **env) for (j = 0; j < jm; ++j) ((dl_init_t) addrs[j]) (argc, argv, env); } + + /* Mark the DSO as fully initialised so that a later call_init from + another thread (which can happen transitively when a new DSO is + loaded that depends on this one) sees l_init_once == 2 and does + not wait. In single-threaded mode there can be no waiters, so + the futex wake is skipped. */ + atomic_store_release (&l->l_init_once, 2); + if (!RTLD_SINGLE_THREAD_P) + DL_INIT_ONCE_WAKE (&l->l_init_once, INT_MAX, LLL_PRIVATE); } diff --git a/elf/dl-open.c b/elf/dl-open.c index 87fcee8b02..9930371867 100644 --- a/elf/dl-open.c +++ b/elf/dl-open.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -597,6 +598,64 @@ dl_open_worker_begin (void *a) dlopen (NULL, RTLD_LAZY) call from a constructor of an initially loaded shared object. */ + /* BZ 15686: dl_load_lock is released during the constructor on + the thread that first loaded this DSO, so this thread may have + reached the already-loaded early-return path while that + constructor is still running (or, in a tight race, after the + loader thread released the lock but before it reached + call_init - covered by l_init_pending which is set under the + lock). Without this wait, dlopen would return before the + constructor finished - a regression from the pre-BZ-15686 model + where dl_load_lock serialised the whole _dl_open. + + We only wait; we do not run the ctor ourselves. Per the + comment above, running _dl_init here could expose partially + constructed state to objects that depend on this DSO if this + dlopen call came from inside another ELF constructor. The + loader thread that scheduled the init (signalled by + l_init_pending or l_init_called) will run the ctor when it + reaches call_init. + + Fast path: if l_init_once == 2, ctor already finished. + + Release dl_load_lock before waiting so concurrent dlopen + callers are not blocked. dl_load_tls_lock (held by + dl_open_worker across this call) must be released too: the + constructor we are about to wait for may spawn a thread, and + thread creation takes dl_load_tls_lock in + _dl_allocate_tls_init. Blocking on the futex while still + holding dl_load_tls_lock would deadlock - the ctor would + wait for pthread_create, which would wait for us. + + The locks are re-acquired in the canonical nesting order: + dl_load_lock first, then dl_load_tls_lock (the same order + _dl_open and dl_open_worker take them). */ + if (!RTLD_SINGLE_THREAD_P + && (atomic_load_acquire (&new->l_init_pending) + || new->l_init_called) + && atomic_load_acquire (&new->l_init_once) != 2 + && new->l_init_owner != THREAD_GETMEM (THREAD_SELF, tid)) + { + bool unlock_for_ctor + = TUNABLE_GET (glibc, rtld, strict_init_order, + int32_t, NULL) == 0; + + if (unlock_for_ctor) + { + __rtld_lock_unlock_recursive (GL(dl_load_tls_lock)); + __rtld_lock_unlock_recursive (GL(dl_load_lock)); + } + + while (atomic_load_acquire (&new->l_init_once) != 2) + lll_futex_wait (&new->l_init_once, 1, LLL_PRIVATE); + + if (unlock_for_ctor) + { + __rtld_lock_lock_recursive (GL(dl_load_lock)); + __rtld_lock_lock_recursive (GL(dl_load_tls_lock)); + } + } + return; } @@ -792,11 +851,46 @@ dl_open_worker (void *a) int mode = args->mode; struct link_map *new = args->map; + /* By default, release dl_load_lock so constructors can spawn + threads without deadlocking (e.g. if the new thread's first + thread_local access triggers __cxa_thread_atexit_impl, which + needs dl_load_lock). See BZ 15686. + + The DSO has l_direct_opencount == 1, so a concurrent dlclose + cannot unload it. Per-DSO serialisation is handled in call_init + (dl-init.c) via l_init_once + futex, and the l_init_owner check + in call_init makes a concurrent dlopen caller wait for the + constructor to finish (rather than returning prematurely). + + 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 constructor execution. This gives a strict total order + of dlopen calls across threads - useful for diagnosing + applications that implicitly relied on that order, at the cost + of reintroducing the deadlock. */ + bool release_lock_for_ctor + = TUNABLE_GET (glibc, rtld, strict_init_order, int32_t, NULL) == 0; + + /* Signal "init is scheduled" while still holding dl_load_lock, so a + concurrent dlopen caller that takes the early-return path for an + already-loaded DSO knows to wait for the ctor even before + call_init runs. Cleared by call_init after it wins the + l_init_once CAS. See BZ 15686. */ + atomic_store_release (&new->l_init_pending, 1); + + if (release_lock_for_ctor) + __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. */ + if (release_lock_for_ctor) + __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); @@ -889,10 +983,43 @@ no more namespaces available for dlmopen()")); args.map = _dl_lookup_map (args.nsid, file); if (is_already_fully_open (args.map, mode)) { - /* We can use the fast path. */ - ++args.map->l_direct_opencount; + struct link_map *map = args.map; + + /* We can use the fast path. Account for our reference BEFORE + entering the wait below: while we wait, dl_load_lock is released, + and another caller that already holds a reference may dlclose. + Without our own increment first, the last such dlclose could + drive l_direct_opencount to 0 and unload the DSO - and us with + it. */ + ++map->l_direct_opencount; + + /* BZ 15686: dl_load_lock is released during the constructor on + the thread that first loaded this DSO, so this thread may have + reached the already-loaded fast path while that constructor is + still running (or, in a tight race, after the loader thread + released the lock but before it reached call_init - covered by + l_init_pending which is set under the lock). Without this + wait, dlopen would return before the constructor finished. + + Same logic as the early-return path in dl_open_worker_begin; + duplicated here because this fast path bypasses the worker + entirely. */ + if (!RTLD_SINGLE_THREAD_P + && (atomic_load_acquire (&map->l_init_pending) + || map->l_init_called) + && atomic_load_acquire (&map->l_init_once) != 2 + && map->l_init_owner != THREAD_GETMEM (THREAD_SELF, tid)) + { + __rtld_lock_unlock_recursive (GL(dl_load_lock)); + + while (atomic_load_acquire (&map->l_init_once) != 2) + lll_futex_wait (&map->l_init_once, 1, LLL_PRIVATE); + + __rtld_lock_lock_recursive (GL(dl_load_lock)); + } + __rtld_lock_unlock_recursive (GL(dl_load_lock)); - return args.map; + return map; } struct dl_exception exception; diff --git a/elf/dl-tunables.list b/elf/dl-tunables.list index 111649f145..5149dbe1ab 100644 --- a/elf/dl-tunables.list +++ b/elf/dl-tunables.list @@ -113,6 +113,12 @@ glibc { maxval: 2 default: 1 } + strict_init_order { + type: INT_32 + minval: 0 + maxval: 1 + default: 0 + } } mem { diff --git a/elf/tst-rtld-list-tunables.exp b/elf/tst-rtld-list-tunables.exp index 9590021f3a..2a4c7a5eda 100644 --- a/elf/tst-rtld-list-tunables.exp +++ b/elf/tst-rtld-list-tunables.exp @@ -15,3 +15,4 @@ glibc.rtld.enable_secure: 0 (min: 0, max: 1) glibc.rtld.execstack: 1 (min: 0, max: 2) glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10) glibc.rtld.optional_static_tls: 0x200 (min: 0x0, max: 0x[f]+) +glibc.rtld.strict_init_order: 0 (min: 0, max: 1) diff --git a/include/link.h b/include/link.h index 8f851d2212..1047f2ab23 100644 --- a/include/link.h +++ b/include/link.h @@ -346,6 +346,28 @@ struct link_map size_t l_relro_size; unsigned long long int l_serial; + + /* Per-DSO once-initialization control for constructor execution. + Used as a low-level lock (LLL): 0 = uninitialized, 1 = initializing, + 2 = initialized. Zero from calloc matches the unlocked state. */ + int l_init_once; + + /* TID of the thread that won the l_init_once CAS (0 -> 1) and is + currently running this DSO's constructor. Lets a recursive + call_init (originating from inside the constructor itself, e.g. + via a transitive dlopen of a circular dependency) distinguish + itself from a concurrent call_init on another thread and return + immediately instead of waiting for itself. Zero from calloc. */ + pid_t l_init_owner; + + /* Set to 1 under dl_load_lock by dl_open_worker_begin just before + releasing the lock to run the constructor. Lets a concurrent + dlopen caller that takes the early-return path for an + already-loaded DSO know that initialisation is scheduled and + worth waiting for, even before call_init has set l_init_called + and won the l_init_once CAS. Cleared to 0 by call_init once it + has won the CAS and become the init owner. Zero from calloc. */ + int l_init_pending; }; #include diff --git a/manual/tunables.texi b/manual/tunables.texi index 713f669c4c..8d835fe9b4 100644 --- a/manual/tunables.texi +++ b/manual/tunables.texi @@ -476,6 +476,52 @@ can be worked around by setting the tunable to @code{2}, where the stack is always executable. @end deftp +@deftp Tunable glibc.rtld.strict_init_order +Controls whether @theglibc{} retains @code{dl_load_lock} across the +execution of ELF constructors run by @code{dlopen}. + +The default value of @samp{0} releases @code{dl_load_lock} before running +the new objects' constructors and reacquires it afterwards. This prevents +the deadlock described in @uref{https://sourceware.org/bugzilla/show_bug.cgi?id=15686, BZ 15686}: +a constructor that spawns a thread whose first @code{thread_local} access +calls @code{__cxa_thread_atexit_impl} would otherwise block forever on +@code{dl_load_lock} held by the @code{dlopen} caller. Per-DSO +serialisation of constructor execution (via @code{l_init_once} in +@file{elf/dl-init.c}) preserves the ELF guarantee that a DSO's +dependencies are initialised before the DSO itself, and concurrent +@code{dlopen} callers wait for the in-progress constructor to finish. +This default does not change any behaviour required by POSIX or the ELF +specification; it only relaxes a loader implementation detail that +applications could observe through cross-thread @code{dlopen} ordering. + +Setting this tunable to @samp{1} reverts to the pre-BZ-15686 behaviour: +@code{dl_load_lock} is held across constructor execution, giving a +strict total order of @code{dlopen} calls across threads at the cost of +reintroducing the BZ 15686 deadlock. This is intended as an escape +hatch for applications that implicitly relied on the old total order +(e.g., plugin registries whose registration order determined behaviour) +and need time to fix the underlying assumption. + +@strong{NB:} with @samp{1}, any @code{dlopen} of a DSO whose +constructor spawns a thread touching @code{thread_local} state (directly +or via libraries like glycin, gdk-pixbuf-glycin, NSS, etc.) will +deadlock. Use only as a temporary diagnostic aid. + +This tunable is @emph{temporary} and is scheduled for removal. As +noted above, tunables are not part of the @glibcadj{} stable ABI, and +this one is more constrained still: it exists only so that downstream +distributions and application authors can surface ordering regressions +introduced by the BZ 15686 fix while they address the underlying +assumptions. Because @theglibc{} reaches end users through downstream +distributions on the order of one to two years, the tunable is +expected to remain available for approximately four releases. It will +be removed once the ecosystem is shown to work with the new default. +Distributors and application authors should treat @samp{1} as a +stop-gap, not as a supported configuration: the correct long-term fix +is to remove implicit cross-thread @code{dlopen} ordering assumptions +from the affected code. +@end deftp + @node POSIX Thread Tunables @section POSIX Thread Tunables @cindex pthread mutex tunables diff --git a/sysdeps/pthread/Makefile b/sysdeps/pthread/Makefile index d0f3cd59ac..09b46e6d1c 100644 --- a/sysdeps/pthread/Makefile +++ b/sysdeps/pthread/Makefile @@ -349,6 +349,11 @@ tests += \ tst-atfork3 \ tst-atfork4 \ tst-create1 \ + tst-create2 \ + tst-create3 \ + tst-create4 \ + tst-create5 \ + tst-create6 \ tst-fini1 \ tst-pt-tls4 \ # tests @@ -365,6 +370,11 @@ modules-names += \ tst-atfork3mod \ tst-atfork4mod \ tst-create1mod \ + tst-create2mod \ + tst-create3mod \ + tst-create4mod-a \ + tst-create4mod-b \ + tst-create6mod \ tst-fini1mod \ tst-stack2-mod \ tst-tls4moda \ @@ -377,6 +387,9 @@ 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 +tst-create4mod-a.so-no-z-defs = yes +tst-create4mod-b.so-no-z-defs = yes ifeq ($(build-shared),yes) # Build all the modules even when not actually running test programs. @@ -549,3 +562,35 @@ endif tst-stack2-TUNABLES += glibc.rtld.execstack=2 endif + +$(objpfx)tst-create2: $(shared-thread-library) +$(objpfx)tst-create2mod.so: $(shared-thread-library) +$(objpfx)tst-create2.out: $(objpfx)tst-create2mod.so + +$(objpfx)tst-create3: $(shared-thread-library) +$(objpfx)tst-create3.out: $(objpfx)tst-create3mod.so + +# tst-create4 verifies that a long-running constructor does not block +# concurrent dlopen of an unrelated library. Thread A's constructor +# blocks on a barrier; thread B dlopen's a different library. Before +# the BZ 15686 fix, dl_load_lock was held across the entire dlopen, +# including the constructor, so thread B deadlocked. After the fix, +# dl_load_lock is released during constructors and thread B succeeds. +LDFLAGS-tst-create4 = -Wl,-export-dynamic +$(objpfx)tst-create4: $(shared-thread-library) +$(objpfx)tst-create4mod-a.so: $(shared-thread-library) +$(objpfx)tst-create4mod-b.so: $(shared-thread-library) +$(objpfx)tst-create4.out: $(objpfx)tst-create4mod-a.so $(objpfx)tst-create4mod-b.so + +$(objpfx)tst-create5: $(shared-thread-library) + +# tst-create6 covers the combination that tst-create2 and tst-create3 +# exercise separately: the module constructor spawns and joins a +# thread (thread creation takes dl_load_tls_lock in +# _dl_allocate_tls_init) while a second thread concurrently dlopens +# the same DSO and waits for the constructor in dl_open_worker_begin. +# If that wait holds dl_load_tls_lock, the constructor's +# pthread_create blocks on it and the test deadlocks. +$(objpfx)tst-create6: $(shared-thread-library) +$(objpfx)tst-create6mod.so: $(shared-thread-library) +$(objpfx)tst-create6.out: $(objpfx)tst-create6mod.so diff --git a/sysdeps/pthread/tst-create2.c b/sysdeps/pthread/tst-create2.c new file mode 100644 index 0000000000..a419ed6b68 --- /dev/null +++ b/sysdeps/pthread/tst-create2.c @@ -0,0 +1,67 @@ +/* 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 + . */ + +/* 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. + + Beyond "did not deadlock", the test also verifies that the TLS + destructor actually ran (tst_create2mod_dtor_done is set by dtor) + and that dlclose unloaded the DSO: the destructor has already + executed, so no reference is left on the module's + l_tls_dtor_count and dlopen with RTLD_NOLOAD must return NULL. */ + +#include +#include +#include + +static int +do_test (void) +{ + printf ("main: dlopen tst-create2mod.so\n"); + void *h = xdlopen ("tst-create2mod.so", RTLD_NOW); + printf ("main: dlopen done\n"); + + /* The worker thread exited before the constructor's pthread_join + returned, so its TLS destructor has already run. */ + int *dtor_done = xdlsym (h, "tst_create2mod_dtor_done"); + TEST_COMPARE (*dtor_done, 1); + + xdlclose (h); + printf ("main: dlclose done\n"); + + /* The destructor already ran, so no reference is left on the + module's l_tls_dtor_count and dlclose must have unloaded it. */ + TEST_VERIFY (dlopen ("tst-create2mod.so", RTLD_NOW | RTLD_NOLOAD) + == NULL); + + return 0; +} + +#include diff --git a/sysdeps/pthread/tst-create2mod.c b/sysdeps/pthread/tst-create2mod.c new file mode 100644 index 0000000000..92c475e439 --- /dev/null +++ b/sysdeps/pthread/tst-create2mod.c @@ -0,0 +1,74 @@ +/* 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 + . */ + +#include +#include +#include + +extern int __cxa_thread_atexit_impl (void (*) (void *), void *, void *); + +/* Set to 1 by the TLS destructor when it runs. Read by the main + executable after dlopen to confirm the destructor executed. */ +int tst_create2mod_dtor_done; + +static void +dtor (void *obj) +{ + *(int *) obj = 1; +} + +/* The module TLS object mirrors the real-world trigger (a C++ + thread_local or Rust thread_local! first access), exercising + __tls_get_addr from the spawned thread as well. */ +static __thread int tls_obj; + +static void * +worker (void *arg) +{ + (void) arg; + + /* First touch of tls_obj forces __tls_get_addr, which on the + pre-BZ-15686 path is another dl_load_lock contender in addition + to __cxa_thread_atexit_impl below. */ + tls_obj = 1; + + /* Register the TLS destructor. Under the pre-fix locking model + __cxa_thread_atexit_impl acquires dl_load_lock, which is held by + the dlopen caller running this ctor, so the worker blocks here + and pthread_join in the constructor never returns. */ + if (__cxa_thread_atexit_impl (dtor, &tst_create2mod_dtor_done, + __dso_handle) != 0) + abort (); + + return &tls_obj; +} + +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 (); +} diff --git a/sysdeps/pthread/tst-create3.c b/sysdeps/pthread/tst-create3.c new file mode 100644 index 0000000000..59b25908f3 --- /dev/null +++ b/sysdeps/pthread/tst-create3.c @@ -0,0 +1,112 @@ +/* Verify that concurrent dlopen of the same DSO is safe under the + per-DSO init serialisation added for 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 + . */ + +/* Two invariants are checked for concurrent dlopen of the same DSO: + + 1. The DSO's constructor runs EXACTLY ONCE, even if N threads race + into call_init. This catches a world where two threads both + think they lost the l_init_once CAS but actually proceed. + + 2. No dlopen caller returns before the constructor has finished. + A caller that beats the constructor would observe globals in + their BSS-zeroed state -- a regression from the pre-BZ-15686 + model where dl_load_lock serialised the whole _dl_open. + + The DSO constructor sleeps ~200 ms to widen the race window, then + publishes a "done" magic as its final write. Each thread checks + the magic immediately after dlopen returns; any thread observing + the wrong value has raced ahead of the constructor. + + The ctor-runs-once count is read from a handle that is kept open + across the check: every caller leaves its dlopen handle pinned in + the handles[] array and main reads tst_create3mod_ctor_count via + xdlsym on handles[0] before any dlclose. Closing the handles + first would unload the DSO once the last reference dropped, and a + subsequent re-open would load a fresh instance whose counter + starts at zero -- the count would then reflect only the + single-threaded re-open and could not detect a constructor that + ran more than once during the concurrent phase. */ + +#include +#include +#include +#include +#include +#include + +#include "tst-create3.h" + +/* More threads than two so the race is exercised on multiple + pair-wise combinations; small enough to keep the test cheap. */ +#define NTHREADS 8 + +static pthread_barrier_t g_start_barrier; +static void *handles[NTHREADS]; + +static void * +worker (void *arg) +{ + int idx = (int) (intptr_t) arg; + + /* Release all workers at once so they enter _dl_open near-simultaneously. */ + xpthread_barrier_wait (&g_start_barrier); + + void *h = xdlopen ("tst-create3mod.so", RTLD_NOW); + + /* The "done" flag is the constructor's final write. If dlopen + returned before the constructor finished (the BZ 15686 race the + l_init_owner check in call_init prevents), this load will observe + 0 instead of TST_CREATE3_MAGIC_DONE. */ + _Atomic unsigned int *done = xdlsym (h, "tst_create3mod_done"); + unsigned int done_val = atomic_load_explicit (done, memory_order_acquire); + if (done_val != TST_CREATE3_MAGIC_DONE) + FAIL ("thread %d returned from dlopen before the constructor finished" + " (tst_create3mod_done=0x%x)", idx, done_val); + + /* Keep the handle open; main reads the ctor counter from it and + dlcloses all handles afterwards. */ + handles[idx] = h; + return NULL; +} + +static int +do_test (void) +{ + pthread_t threads[NTHREADS]; + + xpthread_barrier_init (&g_start_barrier, NULL, NTHREADS); + + for (int i = 0; i < NTHREADS; ++i) + threads[i] = xpthread_create (0, worker, (void *) (intptr_t) i); + + for (int i = 0; i < NTHREADS; ++i) + xpthread_join (threads[i]); + + /* The DSO is still loaded (all handles open), so the counter + reflects every constructor execution during the race above. */ + _Atomic int *count = xdlsym (handles[0], "tst_create3mod_ctor_count"); + TEST_COMPARE (atomic_load_explicit (count, memory_order_acquire), 1); + + for (int i = 0; i < NTHREADS; ++i) + xdlclose (handles[i]); + + return 0; +} + +#include diff --git a/sysdeps/pthread/tst-create3.h b/sysdeps/pthread/tst-create3.h new file mode 100644 index 0000000000..9a448ba2bf --- /dev/null +++ b/sysdeps/pthread/tst-create3.h @@ -0,0 +1,27 @@ +/* Shared definitions for tst-create3 and tst-create3mod. + 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 + . */ + +#ifndef _TST_CREATE3_H +#define _TST_CREATE3_H + +/* Magic value published by the module constructor as its final action. + Any dlopen caller that observes tst_create3mod_done with a different + value immediately after dlopen returned has beaten the constructor. */ +#define TST_CREATE3_MAGIC_DONE 0xCAFEBABEu + +#endif diff --git a/sysdeps/pthread/tst-create3mod.c b/sysdeps/pthread/tst-create3mod.c new file mode 100644 index 0000000000..e21d473faf --- /dev/null +++ b/sysdeps/pthread/tst-create3mod.c @@ -0,0 +1,68 @@ +/* DSO for tst-create3: concurrent dlopen constructor-once test (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 + . */ + +#include +#include + +#include "tst-create3.h" + +/* How long (nanoseconds) the constructor sleeps to simulate slow + initialisation. Large enough that, under the pre-fix locking model, + concurrent dlopen callers are very likely to reach call_init while + the constructor is still running. */ +#define TST_CREATE3_CTOR_SLEEP_NS 200000000 /* 200 ms */ + +/* Counter incremented by the constructor. Must be exactly 1 after + concurrent dlopen - catches the case where two threads both win the + CAS and run the constructor in parallel. */ +_Atomic int tst_create3mod_ctor_count = 0; + +/* Visibility marker. Set as the final write of the constructor with + release ordering. Concurrent dlopen callers must observe + TST_CREATE3_MAGIC_DONE here after their dlopen returns. */ +_Atomic unsigned int tst_create3mod_done = 0; + +static void +sleep_ns (long ns) +{ + struct timespec ts = + { + .tv_sec = ns / 1000000000L, + .tv_nsec = ns % 1000000000L + }; + nanosleep (&ts, NULL); +} + +static void __attribute__ ((constructor)) +do_init (void) +{ + /* Record that we ran. Two threads winning the l_init_once CAS + would increment this more than once. */ + atomic_fetch_add_explicit (&tst_create3mod_ctor_count, 1, + memory_order_relaxed); + + /* Slow the constructor down to widen the window in which a buggy + call_init would let a concurrent caller return from dlopen. */ + sleep_ns (TST_CREATE3_CTOR_SLEEP_NS); + + /* Publish "constructor finished" as the last write with release + ordering, so that callers observing TST_CREATE3_MAGIC_DONE also + observe every earlier write the constructor made. */ + atomic_store_explicit (&tst_create3mod_done, TST_CREATE3_MAGIC_DONE, + memory_order_release); +} diff --git a/sysdeps/pthread/tst-create4.c b/sysdeps/pthread/tst-create4.c new file mode 100644 index 0000000000..8c7eeae517 --- /dev/null +++ b/sysdeps/pthread/tst-create4.c @@ -0,0 +1,100 @@ +/* Verify that a long-running dlopen constructor does not block a + concurrent dlopen of an unrelated library. + + 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 + . */ + +/* WHAT THIS TEST CHECKS + + Thread A calls dlopen on a library whose constructor blocks on a + barrier. Once the constructor is executing (i.e. dl_load_lock has + been released by the BZ 15686 fix), thread B calls dlopen on an + unrelated library. + + Before the BZ 15686 fix, dl_load_lock was held across the entire + dlopen call, including constructor execution, so thread B's dlopen + would block on dl_load_lock forever. After the fix, dl_load_lock + is released for the duration of the constructor, so thread B's + dlopen proceeds in parallel. + + The test is deterministic (no probabilistic race): we spin until + the constructor confirms it has started, then launch thread B. */ + +#include +#include +#include +#include +#include +#include + +#include "tst-create4.h" + +/* Exported for the DSOs via -Wl,-export-dynamic (LDFLAGS-tst-create4). */ +pthread_barrier_t tst_create4_ctor_barrier; +atomic_int tst_create4_ctor_running = 0; + +static void * +worker_a (void *unused) +{ + (void) unused; + void *h = xdlopen ("tst-create4mod-a.so", RTLD_NOW); + xdlclose (h); + return NULL; +} + +static void * +worker_b (void *unused) +{ + (void) unused; + void *h = xdlopen ("tst-create4mod-b.so", RTLD_NOW); + xdlclose (h); + return NULL; +} + +static int +do_test (void) +{ + xpthread_barrier_init (&tst_create4_ctor_barrier, NULL, 2); + + /* Start thread A. It enters the constructor of tst-create4mod-a, + which sets ctor_running = 1 and then blocks on the barrier. */ + pthread_t ta = xpthread_create (0, worker_a, NULL); + + /* Spin until the constructor has definitely started. */ + while (atomic_load_explicit (&tst_create4_ctor_running, + memory_order_acquire) == 0) + sched_yield (); + + /* Now launch thread B. If dl_load_lock is released during + constructors (BZ 15686 fix), thread B's dlopen will succeed. + If the lock is still held by thread A, thread B blocks on + dl_load_lock and the test will time out. */ + pthread_t tb = xpthread_create (0, worker_b, NULL); + + /* Wait for thread B to finish. */ + xpthread_join (tb); + + printf ("info: concurrent dlopen of unrelated library succeeded\n"); + + /* Signal the barrier so thread A's constructor can return. */ + xpthread_barrier_wait (&tst_create4_ctor_barrier); + xpthread_join (ta); + + return 0; +} + +#include diff --git a/sysdeps/pthread/tst-create4.h b/sysdeps/pthread/tst-create4.h new file mode 100644 index 0000000000..2e6b79a702 --- /dev/null +++ b/sysdeps/pthread/tst-create4.h @@ -0,0 +1,37 @@ +/* Shared definitions for tst-create4 and its modules. + 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 + . */ + +#ifndef _TST_CREATE4_H +#define _TST_CREATE4_H + +#include +#include + +/* Barrier that the module constructor waits on. Main signals it after + the concurrent dlopen of the unrelated module completes. Defined in + the main executable and exported to the modules via the dynamic symbol + table (-Wl,-export-dynamic). */ +extern pthread_barrier_t tst_create4_ctor_barrier; + +/* Flag set to 1 by the module constructor once it has started (i.e. + after dl_load_lock has been released by the BZ 15686 fix). Main + spins on this flag to guarantee the constructor is executing before + it creates the second worker thread. */ +extern atomic_int tst_create4_ctor_running; + +#endif diff --git a/sysdeps/pthread/tst-create4mod-a.c b/sysdeps/pthread/tst-create4mod-a.c new file mode 100644 index 0000000000..d5a44e431a --- /dev/null +++ b/sysdeps/pthread/tst-create4mod-a.c @@ -0,0 +1,30 @@ +/* DSO A for tst-create4: constructor blocks on a barrier, simulating a + long-running initializer (e.g. glycin spawning a sandbox process). + 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 + . */ + +#include + +#include "tst-create4.h" + +static void __attribute__ ((constructor)) +init_a (void) +{ + atomic_store_explicit (&tst_create4_ctor_running, 1, + memory_order_release); + pthread_barrier_wait (&tst_create4_ctor_barrier); +} diff --git a/sysdeps/pthread/tst-create4mod-b.c b/sysdeps/pthread/tst-create4mod-b.c new file mode 100644 index 0000000000..d65340b971 --- /dev/null +++ b/sysdeps/pthread/tst-create4mod-b.c @@ -0,0 +1,22 @@ +/* DSO B for tst-create4: trivial constructor that just returns. + 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 + . */ + +static void __attribute__ ((constructor)) +init_b (void) +{ +} diff --git a/sysdeps/pthread/tst-create5.c b/sysdeps/pthread/tst-create5.c new file mode 100644 index 0000000000..34edf17dac --- /dev/null +++ b/sysdeps/pthread/tst-create5.c @@ -0,0 +1,63 @@ +/* Verify that dlopen(NULL) from a worker thread does not deadlock + after the main executable has been initialized. + 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 + . */ + +/* Reproducer for a regression in the BZ 15686 fix. + + The main executable's constructors are not run by call_init in + elf/dl-init.c; instead they are handled by the startup code. With + the per-DSO init serialization added for BZ 15686, call_init must + nevertheless mark the main executable as fully initialized by + setting l_init_once = 2. Otherwise a later multi-threaded + dlopen(NULL) / __RTLD_OPENEXEC takes the already-loaded early-return + path in dl_open_worker_begin, sees l_init_called == 1 but + l_init_once != 2, and waits forever for a constructor that will + never complete. + + This test spawns a worker thread that calls dlopen(NULL, RTLD_NOW). + If the bug is present, the call deadlocks and the test-driver + timeout surfaces the failure. After the fix, dlopen(NULL) returns + immediately. */ + +#include +#include +#include + +static void * +worker (void *arg) +{ + (void) arg; + + dprintf (1, "worker: dlopen(NULL)\n"); + void *h = xdlopen (NULL, RTLD_NOW); + dprintf (1, "worker: dlopen(NULL) done\n"); + xdlclose (h); + return NULL; +} + +static int +do_test (void) +{ + pthread_t t = xpthread_create (0, worker, NULL); + xpthread_join (t); + + dprintf (1, "main: worker finished\n"); + return 0; +} + +#include diff --git a/sysdeps/pthread/tst-create6.c b/sysdeps/pthread/tst-create6.c new file mode 100644 index 0000000000..3575226860 --- /dev/null +++ b/sysdeps/pthread/tst-create6.c @@ -0,0 +1,111 @@ +/* Verify that a concurrent dlopen caller waiting for another thread's + in-progress constructor does not hold dl_load_tls_lock while it + waits (BZ 15686 follow-up). + 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 + . */ + +/* WHAT THIS TEST CHECKS + + The BZ 15686 constructor fix releases dl_load_lock around + call_dl_init and makes a concurrent dlopen of the same DSO wait in + dl_open_worker_begin until the constructor completes. That wait + runs while dl_open_worker holds dl_load_tls_lock. If the + constructor spawns a thread, thread creation needs dl_load_tls_lock + (_dl_allocate_tls_init), producing a deadlock cycle: + + waiter: holds dl_load_tls_lock, waits for ctor end (futex) + loader: runs ctor, waits for pthread_create -> dl_load_tls_lock + + This test combines the two ingredients that tst-create2 and + tst-create3 exercise separately: a constructor that spawns and + joins a thread (tst-create6mod.c), and two threads concurrently + dlopening the same DSO. The constructor sleeps 200 ms before + pthread_create so the second caller reliably reaches the + early-return wait first. + + Without the dl_load_tls_lock release around the wait, the test + deadlocks and the test-driver timeout fires; with it, both dlopen + calls return only after the constructor published its done magic, + and the constructor ran exactly once. */ + +#include +#include +#include +#include +#include +#include + +#include "tst-create6.h" + +/* Two threads: one becomes the loader running the constructor, the + other becomes the waiter. More waiters would not change the + mechanism. */ +#define NTHREADS 2 + +static pthread_barrier_t g_start_barrier; +static void *handles[NTHREADS]; + +static void * +worker (void *arg) +{ + int idx = (int) (intptr_t) arg; + + /* Release both workers at once so they enter _dl_open + near-simultaneously. */ + xpthread_barrier_wait (&g_start_barrier); + + void *h = xdlopen ("tst-create6mod.so", RTLD_NOW); + + /* If dlopen returned before the constructor finished, the done + magic is not yet visible. */ + _Atomic unsigned int *done = xdlsym (h, "tst_create6mod_done"); + unsigned int done_val = atomic_load_explicit (done, memory_order_acquire); + if (done_val != TST_CREATE6_MAGIC_DONE) + FAIL ("thread %d returned from dlopen before the constructor finished" + " (tst_create6mod_done=0x%x)", idx, done_val); + + /* Keep the handle open so the ctor counter below reflects the + concurrent phase. */ + handles[idx] = h; + return NULL; +} + +static int +do_test (void) +{ + pthread_t threads[NTHREADS]; + + xpthread_barrier_init (&g_start_barrier, NULL, NTHREADS); + + for (int i = 0; i < NTHREADS; ++i) + threads[i] = xpthread_create (0, worker, (void *) (intptr_t) i); + + for (int i = 0; i < NTHREADS; ++i) + xpthread_join (threads[i]); + + /* Both dlopen calls returned; the DSO is still loaded via the + pinned handles. The constructor must have run exactly once. */ + _Atomic int *count = xdlsym (handles[0], "tst_create6mod_ctor_count"); + TEST_COMPARE (atomic_load_explicit (count, memory_order_acquire), 1); + + for (int i = 0; i < NTHREADS; ++i) + xdlclose (handles[i]); + + return 0; +} + +#include diff --git a/sysdeps/pthread/tst-create6.h b/sysdeps/pthread/tst-create6.h new file mode 100644 index 0000000000..7179dfdd8d --- /dev/null +++ b/sysdeps/pthread/tst-create6.h @@ -0,0 +1,26 @@ +/* Shared constants for tst-create6 and tst-create6mod. + 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 + . */ + +#ifndef TST_CREATE6_H +#define TST_CREATE6_H + +/* Final value published by the module constructor as its last write, + after the spawned thread has been created and joined. */ +#define TST_CREATE6_MAGIC_DONE 0x5eed5eedu + +#endif /* TST_CREATE6_H */ diff --git a/sysdeps/pthread/tst-create6mod.c b/sysdeps/pthread/tst-create6mod.c new file mode 100644 index 0000000000..d4aac319ba --- /dev/null +++ b/sysdeps/pthread/tst-create6mod.c @@ -0,0 +1,72 @@ +/* DSO for tst-create6: its constructor spawns and joins a thread. + Thread creation takes dl_load_tls_lock in _dl_allocate_tls_init, + so this exercises the BZ 15686 follow-up fix: a concurrent dlopen + caller waiting for this constructor in dl_open_worker_begin must + not hold dl_load_tls_lock while it waits. + 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 + . */ + +#include +#include +#include +#include + +#include "tst-create6.h" + +/* Number of times the constructor ran. Must end up == 1. */ +_Atomic int tst_create6mod_ctor_count = 0; + +/* Published as the constructor's final write; dlopen callers check + it to prove they did not return before the constructor finished. */ +_Atomic unsigned int tst_create6mod_done = 0; + +static void * +worker (void *arg) +{ + (void) arg; + /* Merely existing is enough: creating this thread already required + dl_load_tls_lock. */ + return NULL; +} + +static void __attribute__ ((constructor)) +do_init (void) +{ + atomic_fetch_add_explicit (&tst_create6mod_ctor_count, 1, + memory_order_relaxed); + + /* Give a concurrent dlopen caller time to reach the early-return + wait in dl_open_worker_begin before we need dl_load_tls_lock + below. */ + struct timespec ts = { .tv_nsec = 200000000 }; /* 200 ms */ + nanosleep (&ts, NULL); + + /* pthread_create -> allocatestack -> _dl_allocate_tls_init takes + dl_load_tls_lock. If the concurrent dlopen caller is blocked in + dl_open_worker_begin while holding dl_load_tls_lock (the bug this + test regresses), this call never returns and pthread_join below + never completes: the test hangs and the test-driver timeout + fires. */ + pthread_t t; + if (pthread_create (&t, NULL, worker, NULL) != 0) + abort (); + if (pthread_join (t, NULL) != 0) + abort (); + + atomic_store_explicit (&tst_create6mod_done, TST_CREATE6_MAGIC_DONE, + memory_order_release); +} From patchwork Mon Aug 3 20:03:24 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Artem Proskurnev X-Patchwork-Id: 140543 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id B2AF64BB24CD for ; Mon, 3 Aug 2026 20:05:29 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org B2AF64BB24CD Authentication-Results: sourceware.org; dkim=fail reason="signature verification failed" (2048-bit key, unprotected) header.d=mail.ru header.i=@mail.ru header.a=rsa-sha256 header.s=mail4 header.b=ZM5cJZSN X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from send279.i.mail.ru (send279.i.mail.ru [95.163.59.118]) by sourceware.org (Postfix) with ESMTPS id 0B8BB4BB1C37 for ; Mon, 3 Aug 2026 20:03:46 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 0B8BB4BB1C37 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=mail.ru Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=mail.ru ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 0B8BB4BB1C37 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=95.163.59.118 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1785787426; cv=none; b=UJ0r9l3PV1J4FJfCaj8aTqi7ilVCaUizYi5tV3FREyIJfZrhGojMoJaAPWqM7XPyQarx2aD+1StfuF6dMocS1YG9Cew5MxTXksji+sCGWET2t/b6LaowMggC9bI7kVXD4JIlwm5SDrm9sE0gvSME2LqrZwY80jgUNo1jszY64xA= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1785787426; c=relaxed/simple; bh=lvup774XWVPIOMHZadiIseVWTkznkuNoh8XgF5xQAYo=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=asAMoWTeMyw8ebCZGpNBfrku+xcaBOGGBo3DJdaHdmEc1UqyjAmdhQt2V4kOci4cdQTV/IpXoCjB11jEaZ1BVGwbx8Y5hGJYPfvp8OGYWS3Je4wIebWP4x1WPngCWfEn7ktqQ2nL0BgGkmBMJyTGemK+yAnHga4iSDJw9x4ESkI= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=mail.ru header.i=@mail.ru header.a=rsa-sha256 header.s=mail4 header.b=ZM5cJZSN DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 0B8BB4BB1C37 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=mail.ru; s=mail4; h=Content-Transfer-Encoding:MIME-Version:References:In-Reply-To: Message-ID:Date:Subject:Cc:To:From:From:Sender:Reply-To:To:Cc:Content-Type: Content-Transfer-Encoding:Content-ID:Content-Description:Resent-Date: Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Id: List-Help:List-Unsubscribe:List-Subscribe:List-Post:List-Owner:List-Archive: X-Cloud-Ids:Disposition-Notification-To; bh=UMWoeSfmOYlI7Fmv35d39YyyEFVEey0v/3ddMRyLfeA=; t=1785787426; x=1785877426; b=ZM5cJZSN57uaDN4bep2GWzK+up4/oEly88oaV6UjKQN2qxRqkshe6buJ1pxZvQ2e1NT8i7Hst4R K0DbWoj6FbamRI70HSHrVkTfphYiP3pTa+FHjRCd0yBN8BIAkJ57KSLK0Ly66ZLxbsB3JEE5fcus7 Hd6oCwiZDx4ids/WVz1UoKDLmpYq2ZXskN7GEGvgpsWYAlki9yvx+PyBsN8fAr0aMN22oL+8jy+CV 34Ixs9P6Eb+LgCvnUXKrJR34YyjH9zOxZguo9Dpf5mOo4kxgAZQZd7YfUS3Q2sycpEIjF96eu5EgD SADkmbg3j6t1JyKWmQ6J+nIkEV3CNgsMz9MQ==; Received: by exim-smtp-7f4897b4d8-jnp6d with esmtpa (envelope-from ) id 1wqysx-00000000ODN-14Gc; Mon, 03 Aug 2026 23:03:43 +0300 From: temap@mail.ru To: libc-alpha@sourceware.org Cc: fweimer@redhat.com, carlos@redhat.com, adhemerval.zanella@linaro.org, pzz@apevzner.com, m.novosyolov@rosa.ru, Artem Proskurnev Subject: [PATCH v7 2/4] elf: Release dl_load_lock before running dlclose destructors (BZ 15686) Date: Mon, 3 Aug 2026 23:03:24 +0300 Message-ID: <20260803200326.477666-3-temap@mail.ru> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260803200326.477666-1-temap@mail.ru> References: <20260801074707.2565716-1-temap@mail.ru> <20260803200326.477666-1-temap@mail.ru> MIME-Version: 1.0 Authentication-Results: exim-smtp-7f4897b4d8-jnp6d; auth=pass smtp.auth=temap@mail.ru smtp.mailfrom=temap@mail.ru X-Mailru-Src: smtp X-7564579A: 78E4E2B564C1792B X-77F55803: 4F1203BC0FB41BD9339FDC13DC05525F6CCD29DBC03D75DEDD9EEE5397E7047E1867C24CE74E72BB5FE18E9CBFBE7C0782E76C5FC542EEAD8F5BD48EAA943B3BC40C7FAD22D4EE5BC129AD43B0A4674908D917D6130B1AFB X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE769BF540C95EF782EEA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F7900637AC83A81C8FD4AD23D82A6BABE6F325AC2E85FA5F3EDFCBAA7353EFBB553375665029C9298A93E94DAAED8741EEB4351CD2DC56DD8BB9F3EB7C3B994F74B3B3BA8EEF46B7454FC60B9742502CCDD46D0D1D471462564A2E19F6B57BC7E64490618DEB871D839B73339E8FC8737B5C2249D07623A0E6354027CC7F00164DA146DAFE8445B8C89999729449624AB7ADAF37F6B57BC7E64490611E7FA7ABCAF51C92176DF2183F8FC7C0565C7A4E90E531F78941B15DA834481F9449624AB7ADAF37BA3038C0950A5D3613377AFFFEAFD269176DF2183F8FC7C07E89C8E9C5A60C077B076A6E789B0E97A8DF7F3B2552694AD5FFEEA1DED7F25D49FD398EE364050F26055571C92BF10F985B8ACC81218E19B3661434B16C20ACC84D3B47A649675FE827F84554CEF5019E625A9149C048EE33AC447995A7AD182BEBFE083D3B9BA73A03B725D353964B0B7D0EA88DDEDAC722CA9DD8327EE4930A3850AC1BE2E735C96613F75B7D048DC4224003CC83647689D4C264860C145E X-C1DE0DAB: 0D63561A33F958A56424874E20EB8D3B5002B1117B3ED696A57A7B8FF31CF335C89B063BDC7FAC353610D81D389A125CDE35189EBF2DEA281E618B5D5F965AFD68BB0711B762D62EB2D1C9E48B6DC60AE2A7E19323554B1272597DDA290DF33208614A52E06EBD9F4EAF44D9B582CE87C8A4C02DF684249CC203C45FEA855C8F X-C8649E89: 1C3962B70DF3F0AD73CAD6646DEDE1918E10F71CB4DF9F9677DD89D51EBB774225B6776AC983F447FC0B9F89525902EE6F57B2FD27647F25E66C117BDB76D659443B414091CDA4B19CEBEA0D7606676EE5D44202C8CFFB26ECE199F45E4350B1DDFFAA418F802871B8341EE9D5BE9A0AFB984B999C9F66B5248AF0251BFDC22D042DC3E2FDD53412DABE3362BFED2FD64C41F94D744909CE8FFD5CA72B28909428BE7793689043A537E69C174A41D00C X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu53w8ahmwBjZKM/YPHZyZHvz5uv+WouB9+ObcCpyrx6l7KImUglyhkEat/+ysWwi0gdhEs0JGjl6ggRWTy1haxBpVdbIX1nthFXMZebaIdHP2ghjoIc/363UZI6Kf1ptIMVYrk7BQKFwEt7pUaW8IEKm+k32EOo7Rc5w== X-Mailru-Sender: 583F1D7ACE8F49BDC25C0C59A06B2F963F62B5A2732737CDB951B70A5BD4BD8E31A7CB3D8A1906639BF697A9CA01BD4B981BBF36307557118FCA44E9AC9C8EC2EEE2A91DED5447003DDE9B364B0DF289AE208404248635DF X-Mras: Ok X-Spam-Status: No, score=-11.3 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, FREEMAIL_FROM, GIT_PATCH_0, KAM_SHORT, SPF_HELO_PASS, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org From: Artem Proskurnev 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 --- 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 #include #include +#include #include @@ -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 + . */ + +/* 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 +#include +#include +#include +#include +#include +#include + +/* 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 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 + . */ + +#include +#include + +/* 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 + . */ + +/* 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 +#include +#include +#include +#include +#include +#include +#include + +/* 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 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 + . */ + +#include +#include + +/* 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 + . */ + +/* 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 + . */ + +/* 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 +#include +#include +#include + +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 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 + . */ + +#include + +/* 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 + . */ + +/* 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 + . */ + +/* 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 +#include +#include +#include +#include +#include +#include + +/* 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 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 + . */ + +#include + +/* 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; From patchwork Mon Aug 3 20:03:25 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Artem Proskurnev X-Patchwork-Id: 140542 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 7CBD54BB24C3 for ; Mon, 3 Aug 2026 20:04:45 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 7CBD54BB24C3 Authentication-Results: sourceware.org; dkim=fail reason="signature verification failed" (2048-bit key, unprotected) header.d=mail.ru header.i=@mail.ru header.a=rsa-sha256 header.s=mail4 header.b=Oha0ZaCm X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from send218.i.mail.ru (send218.i.mail.ru [95.163.59.57]) by sourceware.org (Postfix) with ESMTPS id D4C694BB24C8 for ; Mon, 3 Aug 2026 20:03:47 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D4C694BB24C8 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=mail.ru Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=mail.ru ARC-Filter: OpenARC Filter v1.0.0 sourceware.org D4C694BB24C8 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=95.163.59.57 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1785787428; cv=none; b=tnd+2jkFOkWhIC+vnr3PbRS6/xmvmP3lb/IQ8mGnSJKCnLdSMTFPwRIj36dVjsX32Zgw+ViH/q4728mzBVtiT8I9F8Mo4FhK5WGd051Eoq89GYRZFEA6xDstKDI5MPygjZxl9IJY28kwEdRLzbz3c30NVsFZ7EcTXaV8raRAqsE= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1785787428; c=relaxed/simple; bh=ewVJcYW+ZmtaKVELtaAPHpSdPj3Xu5h0TofhWGr/nIA=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=U5vT+Ch2SXJyzY4GwirXqQ/AN7o3Qp45zMREHgvL50zfqjTZNlHAuuvAEojbkqGF1Vo5/SWKc2mEPVYXMlV8dYZoRA5wBKJ0Xl+wXiNLYYOKoQVvDtZUkz1boTztOochoHbzK0TXqsMhSE7J3AG846QfuDnaqNvESuYGP22HGYA= ARC-Authentication-Results: i=1; sourceware.org DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=mail.ru; s=mail4; h=Content-Transfer-Encoding:MIME-Version:References:In-Reply-To: Message-ID:Date:Subject:Cc:To:From:From:Sender:Reply-To:To:Cc:Content-Type: Content-Transfer-Encoding:Content-ID:Content-Description:Resent-Date: Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Id: List-Help:List-Unsubscribe:List-Subscribe:List-Post:List-Owner:List-Archive: X-Cloud-Ids:Disposition-Notification-To; bh=/JUAh9WfHwLM9ztDnFWT7jN9WngDYqC9fNDA98yQ+sg=; t=1785787427; x=1785877427; b=Oha0ZaCmwtWnV+yn7W9h2e5WxiS3ubuuZ+ibujPVruQZ2dmK8yWfYvN7/JfdaMHsHRU8oi+1dtx gSaqVwEvWnLa5ZOO7w4fHZuUf2nEuk/QRpbBLV1GZwuQUbYxTurYk9ZQ64tlda/kNtGXnehOMTtJC ZX08ezoOKx2HZZqjyOuRVGzVPJLMFEHwYWqqWoUOdMfApNC6+pR65EU/SeSijzjCXmpQjz+YYSmNn RiC6/2/jN7BCVmX0A2rKn4l9dSOgu7dWQxKwNgFU5OQTVZ8+I90DPzNdRVUOmWdvwkguHcoldFPc6 xwZOEd0vqpa7v8UkBHrBIRDMFlFb+aiVSOig==; Received: by exim-smtp-7f4897b4d8-jnp6d with esmtpa (envelope-from ) id 1wqysz-00000000ODN-1I3q; Mon, 03 Aug 2026 23:03:45 +0300 From: temap@mail.ru To: libc-alpha@sourceware.org Cc: fweimer@redhat.com, carlos@redhat.com, adhemerval.zanella@linaro.org, pzz@apevzner.com, m.novosyolov@rosa.ru, Artem Proskurnev Subject: [PATCH v7 3/4] elf: Add LD_DEBUG=loadlock to trace dl_load_lock acquisitions (BZ 15686) Date: Mon, 3 Aug 2026 23:03:25 +0300 Message-ID: <20260803200326.477666-4-temap@mail.ru> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260803200326.477666-1-temap@mail.ru> References: <20260801074707.2565716-1-temap@mail.ru> <20260803200326.477666-1-temap@mail.ru> MIME-Version: 1.0 Authentication-Results: exim-smtp-7f4897b4d8-jnp6d; auth=pass smtp.auth=temap@mail.ru smtp.mailfrom=temap@mail.ru X-Mailru-Src: smtp X-7564579A: 646B95376F6C166E X-77F55803: 4F1203BC0FB41BD9DFD0580A18F85590776127640D954392C793199563D63247182A05F5380850405E8B996AC3B70B253DE06ABAFEAF670598E743DC6196B08E60A95275554C5115492007A4EDD3F072 X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE79B5CC362CEDE941CEA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F7900637AC83A81C8FD4AD23D82A6BABE6F325AC2E85FA5F3EDFCBAA7353EFBB553375665029C9298A93E94D021FD26671F8F06AD2DC56DD8BB9F3EBE4A1BCBC74E0869B8EEF46B7454FC60B9742502CCDD46D0D1D471462564A2E19F6B57BC7E64490618DEB871D839B73339E8FC8737B5C2249D6FBC3EC642A93BBCC7F00164DA146DAFE8445B8C89999729449624AB7ADAF37F6B57BC7E64490611E7FA7ABCAF51C92176DF2183F8FC7C0DCF4F0DC832992758941B15DA834481F9449624AB7ADAF37BA3038C0950A5D3613377AFFFEAFD269176DF2183F8FC7C083875F10470907937B076A6E789B0E97A8DF7F3B2552694AD5FFEEA1DED7F25D49FD398EE364050FB28585415E75ADA9287C8E22D4AE2A51B3661434B16C20ACC84D3B47A649675FE827F84554CEF5019E625A9149C048EE9ECD01F8117BC8BEE2021AF6380DFAD18AA50765F790063735872C767BF85DA227C277FBC8AE2E8B72A01BE2E107A27975ECD9A6C639B01B4E70A05D1297E1BBCB5012B2E24CD356 X-C1DE0DAB: 0D63561A33F958A525D22474C634E6A35002B1117B3ED696C1568EBD6D9FC6BE47A99E6294EE8661823CB91A9FED034534781492E4B8EEAD4444CFA9DED63FEDC79554A2A72441328621D336A7BC284946AD531847A6065A535571D14F44ED41 X-C8649E89: 1C3962B70DF3F0AD73CAD6646DEDE191716CD42B3DD1D34C77DD89D51EBB774225B6776AC983F447FC0B9F89525902EE6F57B2FD27647F25E66C117BDB76D659FED51BAD854D1A6C76343ADE5AF474CCBD3432D7702EF3CB80ECD7952868AE2EC30D720CE4E3B346B8341EE9D5BE9A0A03734A6D3A618BF5248AF0251BFDC22D18E24BE929833C9EDABE3362BFED2FD64C41F94D744909CE8FFD5CA72B28909428BE7793689043A537E69C174A41D00C X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu53w8ahmwBjZKM/YPHZyZHvz5uv+WouB9+ObcCpyrx6l7KImUglyhkEat/+ysWwi0gdhEs0JGjl6ggRWTy1haxBpVdbIX1nthFXMZebaIdHP2ghjoIc/363UZI6Kf1ptIMVYrk7BQKFwEt7pUaW8IEKm+DpTQK6Whuig== X-Mailru-Sender: 583F1D7ACE8F49BDC25C0C59A06B2F96294508203FE81D55B951B70A5BD4BD8EC5BBD93F38968E41830BDBF01570D826981BBF36307557118FCA44E9AC9C8EC2EEE2A91DED5447003DDE9B364B0DF289AE208404248635DF X-Mras: Ok X-Spam-Status: No, score=-11.3 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, FREEMAIL_FROM, GIT_PATCH_0, KAM_SHORT, SPF_HELO_PASS, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org From: Artem Proskurnev For BZ #15686 dynamic-loader diagnostic that logs every dl_load_lock acquisition site together with a backtrace, so that residual deadlocks of this shape -- code running inside an ELF constructor (or a thread it spawns) that re-enters the loader and blocks on dl_load_lock -- can be located without a debugger. This adds such a diagnostic, covering the dlopen constructor path. A new LD_DEBUG keyword, "loadlock", enables a new dl_debug_mask bit, DL_DEBUG_LOADLOCK. While active, each acquire/release of dl_load_lock on the _dl_open path prints "dl_load_lock at " followed by a return-address backtrace. This commit instruments the constructor side (elf/dl-open.c); the destructor side (elf/dl-close.c) will be handled in a follow-up. The backtrace is obtained by walking the frame-pointer chain directly via __builtin_frame_address. This is deliberately lock-free, allocation-free and syscall-free: the trace may be emitted while dl_load_lock is held, so the libc backtrace() helper and any symbol resolution are off limits -- both would re-enter the dynamic loader (dl_iterate_phdr / _dl_addr) and either recurse or deadlock against dl_load_lock itself. Raw code addresses are printed and resolved offline with addr2line. Frame pointers must be present in the code being traced, which is the default for the dynamic linker on the targets of interest; when they are absent the walk simply terminates early and only the site header is printed. No behaviour changes; the option is purely diagnostic. Signed-off-by: Artem Proskurnev --- elf/Makefile | 5 ++ elf/dl-debug.c | 42 ++++++++++++ elf/dl-open.c | 32 ++++++++- elf/rtld.c | 2 + elf/tst-debug-loadlock-mod.c | 4 ++ elf/tst-debug-loadlock.c | 127 +++++++++++++++++++++++++++++++++++ manual/dynlink.texi | 11 +++ sysdeps/generic/ldsodefs.h | 12 ++++ 8 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 elf/tst-debug-loadlock-mod.c create mode 100644 elf/tst-debug-loadlock.c diff --git a/elf/Makefile b/elf/Makefile index 94c5b7e6ed..5553cecbb5 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -432,6 +432,7 @@ tests += \ tst-big-note \ tst-bz26577 \ tst-bz26577-minstack \ + tst-debug-loadlock \ tst-debug1 \ tst-deep1 \ tst-dl-is_dso \ @@ -959,6 +960,7 @@ modules-names += \ tst-auditmod9b \ tst-auxvalmod \ tst-big-note-lib \ + tst-debug-loadlock-mod \ tst-deep1mod1 \ tst-deep1mod2 \ tst-deep1mod3 \ @@ -2891,6 +2893,9 @@ $(objpfx)tst-nodelete-dlclose.out: $(objpfx)tst-nodelete-dlclose-dso.so \ $(objpfx)tst-debug1.out: $(objpfx)tst-debug1mod1.so +$(objpfx)tst-debug-loadlock.out: $(objpfx)tst-debug-loadlock-mod.so +tst-debug-loadlock-ARGS = -- $(host-test-program-cmd) + $(objpfx)tst-debug1mod1.so: $(objpfx)testobj1.so $(OBJCOPY) --only-keep-debug $< $@ diff --git a/elf/dl-debug.c b/elf/dl-debug.c index 3105bad93a..bca5c2a407 100644 --- a/elf/dl-debug.c +++ b/elf/dl-debug.c @@ -167,3 +167,45 @@ _dl_debug_initialize (ElfW(Addr) ldbase, Lmid_t ns) return &r->base; } + +/* Log a dl_load_lock acquisition/release site together with a raw-address + backtrace. Called only when the DL_DEBUG_LOADLOCK mask is set. + + The backtrace is obtained by walking the frame-pointer chain directly. + This is deliberately lock-free, allocation-free and syscall-free: we may + be running inside an _dl_load_lock critical section, so we cannot call + the libc backtrace() helper or perform any symbol resolution (both would + re-enter the dynamic linker and either recurse or deadlock against + dl_load_lock itself). Resolve the printed addresses offline with + addr2line(1). Frame pointers must be present in the code being traced + (the default for the dynamic linker on most targets). */ +void +_dl_debug_loadlock (const char *action, const char *site) +{ + _dl_debug_printf ("dl_load_lock %s at %s\n", action, site); + + struct layout + { + struct layout *next; + void *ret; + }; + struct layout *p = (struct layout *) __builtin_frame_address (0); + /* Bound the frame-pointer walk to the current stack so a chain that runs + into frame-pointer-less code (e.g. -O2 libc, which does not set up rbp) + terminates instead of dereferencing garbage. The stack grows down, so + caller frames sit at strictly higher addresses: require a monotonic, + in-range, aligned advance. No symbol resolution is performed (it would + re-enter the loader and deadlock on dl_load_lock); resolve the printed + addresses offline with addr2line(1). */ + uintptr_t start = (uintptr_t) p; + for (int n = 0; p != NULL && n < 32; ++n) + { + _dl_debug_printf (" #%d 0x%lx\n", n, (unsigned long int) p->ret); + uintptr_t naddr = (uintptr_t) p->next; + if (naddr <= (uintptr_t) p + || naddr - start > (1u << 20) + || (naddr & 7) != 0) + break; + p = p->next; + } +} diff --git a/elf/dl-open.c b/elf/dl-open.c index 9930371867..86fa3cfcfb 100644 --- a/elf/dl-open.c +++ b/elf/dl-open.c @@ -43,6 +43,17 @@ #include +/* When LD_DEBUG=loadlock is active, log a dl_load_lock acquire/release site + together with a raw-address backtrace (see _dl_debug_loadlock). Inlined so + the hot path costs only a single mask test when the flag is off. */ +static inline void +trace_load_lock (const char *action, const char *site) +{ + if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_LOADLOCK)) + _dl_debug_loadlock (action, site); +} + + /* We must be careful not to leave us in an inconsistent state. Thus we catch any error and re-raise it after cleaning up. */ @@ -644,6 +655,8 @@ dl_open_worker_begin (void *a) { __rtld_lock_unlock_recursive (GL(dl_load_tls_lock)); __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", + "dl_open_worker_begin(already-loaded)"); } while (atomic_load_acquire (&new->l_init_once) != 2) @@ -652,6 +665,8 @@ dl_open_worker_begin (void *a) if (unlock_for_ctor) { __rtld_lock_lock_recursive (GL(dl_load_lock)); + trace_load_lock ("acquire", + "dl_open_worker_begin(already-loaded)"); __rtld_lock_lock_recursive (GL(dl_load_tls_lock)); } } @@ -879,7 +894,10 @@ dl_open_worker (void *a) atomic_store_release (&new->l_init_pending, 1); if (release_lock_for_ctor) - __rtld_lock_unlock_recursive (GL(dl_load_lock)); + { + __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "dl_open_worker(for-ctor)"); + } /* Run the initializer functions of new objects. Temporarily disable the exception handler, so that lazy binding failures are @@ -889,7 +907,10 @@ dl_open_worker (void *a) /* Re-acquire dl_load_lock for the final global scope update and for the lock/unlock pairing expected by _dl_open. */ if (release_lock_for_ctor) - __rtld_lock_lock_recursive (GL(dl_load_lock)); + { + __rtld_lock_lock_recursive (GL(dl_load_lock)); + trace_load_lock ("acquire", "dl_open_worker(for-ctor)"); + } /* Now we can make the new map available in the global scope. */ if (mode & RTLD_GLOBAL) @@ -911,6 +932,7 @@ _dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid, /* Make sure we are alone. */ __rtld_lock_lock_recursive (GL(dl_load_lock)); + trace_load_lock ("acquire", "_dl_open"); if (__glibc_unlikely (nsid == LM_ID_NEWLM)) { @@ -923,6 +945,7 @@ _dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid, { /* No more namespace available. */ __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "_dl_open(no-namespace)"); _dl_signal_error (EINVAL, file, NULL, N_("\ no more namespaces available for dlmopen()")); @@ -1011,14 +1034,17 @@ no more namespaces available for dlmopen()")); && map->l_init_owner != THREAD_GETMEM (THREAD_SELF, tid)) { __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "_dl_open(fast-path wait)"); while (atomic_load_acquire (&map->l_init_once) != 2) lll_futex_wait (&map->l_init_once, 1, LLL_PRIVATE); __rtld_lock_lock_recursive (GL(dl_load_lock)); + trace_load_lock ("acquire", "_dl_open(fast-path wait)"); } __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "_dl_open(fast-path return)"); return map; } @@ -1059,6 +1085,7 @@ no more namespaces available for dlmopen()")); /* Release the lock. */ __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "_dl_open(error cleanup)"); /* Reraise the error. */ _dl_signal_exception (errcode, &exception, NULL); @@ -1070,6 +1097,7 @@ no more namespaces available for dlmopen()")); /* Release the lock. */ __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "_dl_open(done)"); return args.map; } diff --git a/elf/rtld.c b/elf/rtld.c index fc053df858..df3e70eee6 100644 --- a/elf/rtld.c +++ b/elf/rtld.c @@ -2438,6 +2438,8 @@ process_dl_debug (struct dl_main_state *state, const char *dl_debug) DL_DEBUG_STATISTICS }, { LEN_AND_STR ("unused"), "determined unused DSOs", DL_DEBUG_UNUSED }, + { LEN_AND_STR ("loadlock"), "log dl_load_lock acquire/release sites", + DL_DEBUG_LOADLOCK }, { LEN_AND_STR ("help"), "display this help message and exit", DL_DEBUG_HELP }, }; diff --git a/elf/tst-debug-loadlock-mod.c b/elf/tst-debug-loadlock-mod.c new file mode 100644 index 0000000000..dbffd9a39a --- /dev/null +++ b/elf/tst-debug-loadlock-mod.c @@ -0,0 +1,4 @@ +/* Loadable module for elf/tst-debug-loadlock.c. + No constructor is needed: the dl_load_lock acquire/release traces fire + on every dlopen regardless of whether the target runs initializers. */ +int tst_debug_loadlock_mod_variable = 1; diff --git a/elf/tst-debug-loadlock.c b/elf/tst-debug-loadlock.c new file mode 100644 index 0000000000..52623c097e --- /dev/null +++ b/elf/tst-debug-loadlock.c @@ -0,0 +1,127 @@ +/* Test for LD_DEBUG=loadlock. + Verifies that dl_load_lock acquisitions/releases on the dlopen constructor + path are logged with a backtrace when LD_DEBUG=loadlock is active, and that + "loadlock" appears in LD_DEBUG=help output. + + The dl_debug_mask is set by rtld only at process startup, so both checks + re-exec this binary as a child (under the freshly built ld.so, via + $(host-test-program-cmd) passed in tst-debug-loadlock-ARGS) with LD_DEBUG + set in the child environment, and capture the child's std streams. The + trace lines themselves are emitted by the dynamic linker (elf/dl-debug.c, + elf/dl-open.c). + + 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 + . */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/* Child mode (--restart): perform the dlopen whose dl_load_lock sites we want + rtld to trace, then exit. */ +static int restart; +#define CMDLINE_OPTIONS \ + { "restart", no_argument, &restart, 1 }, + +static int +handle_restart (void) +{ + void *h = dlopen ("tst-debug-loadlock-mod.so", RTLD_LAZY); + if (h == NULL) + { + fprintf (stderr, "dlopen failed: %s\n", dlerror ()); + _exit (1); + } + dlclose (h); + _exit (0); +} + +static int +do_test (int argc, char *argv[]) +{ + if (restart) + return handle_restart (); + + /* Re-exec ourselves under the freshly built ld.so. argv[1..] holds the + ld.so invocation prefix (the tokens of $(host-test-program-cmd)) supplied + via tst-debug-loadlock-ARGS; support_test_main has already stripped the + leading "--" separator used to terminate option parsing. */ + char *spargv[argc + 2]; + int i = 0; + for (; i < argc - 1; i++) + spargv[i] = argv[i + 1]; + spargv[i] = NULL; + + /* Check 1: LD_DEBUG=help lists "loadlock". rtld prints the table (via + _dl_printf, i.e. to stdout) and _exit()s before main runs, so the child + need not (and will not) reach handle_restart. */ + setenv ("LD_DEBUG", "help", 1); + { + struct support_capture_subprocess p + = support_capture_subprogram (spargv[0], spargv, NULL); + support_capture_subprocess_check (&p, "tst-debug-loadlock (help)", 0, + sc_allow_stdout); + if (strstr (p.out.buffer, "loadlock") == NULL) + { + support_record_failure (); + printf ("LD_DEBUG=help stdout was:\n%s\n", p.out.buffer); + FAIL_EXIT1 ("'loadlock' missing from LD_DEBUG=help output"); + } + support_capture_subprocess_free (&p); + } + + /* Check 2: a real dlopen with LD_DEBUG=loadlock emits the dl_load_lock + acquire trace for _dl_open and the BZ 15686 release-for-ctor site. + Trace goes to stderr (dl_debug_fd defaults to STDERR_FILENO). */ + setenv ("LD_DEBUG", "loadlock", 1); + { + spargv[i++] = (char *) "--restart"; + spargv[i] = NULL; + struct support_capture_subprocess p + = support_capture_subprogram (spargv[0], spargv, NULL); + support_capture_subprocess_check (&p, "tst-debug-loadlock (loadlock)", 0, + sc_allow_stderr); + unsetenv ("LD_DEBUG"); + + static const char *const needles[] = + { + "dl_load_lock acquire at _dl_open", + "dl_open_worker(for-ctor)", + }; + for (int k = 0; k < (int) array_length (needles); k++) + if (strstr (p.err.buffer, needles[k]) == NULL) + { + support_record_failure (); + printf ("LD_DEBUG=loadlock stderr was:\n%s\n", p.err.buffer); + FAIL_EXIT1 ("'%s' missing from trace", needles[k]); + } + support_capture_subprocess_free (&p); + } + + return 0; +} + +#define TEST_FUNCTION_ARGV do_test +#include diff --git a/manual/dynlink.texi b/manual/dynlink.texi index ad4da753a5..5569830f23 100644 --- a/manual/dynlink.texi +++ b/manual/dynlink.texi @@ -407,6 +407,17 @@ Display relocation statistics. @item unused Determined unused DSOs. +@item loadlock +Log every acquisition and release of @code{dl_load_lock} on the @code{dlopen} +constructor path, each followed by a raw return-address backtrace. Use this to +diagnose deadlocks of the shape described in +@uref{https://sourceware.org/bugzilla/show_bug.cgi?id=15686, BZ 15686}, where +code running inside an ELF constructor (or a thread it spawns) re-enters the +dynamic linker and blocks on @code{dl_load_lock}. The backtrace lists raw code +addresses; resolve them offline with @command{addr2line}. Frame pointers must +be present in the code being traced (the default for the dynamic linker on most +targets). + @item help Display a help message with all available options and exit. @end table diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h index c0deb11c02..d617864efd 100644 --- a/sysdeps/generic/ldsodefs.h +++ b/sysdeps/generic/ldsodefs.h @@ -533,6 +533,10 @@ struct rtld_global_ro #define DL_DEBUG_HELP (1 << 10) #define DL_DEBUG_TLS (1 << 11) #define DL_DEBUG_SECURITY (1 << 12) +/* Trace dl_load_lock acquisitions (with a raw-address backtrace) to help + diagnose deadlocks where code running inside a dlopen constructor (or a + thread it spawns) re-enters the dynamic linker. */ +#define DL_DEBUG_LOADLOCK (1 << 13) /* Platform name. */ EXTERN const char *_dl_platform; @@ -766,6 +770,14 @@ extern void _dl_debug_printf (const char *fmt, ...) extern void _dl_debug_printf_c (const char *fmt, ...) __attribute__ ((__format__ (__printf__, 1, 2))) attribute_hidden; +/* Print "dl_load_lock at " followed by a raw-address + backtrace obtained by walking the frame-pointer chain. The caller must + have already checked the DL_DEBUG_LOADLOCK mask. No symbol resolution + is performed (it would re-enter the loader and deadlock on + dl_load_lock); resolve the printed addresses offline with addr2line. */ +extern void _dl_debug_loadlock (const char *action, const char *site) + attribute_hidden; + /* Write a message on the specified descriptor FD. The parameters are interpreted as for a `printf' call. */ From patchwork Mon Aug 3 20:03:26 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Artem Proskurnev X-Patchwork-Id: 140540 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 377984BB24F0 for ; Mon, 3 Aug 2026 20:04:17 +0000 (GMT) X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from send197.i.mail.ru (send197.i.mail.ru [95.163.59.36]) by sourceware.org (Postfix) with ESMTPS id C8AAA4BB24D4 for ; Mon, 3 Aug 2026 20:03:49 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org C8AAA4BB24D4 Authentication-Results: sourceware.org; dmarc=pass (p=reject dis=none) header.from=mail.ru Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=mail.ru ARC-Filter: OpenARC Filter v1.0.0 sourceware.org C8AAA4BB24D4 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=95.163.59.36 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1785787430; cv=none; b=NmL+hj1AVVVpzwoS5KwTJFdpNsNkS1YI/hoPrkCwEsUOXHlmsIH/ZCeJWsw4khv2BsMWeV23WMEjCTL0CQQiI+e23V6+SsGx6YhZN9yp57/dH45sYgokWxh+PVy3XGrOb52FqssJu+it+wZfAqzcbgYhglOrKbjilq0601qhWcc= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1785787430; c=relaxed/simple; bh=x3hgypw1w9AN3iM1YsI361gDzETz3VtO/J1UvpvHFEw=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=Hgi2A1MLQ2x2rNv3HwMumsC5Sh4/ESKOA3mVaNYlKQnQ0WWD9gUhftAiQyy2dHRTKOqBZR+AKitsiVM8bhVlTrmRhEumdoyN3dtk6vsxzQZLsbF7u3sz5GFNAByo6FvZB8Z0nhiH9eDDI9YCvQhFvrM6ctsygDqy3IKo1OQRkgo= ARC-Authentication-Results: i=1; sourceware.org DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=mail.ru; s=mail4; h=Content-Transfer-Encoding:MIME-Version:References:In-Reply-To: Message-ID:Date:Subject:Cc:To:From:From:Sender:Reply-To:To:Cc:Content-Type: Content-Transfer-Encoding:Content-ID:Content-Description:Resent-Date: Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Id: List-Help:List-Unsubscribe:List-Subscribe:List-Post:List-Owner:List-Archive: X-Cloud-Ids:Disposition-Notification-To; bh=Ou56BdAsj1rDXmCxoBsxO73LogEm1GVaE3y/9AddB4c=; t=1785787429; x=1785877429; b=N6tVysRLVEPO7qbAbdpBdTrsyRaJ0IyfixP+VBDm0BAipKHa35+gObzZtYfcWnx4EbZPd9w4Zoi xuWJFK0ynsb6DaRKn2dRW29vPe2Kncbj3Jam9Q4sB4c3kTKl7H6WJ6uRaktHAF9jw5UL96aL3rO4C unyqhPJoR7A3sl0aIbxkrMKwhYK3F0vV3onFSBYqkQl/JJM/TOtN9m3rHjRuzVRvNSyO4wvVWNu/t oHNCC/T36BV9SK06tAVd0s0+mec1EZ6jCkEw+RYhd4ONRauZVf/1UzNVfxxjLG1rl5LcgRVz94JJf jcZdaow8gPKUxcJ24N/1Xhy97I6THtxg4kPA==; Received: by exim-smtp-7f4897b4d8-jnp6d with esmtpa (envelope-from ) id 1wqyt1-00000000ODN-1mFe; Mon, 03 Aug 2026 23:03:47 +0300 From: temap@mail.ru To: libc-alpha@sourceware.org Cc: fweimer@redhat.com, carlos@redhat.com, adhemerval.zanella@linaro.org, pzz@apevzner.com, m.novosyolov@rosa.ru, Artem Proskurnev Subject: [PATCH v7 4/4] elf: Add LD_DEBUG=loadlock tracing for the dlclose destructor path (BZ 15686) Date: Mon, 3 Aug 2026 23:03:26 +0300 Message-ID: <20260803200326.477666-5-temap@mail.ru> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260803200326.477666-1-temap@mail.ru> References: <20260801074707.2565716-1-temap@mail.ru> <20260803200326.477666-1-temap@mail.ru> MIME-Version: 1.0 Authentication-Results: exim-smtp-7f4897b4d8-jnp6d; auth=pass smtp.auth=temap@mail.ru smtp.mailfrom=temap@mail.ru X-Mailru-Src: smtp X-7564579A: 646B95376F6C166E X-77F55803: 4F1203BC0FB41BD9DFD0580A18F8559024A32C80DF5F49B590795081DF7C7FD9182A05F538085040CC227549B05F0ABC3DE06ABAFEAF6705B16955B1BF919EAC60A95275554C5115450968D14EBE582C X-7FA49CB5: FF5795518A3D127A4AD6D5ED66289B5278DA827A17800CE75909A206F8DB96D1EA1F7E6F0F101C67BD4B6F7A4D31EC0BCC500DACC3FED6E28638F802B75D45FF8AA50765F7900637AC83A81C8FD4AD23D82A6BABE6F325AC2E85FA5F3EDFCBAA7353EFBB553375665029C9298A93E94DDA6FD1D62AE0E8B3D2DC56DD8BB9F3EB52D1D5A851F392618EEF46B7454FC60B9742502CCDD46D0D0CABCCA60F52D7EBF6B57BC7E64490618DEB871D839B73339E8FC8737B5C22491638054B7D09EC08CC7F00164DA146DAFE8445B8C89999729449624AB7ADAF37F6B57BC7E64490611E7FA7ABCAF51C92176DF2183F8FC7C058C1844A7A85E7B68941B15DA834481F9449624AB7ADAF37BA3038C0950A5D3613377AFFFEAFD269176DF2183F8FC7C083875F10470907937B076A6E789B0E97A8DF7F3B2552694AD5FFEEA1DED7F25D49FD398EE364050FB28585415E75ADA9287C8E22D4AE2A51B3661434B16C20ACC84D3B47A649675FE827F84554CEF5019E625A9149C048EE9ECD01F8117BC8BEE2021AF6380DFAD18AA50765F790063735872C767BF85DA227C277FBC8AE2E8B72A01BE2E107A27975ECD9A6C639B01B4E70A05D1297E1BBCB5012B2E24CD356 X-C1DE0DAB: 0D63561A33F958A572FDE94FBF9039D75002B1117B3ED696559EA19C6C33F12414DB8790748E3E77823CB91A9FED034534781492E4B8EEAD86106675DE625196C79554A2A72441328621D336A7BC284946AD531847A6065A535571D14F44ED41 X-C8649E89: 1C3962B70DF3F0AD73CAD6646DEDE1918E10F71CB4DF9F9677DD89D51EBB774225B6776AC983F447FC0B9F89525902EE6F57B2FD27647F25E66C117BDB76D6590E6E9B87AA2E99D87C3B8C1FAB20A32AFDFDE4FAF52DCE224947491B58CDFDD42E1487B7088F849EB8341EE9D5BE9A0A635611191C774006248AF0251BFDC22DD3A037FB823A0A95DABE3362BFED2FD64C41F94D744909CE8FFD5CA72B28909428BE7793689043A537E69C174A41D00C X-D57D3AED: 3ZO7eAau8CL7WIMRKs4sN3D3tLDjz0dLbV79QFUyzQ2Ujvy7cMT6pYYqY16iZVKkSc3dCLJ7zSJH7+u4VD18S7Vl4ZUrpaVfd2+vE6kuoey4m4VkSEu53w8ahmwBjZKM/YPHZyZHvz5uv+WouB9+ObcCpyrx6l7KImUglyhkEat/+ysWwi0gdhEs0JGjl6ggRWTy1haxBpVdbIX1nthFXMZebaIdHP2ghjoIc/363UZI6Kf1ptIMVYrk7BQKFwEt7pUaW8IEKm+AxEDwNz/zOQ== X-Mailru-Sender: 583F1D7ACE8F49BDC25C0C59A06B2F96466D2EEECE6E5FDAB951B70A5BD4BD8E417ACBA76F8F435512F9F109D7E18B13981BBF36307557118FCA44E9AC9C8EC2EEE2A91DED5447003DDE9B364B0DF289AE208404248635DF X-Mras: Ok X-Spam-Status: No, score=-10.3 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, FREEMAIL_FROM, GIT_PATCH_0, SPF_HELO_PASS, SPF_PASS, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org From: Artem Proskurnev This is the follow-up to the LD_DEBUG=loadlock diagnostic ("elf: Add LD_DEBUG=loadlock to trace dl_load_lock acquisitions"): it extends coverage to the dlclose destructor path, complementing the dlopen constructor path instrumented by the earlier change. Together the two cover both halves of the BZ #15686 deadlock class -- a destructor that (directly or via a spawned thread) re-enters the loader and blocks on dl_load_lock is now as easy to localise as the constructor case. The DL_DEBUG_LOADLOCK mask bit and the _dl_debug_loadlock helper established by the first commit are reused unchanged. This commit adds the per-file inlined trace_load_lock helper to elf/dl-close.c (identical to the one in dl-open.c) and calls it at every dl_load_lock acquire/release site reachable from _dl_close: * _dl_close entry acquire and the nodelete / not-open / done release paths. * The BZ #15686 release/reacquire around _dl_call_fini in _dl_close_worker (the direct mirror of dl_open_worker's release around the constructor), which is gated on the same glibc.rtld.strict_init_order tunable. As on the constructor side, the backtrace is a manual frame-pointer walk with no symbol resolution, so the trace can be emitted while dl_load_lock is held without re-entering the loader; raw addresses are resolved offline with addr2line. The exit-time finalizer path in elf/dl-fini.c also takes dl_load_lock, but that path is outside the BZ #15686 dlclose-destructor deadlock class and is left uninstrumented for now. No behaviour changes; the option is purely diagnostic. Signed-off-by: Artem Proskurnev --- elf/dl-close.c | 25 +++++++++++++++++++++++-- elf/tst-debug-loadlock.c | 14 +++++++++----- manual/dynlink.texi | 17 +++++++++-------- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/elf/dl-close.c b/elf/dl-close.c index 8e0b504a52..84e655fd7c 100644 --- a/elf/dl-close.c +++ b/elf/dl-close.c @@ -37,6 +37,17 @@ #include +/* When LD_DEBUG=loadlock is active, log a dl_load_lock acquire/release site + together with a raw-address backtrace (see _dl_debug_loadlock). Inlined so + the hot path costs only a single mask test when the flag is off. */ +static inline void +trace_load_lock (const char *action, const char *site) +{ + if (__glibc_unlikely (GLRO (dl_debug_mask) & DL_DEBUG_LOADLOCK)) + _dl_debug_loadlock (action, site); +} + + /* Special l_idx value used to indicate which objects remain loaded. */ #define IDX_STILL_USED -1 @@ -317,12 +328,18 @@ _dl_close_worker (struct link_map *map, bool force) == 0); if (release_lock_for_fini) - __rtld_lock_unlock_recursive (GL (dl_load_lock)); + { + __rtld_lock_unlock_recursive (GL (dl_load_lock)); + trace_load_lock ("release", "_dl_close_worker(for-fini)"); + } _dl_catch_exception (NULL, _dl_call_fini, imap); if (release_lock_for_fini) - __rtld_lock_lock_recursive (GL (dl_load_lock)); + { + __rtld_lock_lock_recursive (GL (dl_load_lock)); + trace_load_lock ("acquire", "_dl_close_worker(for-fini)"); + } } #ifdef SHARED @@ -820,6 +837,7 @@ _dl_close (void *_map) /* We must take the lock to examine the contents of map and avoid concurrent dlopens. */ __rtld_lock_lock_recursive (GL(dl_load_lock)); + trace_load_lock ("acquire", "_dl_close"); /* At this point we are guaranteed nobody else is touching the list of loaded maps, but a concurrent dlclose might have freed our map @@ -830,6 +848,7 @@ _dl_close (void *_map) { /* Nope. Do nothing. */ __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "_dl_close(nodelete)"); return; } @@ -846,10 +865,12 @@ _dl_close (void *_map) if (__builtin_expect (map->l_direct_opencount, 1) == 0) { __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "_dl_close(not-open)"); _dl_signal_error (0, map->l_name, NULL, N_("shared object not open")); } _dl_close_worker (map, false); __rtld_lock_unlock_recursive (GL(dl_load_lock)); + trace_load_lock ("release", "_dl_close(done)"); } diff --git a/elf/tst-debug-loadlock.c b/elf/tst-debug-loadlock.c index 52623c097e..2ff72b4886 100644 --- a/elf/tst-debug-loadlock.c +++ b/elf/tst-debug-loadlock.c @@ -1,14 +1,15 @@ /* Test for LD_DEBUG=loadlock. - Verifies that dl_load_lock acquisitions/releases on the dlopen constructor - path are logged with a backtrace when LD_DEBUG=loadlock is active, and that - "loadlock" appears in LD_DEBUG=help output. + Verifies that dl_load_lock acquisitions/releases around ELF constructor and + destructor execution (on the dlopen and dlclose paths) are logged with a + backtrace when LD_DEBUG=loadlock is active, and that "loadlock" appears in + LD_DEBUG=help output. The dl_debug_mask is set by rtld only at process startup, so both checks re-exec this binary as a child (under the freshly built ld.so, via $(host-test-program-cmd) passed in tst-debug-loadlock-ARGS) with LD_DEBUG set in the child environment, and capture the child's std streams. The trace lines themselves are emitted by the dynamic linker (elf/dl-debug.c, - elf/dl-open.c). + elf/dl-open.c, elf/dl-close.c). Copyright (C) 2026 Free Software Foundation, Inc. This file is part of the GNU C Library. @@ -93,7 +94,8 @@ do_test (int argc, char *argv[]) } /* Check 2: a real dlopen with LD_DEBUG=loadlock emits the dl_load_lock - acquire trace for _dl_open and the BZ 15686 release-for-ctor site. + acquire trace for both _dl_open (constructor path) and _dl_close + (destructor path), plus the BZ 15686 release-for-ctor/for-fini sites. Trace goes to stderr (dl_debug_fd defaults to STDERR_FILENO). */ setenv ("LD_DEBUG", "loadlock", 1); { @@ -109,6 +111,8 @@ do_test (int argc, char *argv[]) { "dl_load_lock acquire at _dl_open", "dl_open_worker(for-ctor)", + "dl_load_lock acquire at _dl_close", + "dl_close_worker(for-fini)", }; for (int k = 0; k < (int) array_length (needles); k++) if (strstr (p.err.buffer, needles[k]) == NULL) diff --git a/manual/dynlink.texi b/manual/dynlink.texi index 5569830f23..43d84a7417 100644 --- a/manual/dynlink.texi +++ b/manual/dynlink.texi @@ -408,15 +408,16 @@ Display relocation statistics. Determined unused DSOs. @item loadlock -Log every acquisition and release of @code{dl_load_lock} on the @code{dlopen} -constructor path, each followed by a raw return-address backtrace. Use this to -diagnose deadlocks of the shape described in +Log every acquisition and release of @code{dl_load_lock} around ELF constructor +and destructor execution on the @code{dlopen} and @code{dlclose} paths, each +followed by a raw return-address backtrace. Use this to diagnose deadlocks of +the shape described in @uref{https://sourceware.org/bugzilla/show_bug.cgi?id=15686, BZ 15686}, where -code running inside an ELF constructor (or a thread it spawns) re-enters the -dynamic linker and blocks on @code{dl_load_lock}. The backtrace lists raw code -addresses; resolve them offline with @command{addr2line}. Frame pointers must -be present in the code being traced (the default for the dynamic linker on most -targets). +code running inside an ELF constructor or destructor (or a thread it spawns) +re-enters the dynamic linker and blocks on @code{dl_load_lock}. The backtrace +lists raw code addresses; resolve them offline with @command{addr2line}. Frame +pointers must be present in the code being traced (the default for the dynamic +linker on most targets). @item help Display a help message with all available options and exit.