CFI: Fix LTO for #![no_builtins] crates with CFI

Fixes LTO for `#![no_builtins]` crates with CFI enabled by using rustc's
`EmitObj::Bitcode` path (and emitting LLVM bitcode in the `.o` for
linker-based LTO).
This commit is contained in:
Ramon de C Valle
2026-04-30 19:37:04 -07:00
parent f53b654a88
commit acc33fe5dd
37 changed files with 987 additions and 9 deletions
+19 -9
View File
@@ -136,7 +136,8 @@ macro_rules! if_regular {
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
@@ -144,14 +145,23 @@ macro_rules! if_regular {
// 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).
EmitObj::Bitcode
} else if need_bitcode_in_object(tcx) || sess.target.requires_lto {
EmitObj::ObjectCode(BitcodeSection::Full)
@@ -0,0 +1,45 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cfi-types"
version = "0.0.8"
[[package]]
name = "cross-lang-cfi-types-crate-abort"
version = "0.1.0"
dependencies = [
"cfi-types",
]
[[package]]
name = "cross-lang-cfi-types-crate-not-abort"
version = "0.1.0"
dependencies = [
"cfi-types",
]
[[package]]
name = "indirect-arity-mismatch-abort"
version = "0.1.0"
[[package]]
name = "indirect-pointee-type-mismatch-abort"
version = "0.1.0"
[[package]]
name = "indirect-return-type-mismatch-abort"
version = "0.1.0"
[[package]]
name = "indirect-type-mismatch-abort"
version = "0.1.0"
[[package]]
name = "indirect-type-qualifier-mismatch-abort"
version = "0.1.0"
[[package]]
name = "invalid-branch-target-abort"
version = "0.1.0"
@@ -0,0 +1,13 @@
# 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-pointee-type-mismatch-abort",
"indirect-return-type-mismatch-abort",
"indirect-type-qualifier-mismatch-abort",
"indirect-type-mismatch-abort",
"cross-lang-cfi-types-crate-abort",
"cross-lang-cfi-types-crate-not-abort",
]
@@ -0,0 +1,7 @@
[package]
name = "cross-lang-cfi-types-crate-abort"
version = "0.1.0"
edition = "2021"
[dependencies]
cfi-types = { path = "../vendor/cfi-types" }
@@ -0,0 +1,5 @@
include!("../shared_build_rs.rs");
fn main() {
build_foo_static_lib(&[]);
}
@@ -0,0 +1,5 @@
int
do_twice(int (*fn)(int), int arg)
{
return fn(arg) + fn(arg);
}
@@ -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);
}
@@ -0,0 +1,7 @@
[package]
name = "cross-lang-cfi-types-crate-not-abort"
version = "0.1.0"
edition = "2021"
[dependencies]
cfi-types = { path = "../vendor/cfi-types" }
@@ -0,0 +1,5 @@
include!("../shared_build_rs.rs");
fn main() {
build_foo_static_lib(&[]);
}
@@ -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);
}
@@ -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));
}
}
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cross-lang-integer-normalization-abort"
version = "0.1.0"
@@ -0,0 +1,10 @@
[package]
name = "cross-lang-integer-normalization-abort"
version = "0.1.0"
edition = "2021"
# 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"
@@ -0,0 +1,5 @@
include!("../shared_build_rs.rs");
fn main() {
build_foo_static_lib(&["-fsanitize-cfi-icall-experimental-normalize-integers"]);
}
@@ -0,0 +1,5 @@
int
do_twice(int (*fn)(int), int arg)
{
return fn(arg) + fn(arg);
}
@@ -0,0 +1,34 @@
// 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 integer normalization for cross-language LLVM
// CFI.
use std::mem;
#[link(name = "foo")]
extern "C" {
fn do_twice(f: unsafe extern "C" fn(i32) -> i32, arg: i32) -> i32;
}
unsafe extern "C" fn add_one(x: i32) -> i32 {
x + 1
}
unsafe extern "C" fn add_two(x: i64) -> i64 {
x + 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(i32) -> i32 = unsafe {
mem::transmute::<*const u8, unsafe extern "C" fn(i32) -> i32>(add_two as *const u8)
};
let next_answer = unsafe { do_twice(f, 5) };
println!("The next answer is: {}", next_answer);
}
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cross-lang-integer-normalization-not-abort"
version = "0.1.0"
@@ -0,0 +1,10 @@
[package]
name = "cross-lang-integer-normalization-not-abort"
version = "0.1.0"
edition = "2021"
# 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"
@@ -0,0 +1,5 @@
include!("../shared_build_rs.rs");
fn main() {
build_foo_static_lib(&["-fsanitize-cfi-icall-experimental-normalize-integers"]);
}
@@ -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);
}
@@ -0,0 +1,89 @@
use std::ffi::c_long;
#[link(name = "foo")]
extern "C" {
// This declaration would have the type id "_ZTSFvlE", but at the time types
// are encoded, all type aliases are already resolved to their respective
// Rust aliased types, so this is encoded either as "_ZTSFvu3i32E" or
// "_ZTSFvu3i64E" depending to what type c_long type alias is resolved to,
// which currently uses the u<length><type-name> vendor extended type
// encoding for the Rust integer types--this is the problem demonstrated in
// this example.
fn hello_from_c(_: c_long);
// This declaration would have the type id "_ZTSFvPFvlElE", but is encoded
// either as "_ZTSFvPFvu3i32ES_E" (compressed) or "_ZTSFvPFvu3i64ES_E"
// (compressed), similarly to the hello_from_c declaration above--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 would have the type id "_ZTSFvlE", but is encoded either as
// "_ZTSFvu3i32E" or "_ZTSFvu3i64E", similarly to the hello_from_c declaration
// above.
unsafe extern "C" fn hello_from_rust(_: c_long) {
println!("Hello, world!");
}
// This definition would have the type id "_ZTSFvlE", but is encoded either as
// "_ZTSFvu3i32E" or "_ZTSFvu3i64E", 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", but is encoded
// either as "_ZTSFvPFvu3i32ES_E" (compressed) or "_ZTSFvPFvu3i64ES_E"
// (compressed), 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
// would have the type id "_ZTSFvlE", but is encoded either as
// "_ZTSFvu3i32E" or "_ZTSFvu3i64E", similarly to the hello_from_c
// declaration above.
//
// Notice that since the test is at the call site and generated by the Rust
// compiler, the type id used in the test is encoded by the Rust compiler.
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., "_ZTSFvu3i32E" or "_ZTSFvu3i64E").
indirect_call(hello_from_rust, 5);
// This demonstrates an indirect call across the FFI boundary with the Rust
// compiler and Clang using different encodings for hello_from_c and the
// test at the indirect call site at indirect_call (i.e., "_ZTSFvu3i32E" or
// "_ZTSFvu3i64E" vs "_ZTSFvlE").
//
// When using rustc LTO (i.e., -Clto), this works because the type id used
// is from the Rust-declared hello_from_c, which is encoded by the Rust
// compiler (i.e., "_ZTSFvu3i32E" or "_ZTSFvu3i64E").
//
// When using (proper) LTO (i.e., -Clinker-plugin-lto), this does not work
// because the type id used is from the C-defined hello_from_c, which is
// encoded by Clang (i.e., "_ZTSFvlE").
indirect_call(hello_from_c, 5);
// This demonstrates an indirect call to a function passed as a callback
// across the FFI boundary with the Rust compiler and Clang using different
// encodings for the passed-callback declaration and the test at the
// indirect call site at indirect_call_from_c (i.e., "_ZTSFvu3i32E" or
// "_ZTSFvu3i64E" vs "_ZTSFvlE").
//
// When Rust functions are passed as callbacks across the FFI boundary to be
// called back from C code, the tests are also at the call site but
// generated by Clang instead, so the type ids used in the tests are encoded
// by Clang, which will not match the type ids of declarations encoded by
// the Rust compiler (e.g., hello_from_rust_again). (The same happens the
// other way around for C functions passed as callbacks across the FFI
// boundary to be called back from Rust code.)
unsafe {
indirect_call_from_c(hello_from_rust_again, 5);
}
}
@@ -0,0 +1,4 @@
[package]
name = "indirect-arity-mismatch-abort"
version = "0.1.0"
edition = "2021"
@@ -0,0 +1,30 @@
// This example demonstrates redirecting control flow using an indirect
// branch/call to a function with a different number of parameters than
// arguments intended/passed at the call/branch site.
use std::mem;
fn add_one(x: i32) -> i32 {
x + 1
}
fn add_two(x: i32, _y: i32) -> i32 {
x + 2
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
println!("With CFI enabled, you should not see the next answer");
let f: fn(i32) -> i32 =
unsafe { mem::transmute::<*const u8, fn(i32) -> i32>(add_two as *const u8) };
let next_answer = do_twice(f, 5);
println!("The next answer is: {}", next_answer);
}
@@ -0,0 +1,4 @@
[package]
name = "indirect-pointee-type-mismatch-abort"
version = "0.1.0"
edition = "2021"
@@ -0,0 +1,32 @@
// This example demonstrates redirecting control flow using an indirect
// branch/call to a function with different return and parameter (i.e., pointee)
// types than the return type expected and arguments intended/passed at the
// call/branch site.
use std::mem;
fn add_one(x: *const i32) -> i32 {
unsafe { *x + 1 }
}
fn add_two(x: *const i64) -> i32 {
unsafe { (*x + 2) as i32 }
}
fn do_twice(f: fn(*const i32) -> i32, arg: *const i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let value: i32 = 5;
let answer = do_twice(add_one, &value);
println!("The answer is: {}", answer);
println!("With CFI enabled, you should not see the next answer");
let f: fn(*const i32) -> i32 =
unsafe { mem::transmute::<*const u8, fn(*const i32) -> i32>(add_two as *const u8) };
let next_answer = do_twice(f, &value);
println!("The next answer is: {}", next_answer);
}
@@ -0,0 +1,4 @@
[package]
name = "indirect-return-type-mismatch-abort"
version = "0.1.0"
edition = "2021"
@@ -0,0 +1,30 @@
// This example demonstrates redirecting control flow using an indirect
// branch/call to a function with a different return type than the return type
// expected at the call/branch site.
use std::mem;
fn add_one(x: i32) -> i32 {
x + 1
}
fn add_two(x: i32) -> i64 {
i64::from(x + 2)
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
println!("With CFI enabled, you should not see the next answer");
let f: fn(i32) -> i32 =
unsafe { mem::transmute::<*const u8, fn(i32) -> i32>(add_two as *const u8) };
let next_answer = do_twice(f, 5);
println!("The next answer is: {}", next_answer);
}
@@ -0,0 +1,4 @@
[package]
name = "indirect-type-mismatch-abort"
version = "0.1.0"
edition = "2021"
@@ -0,0 +1,30 @@
// 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.
use std::mem;
fn add_one(x: i32) -> i32 {
x + 1
}
fn add_two(x: i64) -> i64 {
x + 2
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
println!("With CFI enabled, you should not see the next answer");
let f: fn(i32) -> i32 =
unsafe { mem::transmute::<*const u8, fn(i32) -> i32>(add_two as *const u8) };
let next_answer = do_twice(f, 5);
println!("The next answer is: {}", next_answer);
}
@@ -0,0 +1,4 @@
[package]
name = "indirect-type-qualifier-mismatch-abort"
version = "0.1.0"
edition = "2021"
@@ -0,0 +1,31 @@
// This example demonstrates redirecting control flow using an indirect
// branch/call to a function with parameter type qualifiers than the argument
// type qualifiers intended/passed at the call/branch site.
use std::mem;
fn add_one(x: &i32) -> i32 {
*x + 1
}
fn add_two(x: &mut i32) -> i32 {
*x + 2
}
fn do_twice(f: fn(&i32) -> i32, arg: &i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let value: i32 = 5;
let answer = do_twice(add_one, &value);
println!("The answer is: {}", answer);
println!("With CFI enabled, you should not see the next answer");
let f: fn(&i32) -> i32 =
unsafe { mem::transmute::<*const u8, fn(&i32) -> i32>(add_two as *const u8) };
let next_answer = do_twice(f, &value);
println!("The next answer is: {}", next_answer);
}
@@ -0,0 +1,4 @@
[package]
name = "invalid-branch-target-abort"
version = "0.1.0"
edition = "2021"
@@ -0,0 +1,50 @@
// This example demonstrates redirecting control flow using an indirect
// branch/call to an invalid destination (i.e., within the body of the
// function).
use std::mem;
fn add_one(x: i32) -> i32 {
x + 1
}
#[unsafe(naked)]
pub extern "C" fn add_two(_x: i32) -> ! {
// x + 2 preceded by a landing pad/nop block
core::arch::naked_asm!(
r#"
nop
nop
nop
nop
nop
nop
nop
nop
nop
lea eax, [rdi + 2]
ret
"#,
);
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
println!("With CFI enabled, you should not see the next answer");
let f: fn(i32) -> i32 = unsafe {
// Offset 0 is a valid branch/call destination (i.e., the function entry
// point), but offsets 1-8 within the landing pad/nop block are invalid
// branch/call destinations (i.e., within the body of the function).
mem::transmute::<*const u8, fn(i32) -> i32>((add_two as *const u8).offset(5))
};
let next_answer = do_twice(f, 5);
println!("The next answer is: {}", next_answer);
}
@@ -0,0 +1,151 @@
//! Verifies that the examples in <https://github.com/rcvalle/rust-cfi-examples> build and run with
//!`-Zbuild-std` to prevent regressions such as [rust-lang/rust#142284].
//@ needs-sanitizer-cfi
//@ needs-force-clang-based-tests
//@ needs-rust-lld
//@ needs-target-std
//@ ignore-cross-compile
//@ only-x86_64-unknown-linux-gnu
#![deny(warnings)]
use std::path::Path;
use run_make_support::external_deps::rustc::sysroot as rustc_sysroot;
use run_make_support::run::cmd;
use run_make_support::{bin_name, cargo, path, target};
fn clang_path() -> String {
if let Ok(d) = std::env::var("LLVM_BIN_DIR") {
let clang = Path::new(d.trim_end_matches('/')).join("clang");
if clang.exists() {
return clang.display().to_string();
}
}
if let Ok(clang) = std::env::var("CLANG") {
let clang = Path::new(clang.trim_end_matches('/'));
if clang.exists() {
return clang.display().to_string();
}
}
"clang".to_string()
}
fn fuse_ld_path() -> String {
if let Ok(d) = std::env::var("LLVM_BIN_DIR") {
let llvm_bin_dir = Path::new(d.trim_end_matches('/'));
let gcc_ld_lld = llvm_bin_dir.join("gcc-ld").join("ld.lld");
if gcc_ld_lld.exists() {
return gcc_ld_lld.display().to_string();
}
let ld_lld = llvm_bin_dir.join("ld.lld");
if ld_lld.exists() {
return ld_lld.display().to_string();
}
}
if let Ok(clang) = std::env::var("CLANG") {
let clang = Path::new(clang.trim_end_matches('/'));
if let Some(clang_dir) = clang.parent() {
let gcc_ld_lld = clang_dir.join("gcc-ld").join("ld.lld");
if gcc_ld_lld.exists() {
return gcc_ld_lld.display().to_string();
}
let ld_lld = clang_dir.join("ld.lld");
if ld_lld.exists() {
return ld_lld.display().to_string();
}
}
}
let target_bin_dir = rustc_sysroot().join("lib").join("rustlib").join(target()).join("bin");
let gcc_ld_lld = target_bin_dir.join("gcc-ld").join("ld.lld");
if gcc_ld_lld.exists() {
return gcc_ld_lld.display().to_string();
}
"ld.lld".to_string()
}
fn run_and_expect_cfi_abort(target_dir: &Path, target: &str, binary: &str) {
let exe = target_dir.join(target).join("release").join(bin_name(binary));
let output = cmd(&exe).run_fail();
output
.assert_stdout_contains("With CFI enabled, you should not see the next answer")
.assert_stdout_not_contains("The next answer is:");
}
fn run_and_expect_cfi_not_abort(target_dir: &Path, target: &str, binary: &str) {
let exe = target_dir.join(target).join("release").join(bin_name(binary));
let output = cmd(&exe).run();
output.assert_stdout_contains("Hello from C!");
}
fn main() {
let clang = clang_path();
let fuse_ld = fuse_ld_path();
let tgt = target();
let target_dir = path("target");
let lib = std::env::var("LIB").unwrap_or_default();
let prior_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
let rustflags = format!(
"{prior_rustflags} -Clinker-plugin-lto -Clinker={clang} \
-Clink-arg=-fuse-ld={fuse_ld} -Zsanitizer=cfi \
-Ctarget-feature=-crt-static"
)
.trim()
.to_owned();
let rustflags_with_integer_normalization =
format!("{rustflags} -Zsanitizer-cfi-normalize-integers").trim().to_owned();
let run = |manifest: &Path, rustflags: &str, workspace: bool| {
let mut c = cargo();
c.arg("build")
.arg("--manifest-path")
.arg(manifest)
.arg("--release")
.arg("-Zbuild-std")
.arg("--target")
.arg(&tgt);
if workspace {
c.arg("--workspace");
}
c.env("RUSTFLAGS", rustflags)
.env("CC", &clang)
.env("CARGO_TARGET_DIR", &target_dir)
.env("RUSTC_BOOTSTRAP", "1")
.env("LIB", &lib)
.run();
};
run(Path::new("Cargo.toml"), &rustflags, true);
for bin in [
"invalid-branch-target-abort",
"indirect-arity-mismatch-abort",
"indirect-pointee-type-mismatch-abort",
"indirect-return-type-mismatch-abort",
"indirect-type-qualifier-mismatch-abort",
"indirect-type-mismatch-abort",
"cross-lang-cfi-types-crate-abort",
] {
run_and_expect_cfi_abort(&target_dir, &tgt, bin);
}
for bin in ["cross-lang-cfi-types-crate-not-abort"] {
run_and_expect_cfi_not_abort(&target_dir, &tgt, bin);
}
run(
Path::new("cross-lang-integer-normalization-abort/Cargo.toml"),
&rustflags_with_integer_normalization,
false,
);
run_and_expect_cfi_abort(&target_dir, &tgt, "cross-lang-integer-normalization-abort");
run(
Path::new("cross-lang-integer-normalization-not-abort/Cargo.toml"),
&rustflags_with_integer_normalization,
false,
);
run_and_expect_cfi_not_abort(&target_dir, &tgt, "cross-lang-integer-normalization-not-abort");
}
@@ -0,0 +1,73 @@
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
fn clang_path() -> PathBuf {
if let Ok(d) = env::var("LLVM_BIN_DIR") {
let clang = Path::new(d.trim_end_matches('/')).join("clang");
if clang.exists() {
return clang;
}
}
if let Ok(clang) = env::var("CLANG") {
let clang = Path::new(clang.trim_end_matches('/'));
if clang.exists() {
return clang.to_path_buf();
}
}
PathBuf::from("clang")
}
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 build_foo_static_lib(extra_flags: &[&str]) {
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 = clang_path();
let llvm_ar = llvm_ar_path();
let mut clang_args = vec!["-Wall", "-flto=thin", "-fsanitize=cfi"];
clang_args.extend_from_slice(extra_flags);
clang_args.extend_from_slice(&["-fvisibility=hidden", "-c", "-emit-llvm", "-o"]);
let st = Command::new(&clang)
.args(&clang_args)
.arg(&bc_path)
.arg(&c_src)
.status()
.unwrap_or_else(|e| panic!("failed to spawn `{}`: {e}", clang.display()));
assert!(st.success(), "`{}` failed with {st}", clang.display());
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");
println!("cargo:rerun-if-changed=../shared_build_rs.rs");
}
@@ -0,0 +1,13 @@
[package]
name = "cfi-types"
version = "0.0.8"
edition = "2021"
description = "CFI types for cross-language LLVM CFI support"
homepage = "https://github.com/rcvalle/rust-crate-cfi-types"
license = "MIT OR Apache-2.0"
repository = "https://github.com/rcvalle/rust-crate-cfi-types"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
@@ -0,0 +1,96 @@
//! 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.
//!
//! The use of these types are optional and are recommended for when enforcement
//! and explicitness of types used across the FFI boundary and no loss of
//! granularity for cross-language LLVM CFI are preferred.
//!
//! Alternatively, the `-Zsanitizer-cfi-normalize-integers` option may be used
//! with the Clang `-fsanitize-cfi-icall-experimental-normalize-integers` option
//! for cross-language LLVM CFI support.
#![feature(cfg_sanitizer_cfi)]
#![feature(cfi_encoding)]
#![allow(non_camel_case_types)]
/// CFI type equivalent to Rust's core::ffi::c_char type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "c")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_char(pub core::ffi::c_char);
/// CFI type equivalent to Rust's core::ffi::c_int type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "i")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_int(pub core::ffi::c_int);
/// CFI type equivalent to Rust's core::ffi::c_long type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "l")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_long(pub core::ffi::c_long);
/// CFI type equivalent to Rust's core::ffi::c_longlong type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "x")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_longlong(pub core::ffi::c_longlong);
/// CFI type equivalent to Rust's core::ffi::c_schar type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "a")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_schar(pub core::ffi::c_schar);
/// CFI type equivalent to Rust's core::ffi::c_short type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "s")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_short(pub core::ffi::c_short);
/// CFI type equivalent to Rust's core::ffi::c_uchar type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "h")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_uchar(pub core::ffi::c_uchar);
/// CFI type equivalent to Rust's core::ffi::c_uint type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "j")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_uint(pub core::ffi::c_uint);
/// CFI type equivalent to Rust's core::ffi::c_ulong type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "m")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_ulong(pub core::ffi::c_ulong);
/// CFI type equivalent to Rust's core::ffi::c_ulonglong type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "y")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_ulonglong(pub core::ffi::c_ulonglong);
/// CFI type equivalent to Rust's core::ffi::c_ushort type alias.
#[allow(dead_code)]
#[cfg_attr(not(sanitizer_cfi_normalize_integers), cfi_encoding = "t")]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
#[repr(transparent)]
pub struct c_ushort(pub core::ffi::c_ushort);