rust-lang / rust-lang/rust

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

Open
#159,533 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-LLVM C-bug E-needs-investigation P-medium regression-from-stable-to-stable T-compiler
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the one- and two-codegen-unit commands, extract the embedded bitcode with llvm-ar, llvm-objcopy, and llvm-dis, and compare the results. Trace the PreLinkNoLTO and pre-link ThinLTO scheduling around ThinLTOBitcodeWriterPass and ExtraPasses; done means -Cpasses transformations are represented consistently in embedded bitcode regardless of codegen-unit count.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
43/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.