Consider documenting: rust-lld default (1.90+) disables LTO for cc-rs consumers using GCC -flto -ffat-lto-objects
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
Location (URL)
https://blog.rust-lang.org/2025/09/01/rust-lld-on-1.90.0-stable/
Summary
Just to preface, I'm not sure whether this is the right place/tag with which to file. It seems like I'm running into expected behavior that I just don't see documented anywhere, but I'd like to check with the broader community first. If this is already known/documented somewhere upstream that I missed, a pointer would be great. Happy to move to a different repo/tag if there's a better home.
Issue
We recently bumped MSRV on the s2n-tls Rust bindings from 1.89 to 1.91. Afterwards, every TLS handshake benchmark in our daily CI regressed by ~2-4% (~20 µs/handshake).
Bisecting inside the MSRV bump window pointed at the toolchain bump itself as the trigger, and once I ran with rust-lld explicitly, I was able to pinpoint the cause of the regression cleanly.
Root cause
This seems to happen because s2n-tls-sys's build.rs compiles the vendored libs2n C code with -flto -ffat-lto-objects when the C compiler is GCC-like. GCC produces fat objects with the compiled function code in .gnu.lto_* (GIMPLE bytecode) sections. Under bfd, collect2 auto-loads GCC's liblto_plugin.so, which consumes the GIMPLE and performs cross-file LTO. Under LLD (the x86_64-unknown-linux-gnu default as of Rust 1.90), the plugin protocol isn't implemented and, per LLD's own docs, LLD's LTO story seems primarily scoped to LLVM bitcode from Clang. The .gnu.lto_* sections get silently discarded, only the native-code fallback in the fat objects is linked, and the build succeeds with no warning. However, the resulting binary is measurably slower.
The behavior itself is a straightforward consequence of rust-lld's design and LLD's LTO scope. But I couldn't find a note anywhere connecting "1.90 lld default" to "cc-rs + GCC + fat-LTO consumers silently lose their LTO."
Minimal Repro
// foo.c
int foo(int x) { return x * 2; }
int foo_impl(int x) { return foo(x) + 1; }
// main.c
extern int foo_impl(int);
int main() { return foo_impl(1); }
gcc -c -O2 -flto -ffat-lto-objects foo.c main.c
ar rcs libfoo.a foo.o main.o
# Path A: bfd + liblto_plugin.so
gcc -O2 -flto -o out_bfd main.o foo.o
# objdump: main = mov $0x3; ret (LTO folded foo_impl(1) → 3)
# Path B: rustc → rust-lld on the same archive (what cc-rs consumers actually get)
# Wrapper crate links libfoo.a via `cargo:rustc-link-lib=static=foo`,
# built on 1.90+ x86_64-unknown-linux-gnu with default linker.
# objdump: main = jmp foo_impl (no LTO; native fallback only)
Possible Outcomes
- If this is already documented somewhere I missed could someone point me at it?
- If it's expected but undocumented and worth noting a note in the 1.90 blog post under known incompatibilities, the release notes, or the platform support docs would help the next person hit this once rather than try and root cause it. The blog already recommends
RUSTFLAGS="-Clinker-features=-lld"as the general opt-out; the ask is just to acknowledge this specific class of pattern as one of the expected cases.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read the linked Rust 1.90 rust-lld announcement, then compare RELEASES.md and the platform support documentation as possible locations. Document the interaction between rust-lld, GCC -flto -ffat-lto-objects archives, and cc-rs consumers, including the RUSTFLAGS opt-out; done means the expected limitation and workaround are clearly discoverable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, rust
- Domain
- build-system, documentation
- Issue type
- Documentation
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100