Skip to content

Dependency load order in StatsFuns 2.x exhausts static TLS on old glibc (libgomp fails to load) #218

Closed

Description

@mchitre

Upgrading StatsFuns 1.5.2 → 2.2.0 broke our deployment on an embedded aarch64 platform running glibc 2.27 (Ubuntu 18.04), with:

ERROR: LoadError: InitError: could not load library "libgomp.so.1"
.../lib/julia/libgomp.so.1: cannot allocate memory in static TLS block
during initialization of module CompilerSupportLibraries_jll

This is not a binary regression — the resolved JLL versions (Rmath_jll 0.5.1+0, OpenSpecFun_jll, CompilerSupportLibraries_jll 1.1.1+0, etc.) are identical between the working and failing environments. It's purely a change in the order in which StatsFuns loads its dependencies:

  • With StatsFuns 1.5.x, SpecialFunctions (and hence CompilerSupportLibraries_jll / libgomp) is loaded before Rmath, and everything works.
  • With StatsFuns 2.2.0, Rmath is loaded first. libRmath-julia.so carries an 856-byte TLS segment which, on pre-2.31 glibc (where the static TLS surplus is small, ~1664 bytes, and not tunable), consumes the surplus. When CompilerSupportLibraries_jll then dlopens libgomp — which requires static TLS (initial-exec model) — the allocation fails.

Minimal demonstration on the affected platform (Julia 1.11.3, glibc 2.27):

using CompilerSupportLibraries_jll, Rmath   # works
using Rmath, CompilerSupportLibraries_jll   # fails: libgomp cannot allocate static TLS

and consequently using StatsFuns fails at 2.2.0 but works at 1.5.2 with the same artifact versions.

Would you be open to reordering the using statements in StatsFuns.jl so that SpecialFunctions (or CompilerSupportLibraries_jll) is loaded before Rmath? It's a zero-cost change on modern systems and restores compatibility with older glibc, where libraries needing static TLS must be loaded before the surplus is consumed. We're currently pinning StatsFuns to ~1.5 as a workaround.

Activity

  1. devmotion commented on Jul 27, 2026

    @devmotion
    Member

    Are you using the Julia binaries from julialang.org? Or a Ubuntu package?

  2. mchitre commented on Jul 27, 2026

    @mchitre
    Author

    From julialang.org. And its only a problem on Ubuntu 18, not on newer versions.

  3. devmotion commented on Jul 27, 2026

    @devmotion
    Member

    To me this sounds like an upstream problem with libRmath (it seems?) and not something that can be reliably fixed in StatsFuns.

  4. mchitre commented on Jul 27, 2026

    @mchitre
    Author

    Fair enough, but it is still a problem for users of this library, since the dependency on libRMath comes from here.

  5. devmotion commented on Jul 27, 2026

    @devmotion
    Member

    This should be fixed, I just think it should be fixed in one of the upstream dependencies as relying on the load order in StatsFuns is brittle and seemingly users can provoke the issue without StatsFuns by loading these packages manually.

  6. mchitre commented on Jul 27, 2026

    @mchitre
    Author
  7. mchitre commented on Jul 27, 2026

    @mchitre
    Author

    Technically this issue was bumped upstream and not fixed, since the issue still exists for users of this library on older Linux systems.

    The issue should have been closed as "not planned" rather than "completed", which suggests it's fixed and is misleading. I leave this comment for completeness, and won't follow up on this any further.

    Nevertheless, thanks for your quick responses @devmotion, I appreciate it.

  8. nalimilan commented on Jul 29, 2026

    @nalimilan
    Member

    This appears to be a bug in older glibc which is hard or impossible to fix in Rmath-julia (see JuliaStats/Rmath-julia#56 (comment)). If changing the order in which we load dependencies in StatsFuns works around the problem, it seems work doing IMO.

  9. devmotion commented on Jul 29, 2026

    @devmotion
    Member

    The question is: Does the order of import statements actually guarantee any specific load order? Or is the compiler free to rearrange load order of packages that do not depend on each other? The general approach of this possible workaround seems still too brittle for me to serve as an actual fix for the problem. This is an upstream issue, and without an upstream fix it will still be broken outside of StatsFuns, even if we are able to do something here. Moreover, AFAIU if a user would use using Rmath, StatsFuns she would run into the same problem, so it's not even a fix for people using only Rmath and StatsFuns.

  10. nalimilan commented on Jul 29, 2026

    @nalimilan
    Member

    There's no way to fix this cleanly AFAICT, so any workaround would be just that. Given that it's very easy to do and it doesn't have any drawbacks it seems worth doing. Very few (if any) people use Rmath directly.

  11. devmotion commented on Jul 29, 2026

    @devmotion
    Member

    So far nobody has confirmed that this is a stable workaround at all.

    Moreover, I don't understand the underlying problem: why does the load order matter if both packages need to allocate TLS segments? Shouldn't the static surplus be exceeded regardless? And as the pytorch issue implies this can occur with basically any library that exceeds the surplus, so loading other packages with TLS-dependent binaries could provoke the same issue? If that's the case, isn't the proper answer just to tell people to use a newer glibc (which AFAIU fixes these issues at the core?)?

  12. devmotion commented on Jul 29, 2026

    @devmotion
    Member

    I assume people rather use Rmath than Rmath_jll. If the combination of CompilerSupportLibrabries_jll and Rmath_jll is what usually triggers this problem in the Julia ecosystem, another option would be to add a import CompilerSupportLibraries_jll to Rmath.jl before loading Rmath_jll, to avoid this problem regardless of whether Rmath is used with or without StatsFuns.

  13. devmotion commented on Jul 29, 2026

    @devmotion
    Member

    I looked into this in a bit more detail, mainly to answer my own question above about why the load order matters at all. I used Claude for brainstorming and for working through the details.

    Why the order matters: the two libraries are not symmetric. libRmath-julia.so's TLS is global-dynamic, so it does not have to live in the static surplus at all, glibc < 2.32 just puts it there opportunistically because it fits, as an access-time optimization. libgomp on the other hand uses initial-exec, which can only be served from the surplus. So whoever dlopens first wins, and Rmath winning wastes a resource that only libgomp actually needs. As a side note, this also means that rebuilding with -ftls-model=global-dynamic would not help, in case someone suggests it: pre-2.32 glibc allocates from the surplus for any dlopened module with a PT_TLS segment regardless of the TLS model, only the size of the segment matters.

    Where the change came from: e0a0cab (#188). Removing the RFunctions submodule replaced include("rmath.jl"), which sat at the bottom of the module body after using SpecialFunctions, with a using Rmath: Rmath at the very top. So the reordering was an accidental side effect of an unrelated refactoring, and the load order is currently an undocumented invariant that the next refactoring can break again just as silently.

    A few options that have not been mentioned yet:

    1. Rebuild Rmath_jll with dont_dlopen=true. JLLWrappers already supports this (@init_library_product skips the dlopen if dlopen_flags === nothing, and libRmath is still bound to the full path), so ccall((:qgamma, libRmath), ...) would resolve the library lazily on the first call instead of at using Rmath. That takes libRmath out of the load phase entirely, so unlike reordering the imports here it would also fix using Rmath, StatsFuns. I am not convinced this is worth it though: it only changes when the surplus is consumed, not whether it can be exhausted, so a first Rmath call before CSL is loaded would still fail. On top of that a missing or broken artifact would only error at the first call instead of at load time, and it presumably interacts with static compilation. Mentioning it mainly for completeness.

    2. Shrink the TLS segment upstream. Looking at patches/thread-local.patch, the 856 bytes are mostly rpois.c (pp[36] plus scalars, ~400 bytes) and the caches in rhyper.c, rbinom.c, rgamma.c and rbeta.c. Two things stand out there. First, p1, p2 and q1..q4 in toms708.c are initialized constants that are never written, they should be static const and not _Thread_local at all, that looks like a plain mistake in Improve thread-safety Rmath-julia#50. Second, all of these are just caches across repeated calls, so they could be moved behind a single _Thread_local pointer to a lazily allocated per-thread struct, which would bring the segment from 856 bytes down to 8 while keeping the thread-safety of enable travis on 0.7 #50. The downside is that Rmath-julia is maintained as patches against upstream R, and a larger patch is more painful to rebase.

    3. Before any of this, it would be good if @mchitre could confirm the diagnosis, e.g. by running with LD_PRELOAD=<julia>/lib/julia/libgomp.so.1. Preloaded modules are part of the initial static TLS block, so libgomp would always win the race. This is also inherited by the precompilation worker processes, so it should be a usable workaround for the deployment in the meantime. On glibc >= 2.32, GLIBC_TUNABLES=glibc.rtld.optional_static_tls=4096 should have the same effect.

    4. If we reorder the imports here anyway, we should at least add a comment and a test that asserts that the SpecialFunctions (or an explicit CompilerSupportLibraries_jll) import comes before the Rmath import. Otherwise we will just lose the invariant again the next time someone touches the module body, exactly as in Remove Rmath sampling methods #188.

    5. Dropping the Rmath dependency here is already in progress independently of this issue (Native implementations for distrs #37, and Replace Rmath hypergeometric with pure Julia implementation #203, Implement beta/gamma invlogcdf and invlogccdf in pure Julia #204, Implement binomial, Poisson, and negative binomial inverse functions in pure Julia #205, Replace Rmath noncentral beta with pure Julia implementation #206 among others), so for StatsFuns users the problem will eventually disappear on its own. It does not help anyone who hits this outside of StatsFuns though, which is why I think 2 is the most immediately useful thing to pursue.

  14. nalimilan commented on Jul 29, 2026

    @nalimilan
    Member

    Thanks for the detailed investigation. Option 2 seems like the most robust solution, but it requires some work which would only be useful for older glibc versions. Option 4 is much cheaper. I guess it depends on whether @mchitre wants to devote some time to it or not.

  15. mchitre commented on Jul 30, 2026

    @mchitre
    Author

    My current problem is resolved by dropping StatsFuns as a dependency in our project. The dependency originally came from Distributions, and that was only used in a relatively small part of the code base that we refactored to drop the dependency.

    Having said that, if there is anything I can do to help resolve this for the community, I am happy to help.

    The LD_PRELOAD= solution did not work for us because it is not honored with setcap on the julia binary, and we require that.

  16. devmotion commented on Jul 30, 2026

    @devmotion
    Member

    I took a stab at it in JuliaStats/Rmath-julia#57 and JuliaStats/Rmath-julia#58 (based on the former).

  17. devmotion commented on Aug 3, 2026

    @devmotion
    Member

    Rmath_jll 0.5.2+0, built from JuliaStats/Rmath-julia#58, fixes this.

    The shipped artifacts have a 16-byte TLS segment instead of 856 (PT_TLS memsz, aarch64-linux-gnu; 12–16 bytes on the other Linux targets). libgomp needs 136 bytes of initial-exec TLS, so this frees up ~840 bytes of the 1664-byte pre-2.32 surplus.

    I checked in an ubuntu:18.04 aarch64 container (glibc 2.27, Julia 1.11.3 and 1.11.9, StatsFuns 2.2.1, CompilerSupportLibraries_jll 1.1.1+0) with only Rmath_jll differing: with 0.5.1+0 both using Rmath, CompilerSupportLibraries_jll and using StatsFuns fail with the reported cannot allocate memory in static TLS block while the reversed order works, with 0.5.2+0 everything loads. d/p/q values and sampler streams are byte-identical between the two versions.

    Rmath 0.9 allows Rmath_jll 0.4–0.5, so a fresh resolve picks 0.5.2+0 but an existing manifest needs an explicit ]up.

    Thanks @mchitre for the report. The load order here does not matter anymore, so closing.

  18. mchitre commented on Aug 4, 2026

    @mchitre
    Author

    Thanks all, for helping resolve!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions