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

Artem Proskurnev <[email protected]> Sat, 1 Aug 2026 11:17:54 +0300
Newsgroups gmane.comp.lib.glibc.alpha
Message-ID <[email protected]>
Unfortunately, I mistakenly sent the patch chain in the previous email.

I have fixed it:

https://inbox.sourceware.org/libc-alpha/[email protected]/

https://patchwork.sourceware.org/project/glibc/patch/[email protected]/

31.07.2026 16:30, Artem Proskurnev:
> Greetings, Carlos!
>
> A complete solution divided into parts:
>
> https://inbox.sourceware.org/libc-alpha/[email protected]/ 
>
>
> https://inbox.sourceware.org/libc-alpha/[email protected]/ 
>
>
> https://inbox.sourceware.org/libc-alpha/[email protected]/ 
>
>
> https://inbox.sourceware.org/libc-alpha/[email protected]/ 
>
>
> 21.07.2026 16:44, Carlos O'Donell:
>> On 7/21/26 6:47 AM, Artem Proskurnev wrote:
>>> 21.07.2026 00:43, Carlos O'Donell:
>>>> (1) Solving the narrower problem as a short-term solution.
>>>>
>>>> Adhemerval's solution seems like a good immediate fix for the problem.
>>>>
>>>> Does it fix the issue?
>>>>
>>>> As a project lead I would like to see Bug 15686 fixed, but if it takes
>>>> us time to get to that solution, we also want to resolve the problem
>>>> that you're seeing in a timely fashion and are willing to accept a 
>>>> short-term
>>>> solutions to help you out.
>>>>
>>>> (2) Solving the architectural problem may take longer or require 
>>>> more testing.
>>>>
>>>> Large changes like those which are being proposed here will take 
>>>> time to review
>>>> and check against existing application expectations.
>>>>
>>>> The implementation is constrained by more than just what the standards
>>>> say, or the glibc manual says, it is constrained by expectations that
>>>> applications may have created.
>>>>
>>>> It should not be this way, but it is, and we attempt to consider such
>>>> constraints when making changes because we don't want to knowingly 
>>>> break
>>>> such applications unless there is a meaningful and valuable gain.
>>>>
>>>> We might be able to run with this change downstream in Fedora and use
>>>> our build infrastructure to do A/B build testing of the 
>>>> distribution to see if
>>>> something comes out of the larger scale testing.
>>>>
>>>> I'm excited to see the results of ROSA Labs running with these 
>>>> changes in
>>>> production and reporting back the results.
>>>>
>>>> (3) POSIX and glibc say nothing about this today --- making this a 
>>>> hard problem.
>>>>
>>>> The problem *is* that we say nothing about what is and is not 
>>>> allowed and have
>>>> permitted the implementation to grow to the point that users expect 
>>>> a completely
>>>> flexible asynchronous processing of dependencies across multiple 
>>>> threads
>>>> allowing calls to all library functionality.
>>>>
>>>> For example, some users expect a malloc interposer can call dlopen, 
>>>> but this
>>>> clearly has to have some constraints. If dlopen needs malloc then 
>>>> it doesn't
>>>> work as the solution has a dependency cycle (discussed previously 
>>>> on list as a
>>>> new form of safety e.g. "synchronous reetrancy").
>>>>
>>>> Since we have never said anything before about this, as Adhemerval 
>>>> notes, this
>>>> area is going to possibly cause problems with existing 
>>>> applications, and for
>>>> which this patch provides a tunable.
>>>>
>>>> It would be better if instead of using a tunable we spent more time 
>>>> evaluating
>>>> the problem space and a solution, hence I think Adhemerval's 
>>>> suggestion to use
>>>> an intermediate solution. This intermediate solution is not a final 
>>>> solution,
>>>> but an option to resolve the short-term problem while we take more 
>>>> time to
>>>> evaluate a full solution.
>>>>
>>>> In truth we say a few things about this in the glibc manual in the 
>>>> Dynamic Linker
>>>> chapter, but only to warn against having any expectations :-)
>>>>
>>>> (4) A full solution requires understanding what we're asking --- 
>>>> and possibly tooling.
>>>>
>>>> Are we asking that all foreign function callbacks can call into the 
>>>> library safely?
>>>>
>>>> Are we only asking that all foreign function callbacks for library 
>>>> initializers and
>>>> finalizers be able to call back into the library safely?
>>>>
>>>> Constructors and destructors are foreign function calls made during 
>>>> library
>>>> initialization and finalization, and as such we have never 
>>>> discussed what is and
>>>> isn't permitted to be called in those functions.
>>>>
>>>> These foreign functions are not occurring in the normal context of 
>>>> the application
>>>> runtime, nor do we provide ways for them to know what *other* 
>>>> non-libc functionality
>>>> can be safely accessed and if it will ever change (based on load 
>>>> order).
>>>>
>>>> Is there any way we can add tooling or _dl_debug_printf options to 
>>>> detect problems?
>>>>
>>>> Example: lari: 
>>>> https://docs.oracle.com/cd/E88353_01/html/E37839/lari-1.html
>>>>
>>>> Should we add the option to randomly order dependencies whose 
>>>> relative order shouldn't
>>>> matter? To shake out problems in development?
>>>>
>>>> I think a solution to this problem is going to take time, 
>>>> particularly to evaluate
>>>> the effect on existing applications and runtimes.
>>>>
>>>> Thoughts on (1), (2), (3) or (4)?
>>>>
>>> Thanks for the thoughtful response.  Replying to your four points in
>>> the order that I think moves the discussion forward fastest: (1)
>>> first because it is the easiest to settle factually, (3) next
>>> because it is the most actionable, then (2) and (4).
>>>
>>> == (1) Does Adhemerval's fix solve the reported issue?
>>>
>>> No.  I A/B tested it before sending v3, and the result is in my reply
>>> to Adhemerval earlier in this thread.  Repeating the headline for
>>> directness:
>>>
>>> azanella/bz15686 (9039a9cb0f)  DEADLOCK  exit 124
>>> v3 (release-dl-load-lock...)   PASS      exit 0, PNG 256x256
>>>
>>> The blocking site is getgrouplist -> __nss_lookup_function ->
>>> __nss_module_load -> __libc_dlopen_mode -> _dl_open -> dl_load_lock,
>>> not __cxa_thread_atexit_impl.  Adhemerval's patch removes one
>>> acquisition site but leaves the rest of the dl_load_lock surface
>>> intact, and the glycin reproducer fires through one of the sites
>>> that is left intact.
>>>
>>> I would still like to see Adhemerval's patch merged independently.
>>> It is a strict improvement on the path it touches, the cleanup of
>>> the per-thread dso_symbol_cache/lm_cache is real, and it does not
>>> conflict with v3.  My position is "both, not either-or".
>>
>> Thanks for confirming that.
>>
>> So the short-term fix doesn't solve your problem.
>>
>> That makes the issue more pressing to review and discuss.
>>
>>> == (3) POSIX is silent, tunable is not ideal
>>>
>>> I agree the tunable is not ideal, and your concern is well-founded:
>>> a runtime knob becomes permanent API surface the moment it ships,
>>> and we should not enshrine a non-standard invariant just to hedge
>>> against an unknown regression.  Two options, in order of my
>>> preference:
>>>
>>> (a) Keep the tunable, but ship it undocumented and explicitly marked
>>> deprecated from day one.  The manual would say nothing about it;
>>> the tunables list would carry a one-line entry flagged "deprecated,
>>> escape hatch for unforeseen regressions, scheduled for removal".
>>> If no regression surfaces within one release cycle, it gets removed.
>>> If one does, we have a concrete report to discuss rather than a
>>> hypothetical.  This keeps the escape hatch while making it clear
>>> that the v3 default is the contract, not the fallback.
>>>
>>> (b) Remove the tunable entirely for v4.  Default behaviour is the v3
>>> behaviour, period.  If a regression surfaces, we handle it the way
>>> we handle any other regression -- a revert or a targeted fix in the
>>> next release, not a runtime knob.  This is the cleanest position
>>> but also the riskiest, because it removes the safety net for
>>> downstreams that may not have a way to test before shipping.
>>>
>>> I prefer (a).  The cost of carrying an undocumented flag for one
>>> release cycle is small, and the benefit is that if a regression
>>> does surface we have a clean way to write "set this flag, verify
>>> the regression goes away, then we know what we are fixing". Without
>>> it, we are stuck arguing from backtraces.
>>>
>>> That said, if the consensus in this thread is (b), I will do that.
>>> I can send a v4 with the tunable removed and the XFAIL test removed
>>> or reworded accordingly.  Just say the word.
>>
>> Since you ask for guidance further down:
>>
>> * We should always document tunables.
>>
>> * We should not ship undocumented tunables.
>>
>> * Tunables are not ABI. We should remind downstreams.
>>
>> * Tunables should not change standards conforming behaviour (they 
>> don't in this case).
>>
>> I think it's OK to keep the tunable.
>>
>> However, the fact that we need a tunable means we should be reviewing
>> the solution space more carefully and thinking about the impact.
>>
>> For example how will developers know the crashing application can be
>> fixed by using the tunable? This is why I asked the open question about
>> _dl_debug_printf, tooling, and other means of observability. If we can
>> add observability via LD_DEBUG=all or another means that supports the
>> analysis then that would be beneficial.
>>
>> Lastly, we should consider that glibc uptake in downstream takes
>> almost 1-2 years, and as such the tunable would have to remain in place
>> probably for 4 releases before we see all the reports, resolve them
>> and then remove the tunable.
>>
>> All of this is normal for glibc, and I just wanted to set the 
>> expectation.
>>
>>> == (2) Architectural fix needs time, Fedora A/B testing
>>>
>>> I would welcome Fedora A/B testing, and I will help however I can.
>>> The minimal reproducer attached to my earlier reply runs in under a
>>> second on any system with libnss_systemd.so.2 active and glycin
>>> installed.  If Fedora wants a heavier workload, the original report
>>> is ROSA bug 21031 -- Codeblocks startup -- and that is a real
>>> user-visible scenario that a distro-scale build test can exercise.
>>>
>>> For the ROSA side: Mikhail Novosyolov has confirmed earlier in this
>>> thread that v3 fixes ROSA bug 21031 in our downstream testing. Our
>>> plan is to ship v3 (with Adhemerval's lock-free
>>> __cxa_thread_atexit_impl rebased on top) once the upstream design
>>> is settled.
>>
>> Great. I'd like to hear how the deployment goes and if you see any
>> issues.
>>
>>> On the application-expectations concern: the risk surface of v3 is
>>> specifically an application that depends on the constructor of DSO A
>>> always running before the constructor of DSO B when A and B are
>>> loaded by independent dlopen calls in independent threads. That is
>>> the only ordering property v3 changes.  Constructors within a single
>>> dlopen still run in dependency order; constructors of the executable
>>> and its startup-time dependencies still run before main.  If Fedora
>>> A/B testing turns up a regression, it is almost certainly of this
>>> shape, and the discussion can focus on whether the application was
>>> relying on undocumented behaviour, documented behaviour that we
>>> missed, or whether v3 has a bug.
>>
>> Agreed.
>>
>>> == (4) What are we asking for, and tooling
>>>
>>> This is the question I have spent the most time thinking about.
>>> Laying out what I believe v3 is and is not claiming, after
>>> re-reading the patch.
>>>
>>> v3 makes one specific claim:
>>>
>>>    Constructors -- foreign function calls made during library
>>>    initialisation -- may call any libc and dynamic-loader function
>>>    that is safe to call from a regular non-signal-handler thread of
>>>    the same process.
>>
>> OK.
>>
>>> That claim implies:
>>>
>>>    - A constructor may call dlsym, _dl_addr, _dl_find_dso_for_object,
>>>      dlopen of a different DSO, dlclose of a different DSO, and the
>>>      NSS-backed libc functions (getpwnam, getgrnam, getaddrinfo,
>>>      getgrouplist, ...).  dl_iterate_phdr was already safe via
>>>      dl_load_write_lock after BZ 28357 and is unaffected by v3.
>>>    - A constructor may spawn threads, and those threads may call any
>>>      of the above.
>>>    - A constructor may call malloc, and a malloc interposer that is
>>>      itself loaded via dlopen may in turn call any of the above from
>>>      its own constructors.
>>>
>>> The claim does NOT cover:
>>>
>>>    - Destructors.  v3 deliberately scopes to the constructor path,
>>>      which is where the actual reproducer fires.  Destructors run
>>>      with dl_load_lock held inside _dl_close_worker just as
>>>      constructors used to inside dl_open_worker, so the same
>>>      theoretical deadlock shape exists for a destructor that calls
>>>      dlopen, spawns a thread that hits NSS, etc.  In practice this
>>>      does not seem to happen: destructors usually free resources
>>>      rather than load new code or spawn threads, and the one
>>>      recursive case that does come up (a destructor calling dlclose)
>>>      is already handled separately via dl_close_state in dl-close.c.
>>>      Extending v3 to also release dl_load_lock around _dl_call_fini
>>>      would require the same state machine on the close path without
>>>      a real-world reproducer justifying it.  If a destructor
>>>      deadlock of this shape surfaces, it should be a follow-up
>>>      patch with its own reproducer; v3 does not pretend to fix it.
>>>
>>>    - Synchronous loader-internal reentrancy -- the case you describe
>>>      in (4) where the loader's own malloc calls during dlopen
>>>      processing (outside the ctor window) route through an
>>>      interposer that itself calls dlopen.  v3 does not address this;
>>>      it remains the "synchronous reentrancy" problem.  Note that
>>>      malloc calls from inside a constructor are a different matter
>>>      and ARE covered, because the constructor runs with the lock
>>>      released.
>>
>> Good. This is a *better* more narrowly scoped definition of the problem.
>>
>>> Two special cases that v3 does handle, mentioned because they are
>>> easy to get wrong when reading the patch:
>>>
>>>    - Recursive dlopen of the same DSO from within its own
>>>      constructor: detected via l_init_owner (same-TID check in
>>>      call_init), returns immediately without waiting for itself.
>>>      This is the l_init_owner field, not l_init_pending.
>>
>> This is correct (only for same-TID case).
>>
>>>    - Concurrent dlopen of an already-being-initialised DSO from a
>>>      different thread: the late caller takes the already-loaded
>>>      early-return path, observes l_init_pending or l_init_called
>>>      set, and waits on l_init_once until the constructor finishes.
>>>      This preserves the property that dlopen does not return before
>>>      the constructor has run.
>>
>> This is correct.
>>
>>> == Fundamental limitation: ctor-spawned threads are not originally
>>>     independent
>>>
>>> Beyond the NOT-covered list above, there is one deadlock shape
>>> that no same-design patch can lift, and v3 does not claim to.
>>>
>>> A thread spawned from inside a DSO's constructor is not originally
>>> independent -- its existence begins inside the ctor, and any work
>>> it does that transitively depends on the ctor's DSO must wait for
>>> the ctor to complete.  If a spawned worker calls dlopen on a DSO
>>> that (directly or transitively) depends on the ctor's DSO, the
>>> worker correctly blocks on l_init_once: unblocking it would expose
>>> a half-constructed DSO to the new load.  That is DSO dependency
>>> ordering, not lock contention, and the serialization is required
>>> for correctness.
>>
>> Correct. This would be a compositional defect in the application.
>> Tooling to detect this would be beneficial but not required.
>>
>>> What v3 does lift is the orthogonal case: the spawned worker (or
>>> any other thread) doing dlopen of a DSO that has no dependency on
>>> the ctor's DSO.  The glycin reproducer is exactly this shape --
>>> the worker's getgrouplist call routes through NSS module loading,
>>> which dlopens libnss_systemd.so.2, which does not depend on the
>>> ctor's DSO.  v3 lets the worker proceed; pre-v3 it deadlocked on
>>> dl_load_lock.
>>
>> Agreed.
>>
>>> So v3's actual claim, stated precisely, is: originally independent
>>> threads can run independent dlopens concurrently, and a ctor (or a
>>> thread it spawns) can do dlopen of an independent DSO without
>>> serialising against the in-progress ctor.  It cannot untie work
>>> that transitively depends on the ctor's DSO -- but no patch in
>>> this design space can, because that serialization is what makes
>>> concurrent dlopen correct.
>>
>> Agreed.
>>
>>> That answers your question "all callbacks, or just init/fini
>>> callbacks?".  v3's claim is precisely about init callbacks
>>> (constructors).  Fini callbacks (destructors) are not yet covered
>>> by v3; see above.  Other foreign function callbacks (signal
>>> handlers, malloc hooks, pthread destructors, atfork handlers) are
>>> not in scope.
>>
>> Good.
>>
>>> I think stating this scope explicitly is more useful than leaving
>>> it implicit.  If the project's position is that the scope should be
>>> narrower (e.g. exclude some of the bullets above), or that v3
>>> should not ship without also covering one of the NOT-covered
>>> cases, that is a conversation worth having now rather than after
>>> v3 ships.
>>
>> No, the scope as defined is sufficiently narrow.
>>
>> However, I would like to discuss destructors too, and if anything
>> needs to be done there.
>>
>>> On tooling:
>>>
>>> - Random-order dependency init is orthogonal to v3 and would be a
>>> useful shakedown tool.  I would support it as a separate patch
>>> series, behind a tunable with the same "undocumented, deprecated,
>>> removed after one cycle" caveat if it turns up no real-world
>>> breakage.
>>
>> Agreed.
>>
>>> - An _dl_debug_printf option that logs every dl_load_lock
>>> acquisition site with a backtrace would help diagnose residual
>>> deadlocks of this shape.  I do not have a patch for this but I am
>>> happy to write one if there is interest.
>>
>> I think we should add this as a 2/2 patch, which improves the
>> observability.
>>
>> My question to you is: How does a system admin, or developer know
>> to flip the tunable?
>>
>>> - lari-style tooling for "which DSO registered which constructor"
>>> is useful but I think out of scope for this thread.
>>
>> Agreed. I just showed it as an example of observability improvements.
>>
>> I want us to think of "solutions" not just "patches."
>>
>> That is to say that solving this problem might require more than
>> just patching.
>>
>>> Alexander Pevzner's reply on the same question is worth reading in
>>> this light -- he makes the architectural argument independently
>>> and reaches the same place: per-case patches do not fix the
>>> underlying invariant, they just move manifestations around.
>>
>> Right.
>>
>>> == Next steps
>>>
>>> Concretely, what would help from the list:
>>>
>>> - A steer on (3): undocumented-and-deprecated vs. removed entirely.
>>> Once I have that I can send v4.  If the answer takes time, that is
>>> fine -- I would rather get it right than get it fast.
>>
>> I think we should keep the tunable, it should be documented, and the
>> documentation should clearly state we intend to remove this when the
>> ecosystem has been shown to work with the new implemetnation.
>>
>>> - A steer on (4): does the scope claim above match what the project
>>> wants to guarantee, or is it too strong?  If too strong, which
>>> bullet should I narrow?
>>
>> The scope makes sense, but I'm also concerned about destructors since
>> both see this kind of problem. What prevents us from providing the same
>> invariant in destructors?
>>
>>
>>> Thanks,
>>> Artem
>>>
>>