Skip to content

rust-lld default on x86_64-unknown-linux-gnu produces binaries without RUNPATH on NixOS #162781

Description

@zackees

Since 1.90 (#140525) rustc links x86_64-unknown-linux-gnu binaries with the self-contained rust-lld. On NixOS this silently changes the output of any crate that links a system shared library: the binary no longer gets a RUNPATH, and it fails to start.

Background

NixOS has no /usr/lib. The cc on PATH is a nixpkgs wrapper whose companion ld wrapper appends -rpath <dir> for each -L directory in /nix/store that provides a linked -l library (ld-wrapper.sh). That is how every binary linked there finds its libraries at run time.

rust-lld is not reached through that ld wrapper, so no RUNPATH is written. The cc wrapper still passes -dynamic-linker for the Nix store glibc, so the binary is not a "foreign" one either, and NixOS's nix-ld fallback does not apply. The result links cleanly and dies at startup.

Reproduction

NixOS 26.05, rustup stable rustc 1.95.0 (59807616e 2026-04-14), LLVM 22.1.2, nixpkgs gcc-wrapper-15.2.0 as cc. Outside any nix-shell, with PKG_CONFIG_PATH pointing at the store's openssl/zlib/sqlite .pc directories:

[dependencies]
openssl-sys = "=0.9.117"
libz-sys = { version = "=1.1.29", default-features = false, features = ["libc"] }
libsqlite3-sys = "=0.38.2"
fn main() {
    unsafe {
        openssl_sys::init();
        let v = std::ffi::CStr::from_ptr(openssl_sys::OpenSSL_version(0));
        let z = std::ffi::CStr::from_ptr(libz_sys::zlibVersion());
        let s = std::ffi::CStr::from_ptr(libsqlite3_sys::sqlite3_libversion());
        println!("openssl={v:?} zlib={z:?} sqlite={s:?}");
    }
}
cargo build
readelf -p .comment target/debug/app | grep Linker   # Linker: LLD 22.1.2
readelf -d target/debug/app | grep RUNPATH           # (nothing)
ldd target/debug/app                                 # libssl.so.3 => not found, libcrypto.so.3 => not found, libsqlite3.so => not found
./target/debug/app                                   # error while loading shared libraries: libsqlite3.so ... (exit 127)

The same build through the same cc, opting out of the self-contained linker:

RUSTFLAGS="-Clinker-features=-lld -Clink-self-contained=-linker" cargo build
readelf -d target/debug/app | grep RUNPATH   # [/nix/store/…-sqlite-3.51.2/lib:/nix/store/…-openssl-3.6.3/lib:…]
./target/debug/app                           # openssl="OpenSSL 3.6.3 …" zlib="1.3.2" sqlite="3.51.2"
Linker RUNPATH ldd not found Run
rust-lld (default) none 3 exit 127
GNU ld via the nixpkgs wrapper store library dirs 0 exit 0

The same happens with clang-sys (libclang.so.21.1: cannot open shared object file), and with tools that inspect the result: a Python wheel backend that runs ldd to bundle OpenSSL failed because ldd reported it missing.

What would help

Any one of these:

  • Document the NixOS interaction next to the lld default and its opt-out, since the failure shows up at run time rather than link time.
  • When cc is a nixpkgs wrapper (e.g. it has a nix-support/ directory beside it), don't pick the self-contained linker, or warn that RUNPATH handling is lost.
  • Provide a hook for linker-wrapper rpath semantics when rust-lld replaces the system ld.

Workarounds (verified)

  • -Clinker-features=-lld -Clink-self-contained=-linker restores the wrapper's behaviour.
  • Put -rpath <dir> in the cc wrapper's cc-ldflags, which the cc wrapper passes to whatever linker it calls, rust-lld included. That is what this machine does now: zackees/nixos@91d74c1

Related: NixOS/nixpkgs#24744 (lld has no nixpkgs wrapper).

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Sep 14, 2026
  2. ds84182 commented on Sep 14, 2026

    @ds84182

    I think documenting this issue and its workaround is a good first step, but special-casing NixOS would make it impossible to compile a binary targeting another Linux distro on NixOS without the use of a sysroot and custom compilation flags. So NixOS would have to become a separate target to have NixOS-specific linking behavior by default.

  3. added
    O-NixOSOperating system: NixOS, https://nixos.org/
    A-linkageArea: linking into static, shared libraries and binaries
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Sep 15, 2026
  4. bjorn3 commented on Sep 15, 2026

    @bjorn3
    Member

    Is this with a rustup distributed rustc or a NixOS distributed one? I would expect the latter to contain patches to ensure everything is properly linked.

  5. zackees commented on Sep 15, 2026

    @zackees
    Author

    rustup distributed rust lld

  6. zackees commented on Sep 15, 2026

    @zackees
    Author

    I'm going to patch it into my own linker which has an AI optimized Wild fork

    zackees/reld#110

  7. maxdexh commented on Sep 15, 2026

    @maxdexh
    Member

    I'm using nixos, but I can't reproduce your issue. Do I read correctly that you're not in a nix shell, and have instead manually set up PKG_CONFIG_PATH to point to the relevant packages?

  8. zackees commented on Sep 15, 2026

    @zackees
    Author

    @maxdexh You read it correctly — I was outside any nix shell/nix develop, with PKG_CONFIG_PATH pointed manually at the store's .pc directories. But I don't think the shell vs. manual-PKG_CONFIG_PATH distinction is what separates us. Here's the full context (lifted from the session where I hit this and worked around it), plus a self-contained repro.

    What I was actually doing when I hit it

    I build a Rust CLI (bosn) that ships a Linux Python wheel through a custom maturin/PEP 517 backend. That backend runs ldd on the built CLI to copy the OpenSSL sidecars (_copy_linux_openssl). On NixOS, ldd prints:

    libssl.so.3 => not found
    libcrypto.so.3 => not found
    

    so the backend matched not as the source path and literally tried copy2("not", …). On the Ubuntu CI runner ldd resolves real paths, so that lane stays green. That is the real-world trigger — not a synthetic test.

    Minimal repro

    Environment: NixOS 26.05, rustup stable rustc 1.95.0 (59807616e 2026-04-14), LLVM 22.1.2, nixpkgs gcc-wrapper-15.2.0 as cc. Outside any dev shell, PKG_CONFIG_PATH pointing at the store's openssl/zlib/sqlite .pc dirs.

    Cargo.toml:

    [package]
    name = "devlibs-rusttest"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    openssl-sys = "=0.9.117"
    libz-sys = { version = "=1.1.29", default-features = false, features = ["libc"] }
    libsqlite3-sys = "=0.38.2"

    src/main.rs:

    fn main() {
        unsafe {
            openssl_sys::init();
            let v = std::ffi::CStr::from_ptr(openssl_sys::OpenSSL_version(0));
            let z = std::ffi::CStr::from_ptr(libz_sys::zlibVersion());
            let s = std::ffi::CStr::from_ptr(libsqlite3_sys::sqlite3_libversion());
            println!("openssl={v:?} zlib={z:?} sqlite={s:?}");
        }
    }

    Build and inspect:

    cargo build
    readelf -p .comment target/debug/devlibs-rusttest | grep Linker   # Linker: LLD 22.1.2
    readelf -d   target/debug/devlibs-rusttest | grep RUNPATH          # (nothing)
    ldd          target/debug/devlibs-rusttest                        # libssl.so.3 => not found, libcrypto.so.3 => not found, libsqlite3.so => not found
    ./target/debug/devlibs-rusttest                                   # error while loading shared libraries: libsqlite3.so (exit 127)

    Note the cargo build itself exits 0 — it links cleanly and only dies at run time. The NEEDED entries are all present (libsqlite3.so, libssl.so.3, libcrypto.so.3); there is just no RUNPATH.

    Opt out of the self-contained linker:

    RUSTFLAGS="-Clinker-features=-lld -Clink-self-contained=-linker" cargo build
    readelf -d target/debug/devlibs-rusttest | grep RUNPATH   # [/nix/store/…-sqlite-3.51.2/lib:/nix/store/…-openssl-3.6.3/lib:…]
    ./target/debug/devlibs-rusttest                           # openssl="OpenSSL 3.6.3 …" zlib="1.3.2" sqlite="3.51.2" (exit 0)

    Controlled A/B on the same machine — same crate, same nixpkgs cc, only the linker differs:

    RUSTFLAGS Linker RUNPATH ldd not-found run
    (default) rust-lld (Linker: LLD 22.1.2) none 3 exit 127
    -Clinker-features=-lld -Clink-self-contained=-linker GNU ld via cc store lib dirs 0 exit 0

    Why the shell/PKG_CONFIG_PATH distinction isn't the differentiator

    On NixOS the RUNPATH is written by the nixpkgs cc/ld wrapper, which takes the -L directories it sees and emits a matching -rpath. rustc does not read NIX_LDFLAGS itself. Whether the -L flags arrive via NIX_LDFLAGS (a shell's setup hooks) or via the build script's cargo:rustc-link-search (pkg-config), they only tell the linker where to find the .so at link time. The runtime path still has to come from the wrapper.

    With -Clink-self-contained=+linker (the 1.90 default), rustc invokes its bundled rust-lld directly and never calls cc, so the wrapper — and therefore the -rpath injection — never runs. That's why it links cleanly but dies at startup, in a shell or out of one. Plain C links through the same cc still get a correct RUNPATH, which is the contrast that isolated it.

    So if you can't reproduce, my guess is your rustc isn't actually on the self-contained-lld path. One line settles it:

    readelf -p .comment <bin> | grep -i linker

    LLD 22.x → self-contained lld and the bug should reproduce; GNU ld/cc → your toolchain still goes through the wrapper (e.g. nixpkgs rustc built with rust.lld = false, or an older channel), which is why it works for you. I'm on rustup stable 1.95.0.

    What I shipped as a workaround

    Rather than a global LD_LIBRARY_PATH (which would outrank every Nix program's own RUNPATH), I bake an explicit -rpath for a dev-library set into the cc wrapper's cc-ldflags:

    withDevLibraries = cc: cc.override (old: {
      extraBuildCommands = (old.extraBuildCommands or "") + ''
        echo "-idirafter ${devLibraryEnv}/include" >> $out/nix-support/cc-cflags
        echo "-L${devLibraryEnv}/lib -rpath ${devLibraryEnv}/lib" >> $out/nix-support/cc-ldflags
      '';
    });

    That's zackees/nixos@91d74c1 (PR zackees/nixos#19). It reaches any linker the wrapper invokes — rust-lld included — and post-fix the Rust binary still reads Linker: LLD 22.1.2 but now has a RUNPATH, ldd resolves 0 missing, and it runs. So -rpath in cc-ldflags is the wrapper-native fix that survives the self-contained linker.


    This comment was generated by clud, a custom agent made by Zach Vorhies who reviewed and authorized this post.

  9. maxdexh commented on Sep 15, 2026

    @maxdexh
    Member

    I tried your reproducer with nix-shell -p pkg-config openssl zlib sqlite (with rustup from nixpkgs) and nothing was out of the ordinary. So you (or your agent, idk who actually wrote that comment) are missing something. As it stands, I cannot reproduce your issue.

  10. zackees commented on Sep 15, 2026

    @zackees
    Author

    You are in the nix-shell to add the package. But it's unclear if you are trying to link while not in the nix-shell.

  11. maxdexh commented on Sep 15, 2026

    @maxdexh
    Member
    > nix-shell -p pkg-config openssl zlib sqlite
    > cargo run
       Compiling playground v0.1.0 (/home/max/Repos/rust/playground)
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
         Running `target/debug/playground`
    openssl="OpenSSL 3.6.3 9 Jun 2026" zlib="1.3.2" sqlite="3.51.2"
    > exit
    > target/debug/playground                                                                                                                                                      
    openssl="OpenSSL 3.6.3 9 Jun 2026" zlib="1.3.2" sqlite="3.51.2"
    > readelf -p .comment target/debug/playground | grep Linker
      [    48]  Linker: LLD 23.1.1 (/checkout/src/llvm-project/llvm 1b9c0d5ff9bbe7634aead059efe6b11a7eeba145)
    > readelf -d target/debug/playground | grep RUNPATH
     0x000000000000001d (RUNPATH)            Library runpath: [/nix/store/dhafmhsa7fps9i05yh226y9n8b70hmsf-shell/lib:/nix/store/483x61iy35irm4wr2b7dwzihljhp6da2-zlib-1.3.2/lib:/nix/store/pvxx7066ymzmmrlcfn7yl6kcbqvbc8lw-openssl-3.6.3/lib:/nix/store/0zyx0wg82m6pghdbjkrk5q4jvfq402wz-sqlite-3.51.2/lib:/nix/store/avld9cdn23zab2ssl30h2r6444rqh6ms-glibc-2.42-67/lib:/nix/store/7vafhlh0lmcvi75jfyy09qwr4m3x1ks3-gcc-15.2.0-lib/lib]
    > ldd target/debug/playground                                                                                                                                                         
    	linux-vdso.so.1 (0x00007f6a56a0b000)
    	libsqlite3.so => /nix/store/0zyx0wg82m6pghdbjkrk5q4jvfq402wz-sqlite-3.51.2/lib/libsqlite3.so (0x00007f6a56810000)
    	libssl.so.3 => /nix/store/pvxx7066ymzmmrlcfn7yl6kcbqvbc8lw-openssl-3.6.3/lib/libssl.so.3 (0x00007f6a566f6000)
    	libcrypto.so.3 => /nix/store/pvxx7066ymzmmrlcfn7yl6kcbqvbc8lw-openssl-3.6.3/lib/libcrypto.so.3 (0x00007f6a56000000)
    	libgcc_s.so.1 => /nix/store/7vafhlh0lmcvi75jfyy09qwr4m3x1ks3-gcc-15.2.0-lib/lib/libgcc_s.so.1 (0x00007f6a566c9000)
    	libc.so.6 => /nix/store/avld9cdn23zab2ssl30h2r6444rqh6ms-glibc-2.42-67/lib/libc.so.6 (0x00007f6a55c00000)
    	/nix/store/avld9cdn23zab2ssl30h2r6444rqh6ms-glibc-2.42-67/lib/ld-linux-x86-64.so.2 => /nix/store/avld9cdn23zab2ssl30h2r6444rqh6ms-glibc-2.42-67/lib64/ld-linux-x86-64.so.2 (0x00007f6a56a0d000)
    	libm.so.6 => /nix/store/avld9cdn23zab2ssl30h2r6444rqh6ms-glibc-2.42-67/lib/libm.so.6 (0x00007f6a55f08000)
    	libz.so.1 => /nix/store/483x61iy35irm4wr2b7dwzihljhp6da2-zlib-1.3.2/lib/libz.so.1 (0x00007f6a566a8000)
    	libdl.so.2 => /nix/store/avld9cdn23zab2ssl30h2r6444rqh6ms-glibc-2.42-67/lib/libdl.so.2 (0x00007f6a566a3000)
    	libpthread.so.0 => /nix/store/avld9cdn23zab2ssl30h2r6444rqh6ms-glibc-2.42-67/lib/libpthread.so.0 (0x00007f6a5669e000)
  12. zackees commented on Sep 15, 2026

    @zackees
    Author

    @maxdexh Thanks for the full output, that settled it. You can't reproduce because you're using nixpkgs' rustup, and nixpkgs already patches this bug away at toolchain install time.

    nixpkgs' rustup package carries 0001-dynamically-patchelf-binaries.patch (NixOS/nixpkgs#314268, merged June 2024). Its nix_wrap_lld moves every installed ld.lld into a sibling gcc-ld-unwrapped/ directory and writes a shim in its place:

    #!/usr/bin/env bash
    export PROG=".../bin/gcc-ld-unwrapped/ld.lld"
    "/nix/store/...-rustup-1.29.0/nix-support/ld-wrapper.sh" "$@"

    That ld-wrapper.sh is nixpkgs' bintools wrapper, the component that turns each -L /nix/store/... into -rpath. So on your machine rust-lld still gets nixpkgs' RUNPATH injection. Your RUNPATH shows its signature: the nix-shell's own -shell/lib first (from NIX_LDFLAGS), then the -L dirs, then glibc and gcc-lib last.

    I installed rustup via the rustup.rs installer. Its gcc-ld/ld.lld is the raw ELF from Rust CI and there is no gcc-ld-unwrapped/. Nothing wraps it.

    To confirm it was that file and nothing else, I ran your exact nix-shell -p pkg-config openssl zlib sqlite on my machine with nixpkgs rustup in an isolated RUSTUP_HOME, same rustc 1.95.0, and swapped only that one file:

    gcc-ld/ld.lld Linker RUNPATH ldd not found run
    nixpkgs shim LLD 22.1.2 shell, sqlite, zlib, openssl, glibc, gcc-lib 0 exit 0
    raw upstream ELF LLD 22.1.2 <shell>/lib only 3 exit 127

    Same result with the rustup.rs toolchain inside your shell (exit 127), and with a plain C link through cc -fuse-ld=lld -B<gcc-ld> (exit 127), so this is neither the shell nor PKG_CONFIG_PATH.

    Two corrections to what I wrote earlier:

    1. rustc does invoke cc. rustc --print link-args shows "cc" ... "-B<sysroot>/lib/rustlib/x86_64-unknown-linux-gnu/bin/gcc-ld" "-fuse-ld=lld". What gets bypassed is the bintools ld wrapper, because -B plus -fuse-ld=lld makes collect2 resolve ld.lld from the Rust sysroot instead of resolving ld through the wrapper. That is also why putting -rpath in the cc wrapper's cc-ldflags works.
    2. readelf -p .comment does not tell the two cases apart, since both say LLD. The check that does:
    head -c4 "$(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/gcc-ld/ld.lld"
    # ELF  -> rustup.rs toolchain, reproduces
    # #!/u -> nixpkgs rustup shim, does not

    So the behaviour is real for anyone on NixOS who installs rustup the way rustup.rs documents, and nixpkgs has handled it downstream for its own package. That narrows what is left for rustc: at most a note next to the lld default that on NixOS you want nixpkgs' rustup (or -Clinker-features=-lld -Clink-self-contained=-linker), which is in line with @ds84182's point that NixOS-specific linking belongs in nixpkgs.


    This comment was generated by clud, a custom agent made by Zach Vorhies who reviewed and authorized this post.

  13. zackees commented on Sep 15, 2026

    @zackees
    Author

    So disclosure, this is happening with a tool for compile optimization in rust, so many tools are pre-compiled archives that the tool is providing.

    I'm going to have to rule out now that it's not cross linux binaries being provided.

  14. zackees commented on Sep 15, 2026

    @zackees
    Author

    This looks like a work around has been provided in a pre tool and the Rust LLD path fix should be migrated here as well.

  15. zackees commented on Sep 15, 2026

    @zackees
    Author

    TL;DR

    The bug is real but only reaches people who install Rust with the rustup.rs installer on NixOS. nixpkgs' own rustup package already patches it. My tool installs upstream rustup, so it is affected, and I will carry the nixpkgs fix there. The only thing left for rust-lang is a documentation note next to the lld default.

    Background

    Since 1.90, rustc on x86_64-unknown-linux-gnu drives cc with -fuse-ld=lld -B <sysroot>/.../gcc-ld, which makes gcc pick Rust's bundled ld.lld instead of the ld on PATH. On NixOS the ld on PATH is a wrapper that turns every -L /nix/store/... into -rpath, and that wrapper is the only thing that gives a binary its RUNPATH. Skip it and the binary links cleanly, then fails at startup with libssl.so.3: cannot open shared object file.

    nixpkgs fixed this on its side in June 2024 (NixOS/nixpkgs#314268). Its rustup package replaces every installed gcc-ld/ld.lld with a shim that runs the nixpkgs ld wrapper around the real lld. @maxdexh uses that package, so rust-lld still gets RUNPATH injection and the bug does not appear. I use the rustup.rs installer, whose ld.lld is the raw upstream binary.

    What was ruled out

    My tool ships precompiled toolchain bundles, so I checked whether they were the cause. They are not. The failing binary carries a /nix/store/...-glibc-2.42 interpreter and Linker: LLD 22.1.2, which is the stock rustup.rs toolchain through NixOS's own cc. The bundled cross toolchain only engages with an explicit --target, and on that path it uses the bundle's GNU ld with -C link-self-contained=no, so rust-lld never runs there.

    Discriminator

    head -c4 "$(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/gcc-ld/ld.lld"
    # ELF  -> rustup.rs toolchain, reproduces
    # #!/u -> nixpkgs rustup shim, does not

    What happens next

    • I will migrate the nixpkgs ld.lld wrapping into my tool, since it installs its own upstream rustup and misses the nixpkgs fix.
    • I am not proposing rustc carry NixOS-specific linking behavior, in line with @ds84182's point.
    • For rust-lang, the remaining ask is a note next to the lld default: rustup.rs toolchains on NixOS need either nixpkgs' rustup or -Clinker-features=-lld -Clink-self-contained=-linker.

    This comment was generated by clud, a custom agent made by Zach Vorhies who reviewed and authorized this post.

  16. maxdexh commented on Sep 15, 2026

    @maxdexh
    Member

    If you're using rustup from the install script instead of nixpkgs, then I'm honestly surprised you even got it to run a compiler. Last time I tried that I couldn't even get through the installer. I would not expect an unpatched compiler to work on NixOS. Also note that the nixpkgs PR you linked is from a compiler team member.

  17. zackees commented on Sep 15, 2026

    @zackees
    Author

    Things seem to have improved and now works with this last fix.

  18. added
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    on Sep 15, 2026
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

    A-linkageArea: linking into static, shared libraries and binariesC-discussionCategory: Discussion or questions that doesn't represent real issues.O-NixOSOperating system: NixOS, https://nixos.org/

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions