Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions compiler/rustc_codegen_ssa/src/back/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,32 @@ impl ModuleConfig {
let emit_obj = if !should_emit_obj {
EmitObj::None
} else if sess.target.obj_is_bitcode
|| (sess.opts.cg.linker_plugin_lto.enabled() && !no_builtins)
|| (sess.opts.cg.linker_plugin_lto.enabled()
&& (!no_builtins || tcx.sess.is_sanitizer_cfi_enabled()))
{
// This case is selected if the target uses objects as bitcode, or
// if linker plugin LTO is enabled. In the linker plugin LTO case
// the assumption is that the final link-step will read the bitcode
// and convert it to object code. This may be done by either the
// native linker or rustc itself.
//
// Note, however, that the linker-plugin-lto requested here is
// explicitly ignored for `#![no_builtins]` crates. These crates are
// specifically ignored by rustc's LTO passes and wouldn't work if
// loaded into the linker. These crates define symbols that LLVM
// lowers intrinsics to, and these symbol dependencies aren't known
// until after codegen. As a result any crate marked
// `#![no_builtins]` is assumed to not participate in LTO and
// instead goes on to generate object code.
// By default this branch is skipped for `#![no_builtins]` crates so
// they emit native object files (machine code), not LLVM bitcode
// objects for the linker (see rust-lang/rust#146133).
//
// However, when LLVM CFI is enabled (`-Zsanitizer=cfi`), this
// breaks LLVM's expected pipeline: LLVM emits `llvm.type.test`
// intrinsics and related metadata that must be lowered by LLVM's
// `LowerTypeTests` pass before instruction selection during
// link-time LTO. Otherwise, `llvm.type.test` intrinsics and related
// metadata are not lowered by LLVM's `LowerTypeTests` pass before
// reaching the target backend, and LLVM may abort during codegen
// (for example in SelectionDAG type legalization) (see
// rust-lang/rust#142284).
//
// Therefore, with `-Clinker-plugin-lto` and `-Zsanitizer=cfi`, a
// `#![no_builtins]` crate must still use rustc's `EmitObj::Bitcode`
// path (and emit LLVM bitcode in the `.o` for linker-based LTO).
Copy link
Copy Markdown
Member

@bjorn3 bjorn3 May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason we don't use LTO for compiler-builtins is that doing so breaks the weak linkage that compiler-builtins uses. We use a legacy interface for implementing LTO that treats weak and strong symbols identically. We tried to make compiler-builtins participate in LTO before in #113923, but this resulted in the regression #118609.

In any case just emitting bitcode is not enough. You also need to updated ignored_for_lto:

/// Returns a boolean indicating whether the specified crate should be ignored
/// during LTO.
///
/// Crates ignored during LTO are not lumped together in the "massive object
/// file" that we create and are linked in their normal rlib states. See
/// comments below for what crates do not participate in LTO.
///
/// It's unusual for a crate to not participate in LTO. Typically only
/// compiler-specific and unstable crates have a reason to not participate in
/// LTO.
pub fn ignored_for_lto(sess: &Session, info: &CrateInfo, cnum: CrateNum) -> bool {
// If our target enables builtin function lowering in LLVM then the
// crates providing these functions don't participate in LTO (e.g.
// no_builtins or compiler builtins crates).
!sess.target.no_builtins
&& (info.compiler_builtins == Some(cnum) || info.is_no_builtins.contains(&cnum))
}

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change only makes it participate on LTO when using both -Clinker-plugin-lto (not rustc LTO) and -Zsanitizer=cfi, which expects a clang or ld.lld linker already and thus doesn't cause the regressions listed on #146133, with the regression tests added by @dianqk now passing. However, I see that the regressions you mention are different ones. Are there any regression tests for them I can verify?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cargo uses -Clinker-plugin-lto when compiling rlib crates even when not doing linker plugin LTO to skip unnecessary codegen for them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused about what is the issue you're referring to. ignored_for_lto is for rustc LTO (not proper LTO). The #142284 issue is when using both -Clinker-plugin-lto (proper LTO) and -Zsanitizer=cfi, where LTO is deferred to the linker; the fix needed was to ensure those crates emit LLVM bitcode so proper LTO can run the LowerTypeTests pass and lower llvm.type.test intrinsics and related metadata. (Note the issue #142284 doesn't happen with rust LTO: #142284 (comment) because these are properly lowered already.) Would you mind providing more details? Maybe a regression test or a reproducer?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking wrong in my earlier comments. I however did just remember that you tried to land pretty much the same PR previously as #145368, which then had to be reverted in #146133 due to regressions. Has anything changed since then to fix those regressions even when compiling compiler-builtins with LTO enabled?

Copy link
Copy Markdown
Member Author

@rcvalle rcvalle May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in my previous attempt, I changed it to emit LLVM bitcode for whenever -Clinker-plugin-lto was used so when people used it with older non-LLVM (system) linkers, these didn't work with the LLVM bitcode. In this attempt, I changed it to emit LLVM bitcode only when CFI is enabled, which already requires a newer LLVM linker that works with the LLVM bitcode and wouldn't work with older non-LLVM (system) linkers anyway.

What I was unsure is whether there could be any case where emitting LLVM bitcode for compiler_builtins (even only when CFI is enabled) could fail for some other reason such as #118609 that you mentioned, but since it's currently blocking the use of CFI with -Zbuild-std, and I haven't seen any issues so far, and don't have a regression test or a reproducer for it, I'd rather unblock it and fix forward any issues that might appear.

But before merging it, I still want to add a couple more tests and will let you know when it's ready.

EmitObj::Bitcode
} else if need_bitcode_in_object(tcx) || sess.target.requires_lto {
EmitObj::ObjectCode(BitcodeSection::Full)
Expand Down
10 changes: 10 additions & 0 deletions tests/run-make-cargo/sanitizer-cfi-build-std-clang/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Workspace mirroring the examples in <https://github.com/rcvalle/rust-cfi-examples>.
[workspace]
resolver = "2"
members = [
"invalid-branch-target-abort",
"indirect-arity-mismatch-abort",
"indirect-type-mismatch-abort",
"cross-lang-cfi-types-crate-abort",
"cross-lang-cfi-types-crate-not-abort",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "cross-lang-cfi-types-crate-abort"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[dependencies]
cfi-types = "0.0.5"
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn llvm_ar_path() -> PathBuf {
if let Ok(d) = env::var("LLVM_BIN_DIR") {
let llvm_ar = Path::new(d.trim_end_matches('/')).join("llvm-ar");
if llvm_ar.exists() {
return llvm_ar;
}
}
if let Ok(clang) = env::var("CLANG") {
let clang = Path::new(&clang);
if let Some(clang_dir) = clang.parent() {
let llvm_ar = clang_dir.join("llvm-ar");
if llvm_ar.exists() {
return llvm_ar;
}
}
}
PathBuf::from("llvm-ar")
}

fn main() {
let out_dir = env::var("OUT_DIR").expect("OUT_DIR");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let c_src = Path::new(&manifest_dir).join("src/foo.c");
let bc_path = Path::new(&out_dir).join("foo.bc");
let a_path = Path::new(&out_dir).join("libfoo.a");

let clang =
env::var("CC").or_else(|_| env::var("CLANG")).unwrap_or_else(|_| "clang".to_string());
let llvm_ar = llvm_ar_path();

let st = Command::new(&clang)
.args([
"-Wall",
"-flto=thin",
"-fsanitize=cfi",
"-fvisibility=hidden",
"-c",
"-emit-llvm",
"-o",
])
.arg(&bc_path)
.arg(&c_src)
.status()
.unwrap_or_else(|e| panic!("failed to spawn `{clang}`: {e}"));
assert!(st.success(), "`{clang}` failed with {st}");

let st = Command::new(&llvm_ar)
.args(["rcs", a_path.to_str().unwrap(), bc_path.to_str().unwrap()])
.status()
.unwrap_or_else(|e| panic!("failed to spawn `{}`: {e}", llvm_ar.display()));
assert!(st.success(), "`{}` failed with {st}", llvm_ar.display());

println!("cargo:rustc-link-search=native={out_dir}");
println!("cargo:rustc-link-lib=static=foo");
println!("cargo:rerun-if-changed={}", c_src.display());
println!("cargo:rerun-if-changed=build.rs");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
int
do_twice(int (*fn)(int), int arg)
{
return fn(arg) + fn(arg);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// This example demonstrates redirecting control flow using an indirect
// branch/call to a function with different return and parameter types than the
// return type expected and arguments intended/passed at the call/branch site,
// across the FFI boundary using the `cfi_types` crate for cross-language LLVM
// CFI.

use std::mem;

use cfi_types::{c_int, c_long};

#[link(name = "foo")]
unsafe extern "C" {
fn do_twice(f: unsafe extern "C" fn(c_int) -> c_int, arg: i32) -> i32;
}

unsafe extern "C" fn add_one(x: c_int) -> c_int {
c_int(x.0 + 1)
}

unsafe extern "C" fn add_two(x: c_long) -> c_long {
c_long(x.0 + 2)
}

fn main() {
let answer = unsafe { do_twice(add_one, 5) };

println!("The answer is: {}", answer);

println!("With CFI enabled, you should not see the next answer");
let f: unsafe extern "C" fn(c_int) -> c_int = unsafe {
mem::transmute::<*const u8, unsafe extern "C" fn(c_int) -> c_int>(add_two as *const u8)
};
let next_answer = unsafe { do_twice(f, 5) };

println!("The next answer is: {}", next_answer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "cross-lang-cfi-types-crate-not-abort"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[dependencies]
cfi-types = "0.0.5"
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn llvm_ar_path() -> PathBuf {
if let Ok(d) = env::var("LLVM_BIN_DIR") {
let llvm_ar = Path::new(d.trim_end_matches('/')).join("llvm-ar");
if llvm_ar.exists() {
return llvm_ar;
}
}
if let Ok(clang) = env::var("CLANG") {
let clang = Path::new(&clang);
if let Some(clang_dir) = clang.parent() {
let llvm_ar = clang_dir.join("llvm-ar");
if llvm_ar.exists() {
return llvm_ar;
}
}
}
PathBuf::from("llvm-ar")
}

fn main() {
let out_dir = env::var("OUT_DIR").expect("OUT_DIR");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let c_src = Path::new(&manifest_dir).join("src/foo.c");
let bc_path = Path::new(&out_dir).join("foo.bc");
let a_path = Path::new(&out_dir).join("libfoo.a");

let clang =
env::var("CC").or_else(|_| env::var("CLANG")).unwrap_or_else(|_| "clang".to_string());
let llvm_ar = llvm_ar_path();

let st = Command::new(&clang)
.args([
"-Wall",
"-flto=thin",
"-fsanitize=cfi",
"-fvisibility=hidden",
"-c",
"-emit-llvm",
"-o",
])
.arg(&bc_path)
.arg(&c_src)
.status()
.unwrap_or_else(|e| panic!("failed to spawn `{clang}`: {e}"));
assert!(st.success(), "`{clang}` failed with {st}");

let st = Command::new(&llvm_ar)
.args(["rcs", a_path.to_str().unwrap(), bc_path.to_str().unwrap()])
.status()
.unwrap_or_else(|e| panic!("failed to spawn `{}`: {e}", llvm_ar.display()));
assert!(st.success(), "`{}` failed with {st}", llvm_ar.display());

println!("cargo:rustc-link-search=native={out_dir}");
println!("cargo:rustc-link-lib=static=foo");
println!("cargo:rerun-if-changed={}", c_src.display());
println!("cargo:rerun-if-changed=build.rs");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <stdio.h>
#include <stdlib.h>

// This definition has the type id "_ZTSFvlE".
void
hello_from_c(long arg)
{
printf("Hello from C!\n");
}

// This definition has the type id "_ZTSFvPFvlElE"--this can be ignored for the
// purposes of this example.
void
indirect_call_from_c(void (*fn)(long), long arg)
{
// This call site tests whether the destination pointer is a member of the
// group derived from the same type id of the fn declaration, which has the
// type id "_ZTSFvlE".
//
// Notice that since the test is at the call site and generated by Clang,
// the type id used in the test is encoded by Clang.
fn(arg);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use cfi_types::c_long;

#[link(name = "foo")]
extern "C" {
// This declaration has the type id "_ZTSFvlE" because it uses the CFI types
// for cross-language LLVM CFI support. The cfi_types crate provides a new
// set of C types as user-defined types using the cfi_encoding attribute and
// repr(transparent) to be used for cross-language LLVM CFI support. This
// new set of C types allows the Rust compiler to identify and correctly
// encode C types in extern "C" function types indirectly called across the
// FFI boundary when CFI is enabled.
fn hello_from_c(_: c_long);

// This declaration has the type id "_ZTSFvPFvlElE" because it uses the CFI
// types for cross-language LLVM CFI support--this can be ignored for the
// purposes of this example.
fn indirect_call_from_c(f: unsafe extern "C" fn(c_long), arg: c_long);
}

// This definition has the type id "_ZTSFvlE" because it uses the CFI types for
// cross-language LLVM CFI support, similarly to the hello_from_c declaration
// above.
unsafe extern "C" fn hello_from_rust(_: c_long) {
println!("Hello, world!");
}

// This definition has the type id "_ZTSFvlE" because it uses the CFI types for
// cross-language LLVM CFI support, similarly to the hello_from_c declaration
// above.
unsafe extern "C" fn hello_from_rust_again(_: c_long) {
println!("Hello from Rust again!");
}

// This definition would also have the type id "_ZTSFvPFvlElE" because it uses
// the CFI types for cross-language LLVM CFI support, similarly to the
// hello_from_c declaration above--this can be ignored for the purposes of this
// example.
fn indirect_call(f: unsafe extern "C" fn(c_long), arg: c_long) {
// This indirect call site tests whether the destination pointer is a member
// of the group derived from the same type id of the f declaration, which
// has the type id "_ZTSFvlE" because it uses the CFI types for
// cross-language LLVM CFI support, similarly to the hello_from_c
// declaration above.
unsafe { f(arg) }
}

// This definition has the type id "_ZTSFvvE"--this can be ignored for the
// purposes of this example.
fn main() {
// This demonstrates an indirect call within Rust-only code using the same
// encoding for hello_from_rust and the test at the indirect call site at
// indirect_call (i.e., "_ZTSFvlE").
indirect_call(hello_from_rust, c_long(5));

// This demonstrates an indirect call across the FFI boundary with the Rust
// compiler and Clang using the same encoding for hello_from_c and the test
// at the indirect call site at indirect_call (i.e., "_ZTSFvlE").
indirect_call(hello_from_c, c_long(5));

// This demonstrates an indirect call to a function passed as a callback
// across the FFI boundary with the Rust compiler and Clang the same
// encoding for the passed-callback declaration and the test at the indirect
// call site at indirect_call_from_c (i.e., "_ZTSFvlE").
unsafe {
indirect_call_from_c(hello_from_rust_again, c_long(5));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "cross-lang-integer-normalization-abort"
version = "0.1.0"
edition = "2021"
build = "build.rs"

# Not a member of the parent `sanitizer-cfi-build-std-clang` workspace so it can
# be built with different `RUSTFLAGS` (i.e., integer normalization).
[workspace]
members = ["."]
resolver = "2"
Loading
Loading