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

Artem Proskurnev <[email protected]> Fri, 31 Jul 2026 16:30:00 +0300
Newsgroups gmane.comp.lib.glibc.alpha
Message-ID <[email protected]>
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
>>
>