Summary
When an rlib is compiled with -Cembed-bitcode=yes and a custom LLVM pass, the emitted object code reflects the pass but the embedded bitcode may not.
With -Ccodegen-units=1, the embedded bitcode is serialized before -Cpasses runs. With multiple codegen units, it is serialized after the pass.
As a result, a transformation requested through -Cpasses can silently disappear when another crate consumes the rlib through LTO.
Code
#![no_std]
use core::sync::atomic::{AtomicU64, Ordering};
#[unsafe(no_mangle)]
pub unsafe extern "C" fn atomic_add(ptr: *mut AtomicU64, value: u64) -> u64 {
unsafe { &*ptr }.fetch_add(value, Ordering::SeqCst)
}
Compile the same crate with one and two codegen units:
rustc repro.rs \
--edition=2024 \
--crate-type=rlib \
-O \
-Cembed-bitcode=yes \
-Cpasses=lower-atomic \
-Ccodegen-units=1 \
-o cgu1.rlib
rustc repro.rs \
--edition=2024 \
--crate-type=rlib \
-O \
-Cembed-bitcode=yes \
-Cpasses=lower-atomic \
-Ccodegen-units=2 \
-o cgu2.rlib
After extracting each rlib's object member, the embedded bitcode can be read with LLVM tools:
mkdir cgu1 cgu2
(cd cgu1 && llvm-ar x ../cgu1.rlib)
(cd cgu2 && llvm-ar x ../cgu2.rlib)
llvm-objcopy --dump-section .llvmbc=cgu1.bc cgu1/*.o
llvm-objcopy --dump-section .llvmbc=cgu2.bc cgu2/*.o
llvm-dis cgu1.bc -o cgu1.ll
llvm-dis cgu2.bc -o cgu2.ll
On Mach-O, the section name is __LLVM,__bitcode instead of .llvmbc:
llvm-objcopy --dump-section '__LLVM,__bitcode'=cgu1.bc cgu1/*.o
llvm-objcopy --dump-section '__LLVM,__bitcode'=cgu2.bc cgu2/*.o
The result is:
$ rg 'atomicrmw|cmpxchg|load atomic|store atomic' cgu1.ll
%3 = atomicrmw add ptr %0, i64 %1 seq_cst, align 8
$ rg 'atomicrmw|cmpxchg|load atomic|store atomic' cgu2.ll
# no output
The machine-code object is lowered in both cases. On AArch64, both objects contain the same non-atomic sequence:
ldr x8, [x0]
add x9, x8, x1
str x9, [x0]
The inconsistency is between the object code and the bitcode stored alongside it.
Expected behavior
The effect of a pre-link custom pass should be represented consistently in embedded LTO bitcode and should not depend on the number of codegen units.
In particular, because -Cpasses is intentionally not rerun during final LTO, its pre-link transformations need to be present in the bitcode supplied to LTO.
Regression
This worked with Rust 1.86.0: the one-CGU embedded bitcode contained no atomic operations.
It regressed in Rust 1.87.0. Rust 1.88.0 and later retain the atomicrmw in the one-CGU embedded bitcode.
The regression appears related to #133250, which moved embedded-bitcode serialization into the pre-link optimization pipeline.
In the PreLinkNoLTO path, ThinLTOBitcodeWriterPass is scheduled before ExtraPasses. In the pre-link ThinLTO path, the bitcode writer is scheduled after ExtraPasses. This appears to explain the codegen-unit-dependent result.
The final LTO stage does not repair the difference because #97969 intentionally made -Cpasses pre-link-only.
Version
Reproduced on the latest nightly available on 2026-07-18:
rustc 1.99.0-nightly (b6839f4d0 2026-07-17)
binary: rustc
commit-hash: b6839f4d0e2bd63b960bbff8619c6fdea27d81e5
commit-date: 2026-07-17
host: aarch64-apple-darwin
release: 1.99.0-nightly
LLVM version: 22.1.8
@rustbot modify labels: +regression-from-stable-to-stable -regression-untriaged
cc @dianqk, since this appears related to the embedded-bitcode pipeline added in #133250.
Summary
When an rlib is compiled with
-Cembed-bitcode=yesand a custom LLVM pass, the emitted object code reflects the pass but the embedded bitcode may not.With
-Ccodegen-units=1, the embedded bitcode is serialized before-Cpassesruns. With multiple codegen units, it is serialized after the pass.As a result, a transformation requested through
-Cpassescan silently disappear when another crate consumes the rlib through LTO.Code
Compile the same crate with one and two codegen units:
After extracting each rlib's object member, the embedded bitcode can be read with LLVM tools:
On Mach-O, the section name is
__LLVM,__bitcodeinstead of.llvmbc:The result is:
The machine-code object is lowered in both cases. On AArch64, both objects contain the same non-atomic sequence:
The inconsistency is between the object code and the bitcode stored alongside it.
Expected behavior
The effect of a pre-link custom pass should be represented consistently in embedded LTO bitcode and should not depend on the number of codegen units.
In particular, because
-Cpassesis intentionally not rerun during final LTO, its pre-link transformations need to be present in the bitcode supplied to LTO.Regression
This worked with Rust 1.86.0: the one-CGU embedded bitcode contained no atomic operations.
It regressed in Rust 1.87.0. Rust 1.88.0 and later retain the
atomicrmwin the one-CGU embedded bitcode.The regression appears related to #133250, which moved embedded-bitcode serialization into the pre-link optimization pipeline.
In the
PreLinkNoLTOpath,ThinLTOBitcodeWriterPassis scheduled beforeExtraPasses. In the pre-link ThinLTO path, the bitcode writer is scheduled afterExtraPasses. This appears to explain the codegen-unit-dependent result.The final LTO stage does not repair the difference because #97969 intentionally made
-Cpassespre-link-only.Version
Reproduced on the latest nightly available on 2026-07-18:
@rustbot modify labels: +regression-from-stable-to-stable -regression-untriaged
cc @dianqk, since this appears related to the embedded-bitcode pipeline added in #133250.