Skip to content

-Cpasses transformations are missing from embedded bitcode with one codegen unit #159533

Description

@shuklaayush

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions