Skip to content

Commit 994844b

Browse files
committed
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).
1 parent f53b654 commit 994844b

25 files changed

Lines changed: 862 additions & 9 deletions

File tree

compiler/rustc_codegen_ssa/src/back/write.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,22 +136,32 @@ impl ModuleConfig {
136136
let emit_obj = if !should_emit_obj {
137137
EmitObj::None
138138
} else if sess.target.obj_is_bitcode
139-
|| (sess.opts.cg.linker_plugin_lto.enabled() && !no_builtins)
139+
|| (sess.opts.cg.linker_plugin_lto.enabled()
140+
&& (!no_builtins || tcx.sess.is_sanitizer_cfi_enabled()))
140141
{
141142
// This case is selected if the target uses objects as bitcode, or
142143
// if linker plugin LTO is enabled. In the linker plugin LTO case
143144
// the assumption is that the final link-step will read the bitcode
144145
// and convert it to object code. This may be done by either the
145146
// native linker or rustc itself.
146147
//
147-
// Note, however, that the linker-plugin-lto requested here is
148-
// explicitly ignored for `#![no_builtins]` crates. These crates are
149-
// specifically ignored by rustc's LTO passes and wouldn't work if
150-
// loaded into the linker. These crates define symbols that LLVM
151-
// lowers intrinsics to, and these symbol dependencies aren't known
152-
// until after codegen. As a result any crate marked
153-
// `#![no_builtins]` is assumed to not participate in LTO and
154-
// instead goes on to generate object code.
148+
// By default this branch is skipped for `#![no_builtins]` crates so
149+
// they emit native object files (machine code), not LLVM bitcode
150+
// objects for the linker (see rust-lang/rust#146133).
151+
//
152+
// However, when LLVM CFI is enabled (`-Zsanitizer=cfi`), this
153+
// breaks LLVM's expected pipeline: LLVM emits `llvm.type.test`
154+
// intrinsics and related metadata that must be lowered by LLVM's
155+
// `LowerTypeTests` pass before instruction selection during
156+
// link-time LTO. Otherwise, `llvm.type.test` intrinsics and related
157+
// metadata are not lowered by LLVM's `LowerTypeTests` pass before
158+
// reaching the target backend, and LLVM may abort during codegen
159+
// (for example in SelectionDAG type legalization) (see
160+
// rust-lang/rust#142284).
161+
//
162+
// Therefore, with `-Clinker-plugin-lto` and `-Zsanitizer=cfi`, a
163+
// `#![no_builtins]` crate must still use rustc's `EmitObj::Bitcode`
164+
// path (and emit LLVM bitcode in the `.o` for linker-based LTO).
155165
EmitObj::Bitcode
156166
} else if need_bitcode_in_object(tcx) || sess.target.requires_lto {
157167
EmitObj::ObjectCode(BitcodeSection::Full)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Workspace mirroring the examples in <https://github.com/rcvalle/rust-cfi-examples>.
2+
[workspace]
3+
resolver = "2"
4+
members = [
5+
"invalid-branch-target-abort",
6+
"indirect-arity-mismatch-abort",
7+
"indirect-type-mismatch-abort",
8+
"cross-lang-cfi-types-crate-abort",
9+
"cross-lang-cfi-types-crate-not-abort",
10+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "cross-lang-cfi-types-crate-abort"
3+
version = "0.1.0"
4+
edition = "2021"
5+
build = "build.rs"
6+
7+
[dependencies]
8+
cfi-types = "0.0.5"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use std::env;
2+
use std::path::{Path, PathBuf};
3+
use std::process::Command;
4+
5+
fn llvm_ar_path() -> PathBuf {
6+
if let Ok(d) = env::var("LLVM_BIN_DIR") {
7+
let p = Path::new(d.trim_end_matches('/')).join("llvm-ar");
8+
if p.exists() {
9+
return p;
10+
}
11+
}
12+
if let Ok(clang) = env::var("CLANG") {
13+
let clang = Path::new(&clang);
14+
if let Some(parent) = clang.parent() {
15+
let candidate = parent.join("llvm-ar");
16+
if candidate.exists() {
17+
return candidate;
18+
}
19+
}
20+
}
21+
PathBuf::from("llvm-ar")
22+
}
23+
24+
fn main() {
25+
let out_dir = env::var("OUT_DIR").expect("OUT_DIR");
26+
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
27+
let c_src = Path::new(&manifest_dir).join("src/foo.c");
28+
let bc_path = Path::new(&out_dir).join("foo.bc");
29+
let a_path = Path::new(&out_dir).join("libfoo.a");
30+
31+
let clang =
32+
env::var("CC").or_else(|_| env::var("CLANG")).unwrap_or_else(|_| "clang".to_string());
33+
let llvm_ar = llvm_ar_path();
34+
35+
let st = Command::new(&clang)
36+
.args([
37+
"-Wall",
38+
"-flto=thin",
39+
"-fsanitize=cfi",
40+
"-fvisibility=hidden",
41+
"-c",
42+
"-emit-llvm",
43+
"-o",
44+
])
45+
.arg(&bc_path)
46+
.arg(&c_src)
47+
.status()
48+
.unwrap_or_else(|e| panic!("failed to spawn `{clang}`: {e}"));
49+
assert!(st.success(), "`{clang}` failed with {st}");
50+
51+
let st = Command::new(&llvm_ar)
52+
.args(["rcs", a_path.to_str().unwrap(), bc_path.to_str().unwrap()])
53+
.status()
54+
.unwrap_or_else(|e| panic!("failed to spawn `{}`: {e}", llvm_ar.display()));
55+
assert!(st.success(), "`{}` failed with {st}", llvm_ar.display());
56+
57+
println!("cargo:rustc-link-search=native={out_dir}");
58+
println!("cargo:rustc-link-lib=static=foo");
59+
println!("cargo:rerun-if-changed={}", c_src.display());
60+
println!("cargo:rerun-if-changed=build.rs");
61+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
int
2+
do_twice(int (*fn)(int), int arg)
3+
{
4+
return fn(arg) + fn(arg);
5+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// This example demonstrates redirecting control flow using an indirect
2+
// branch/call to a function with different return and parameter types than the
3+
// return type expected and arguments intended/passed at the call/branch site,
4+
// across the FFI boundary using the `cfi_types` crate for cross-language LLVM
5+
// CFI.
6+
7+
use cfi_types::{c_int, c_long};
8+
use std::mem;
9+
10+
#[link(name = "foo")]
11+
unsafe extern "C" {
12+
fn do_twice(f: unsafe extern "C" fn(c_int) -> c_int, arg: i32) -> i32;
13+
}
14+
15+
unsafe extern "C" fn add_one(x: c_int) -> c_int {
16+
c_int(x.0 + 1)
17+
}
18+
19+
unsafe extern "C" fn add_two(x: c_long) -> c_long {
20+
c_long(x.0 + 2)
21+
}
22+
23+
fn main() {
24+
let answer = unsafe { do_twice(add_one, 5) };
25+
26+
println!("The answer is: {}", answer);
27+
28+
println!("With CFI enabled, you should not see the next answer");
29+
let f: unsafe extern "C" fn(c_int) -> c_int = unsafe {
30+
mem::transmute::<*const u8, unsafe extern "C" fn(c_int) -> c_int>(add_two as *const u8)
31+
};
32+
let next_answer = unsafe { do_twice(f, 5) };
33+
34+
println!("The next answer is: {}", next_answer);
35+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "cross-lang-cfi-types-crate-not-abort"
3+
version = "0.1.0"
4+
edition = "2021"
5+
build = "build.rs"
6+
7+
[dependencies]
8+
cfi-types = "0.0.5"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use std::env;
2+
use std::path::{Path, PathBuf};
3+
use std::process::Command;
4+
5+
fn llvm_ar_path() -> PathBuf {
6+
if let Ok(d) = env::var("LLVM_BIN_DIR") {
7+
let p = Path::new(d.trim_end_matches('/')).join("llvm-ar");
8+
if p.exists() {
9+
return p;
10+
}
11+
}
12+
if let Ok(clang) = env::var("CLANG") {
13+
let clang = Path::new(&clang);
14+
if let Some(parent) = clang.parent() {
15+
let candidate = parent.join("llvm-ar");
16+
if candidate.exists() {
17+
return candidate;
18+
}
19+
}
20+
}
21+
PathBuf::from("llvm-ar")
22+
}
23+
24+
fn main() {
25+
let out_dir = env::var("OUT_DIR").expect("OUT_DIR");
26+
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
27+
let c_src = Path::new(&manifest_dir).join("src/foo.c");
28+
let bc_path = Path::new(&out_dir).join("foo.bc");
29+
let a_path = Path::new(&out_dir).join("libfoo.a");
30+
31+
let clang =
32+
env::var("CC").or_else(|_| env::var("CLANG")).unwrap_or_else(|_| "clang".to_string());
33+
let llvm_ar = llvm_ar_path();
34+
35+
let st = Command::new(&clang)
36+
.args([
37+
"-Wall",
38+
"-flto=thin",
39+
"-fsanitize=cfi",
40+
"-fvisibility=hidden",
41+
"-c",
42+
"-emit-llvm",
43+
"-o",
44+
])
45+
.arg(&bc_path)
46+
.arg(&c_src)
47+
.status()
48+
.unwrap_or_else(|e| panic!("failed to spawn `{clang}`: {e}"));
49+
assert!(st.success(), "`{clang}` failed with {st}");
50+
51+
let st = Command::new(&llvm_ar)
52+
.args(["rcs", a_path.to_str().unwrap(), bc_path.to_str().unwrap()])
53+
.status()
54+
.unwrap_or_else(|e| panic!("failed to spawn `{}`: {e}", llvm_ar.display()));
55+
assert!(st.success(), "`{}` failed with {st}", llvm_ar.display());
56+
57+
println!("cargo:rustc-link-search=native={out_dir}");
58+
println!("cargo:rustc-link-lib=static=foo");
59+
println!("cargo:rerun-if-changed={}", c_src.display());
60+
println!("cargo:rerun-if-changed=build.rs");
61+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
4+
// This definition has the type id "_ZTSFvlE".
5+
void
6+
hello_from_c(long arg)
7+
{
8+
printf("Hello from C!\n");
9+
}
10+
11+
// This definition has the type id "_ZTSFvPFvlElE"--this can be ignored for the
12+
// purposes of this example.
13+
void
14+
indirect_call_from_c(void (*fn)(long), long arg)
15+
{
16+
// This call site tests whether the destination pointer is a member of the
17+
// group derived from the same type id of the fn declaration, which has the
18+
// type id "_ZTSFvlE".
19+
//
20+
// Notice that since the test is at the call site and generated by Clang,
21+
// the type id used in the test is encoded by Clang.
22+
fn(arg);
23+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use cfi_types::c_long;
2+
3+
#[link(name = "foo")]
4+
extern "C" {
5+
// This declaration has the type id "_ZTSFvlE" because it uses the CFI types
6+
// for cross-language LLVM CFI support. The cfi_types crate provides a new
7+
// set of C types as user-defined types using the cfi_encoding attribute and
8+
// repr(transparent) to be used for cross-language LLVM CFI support. This
9+
// new set of C types allows the Rust compiler to identify and correctly
10+
// encode C types in extern "C" function types indirectly called across the
11+
// FFI boundary when CFI is enabled.
12+
fn hello_from_c(_: c_long);
13+
14+
// This declaration has the type id "_ZTSFvPFvlElE" because it uses the CFI
15+
// types for cross-language LLVM CFI support--this can be ignored for the
16+
// purposes of this example.
17+
fn indirect_call_from_c(f: unsafe extern "C" fn(c_long), arg: c_long);
18+
}
19+
20+
// This definition has the type id "_ZTSFvlE" because it uses the CFI types for
21+
// cross-language LLVM CFI support, similarly to the hello_from_c declaration
22+
// above.
23+
unsafe extern "C" fn hello_from_rust(_: c_long) {
24+
println!("Hello, world!");
25+
}
26+
27+
// This definition has the type id "_ZTSFvlE" because it uses the CFI types for
28+
// cross-language LLVM CFI support, similarly to the hello_from_c declaration
29+
// above.
30+
unsafe extern "C" fn hello_from_rust_again(_: c_long) {
31+
println!("Hello from Rust again!");
32+
}
33+
34+
// This definition would also have the type id "_ZTSFvPFvlElE" because it uses
35+
// the CFI types for cross-language LLVM CFI support, similarly to the
36+
// hello_from_c declaration above--this can be ignored for the purposes of this
37+
// example.
38+
fn indirect_call(f: unsafe extern "C" fn(c_long), arg: c_long) {
39+
// This indirect call site tests whether the destination pointer is a member
40+
// of the group derived from the same type id of the f declaration, which
41+
// has the type id "_ZTSFvlE" because it uses the CFI types for
42+
// cross-language LLVM CFI support, similarly to the hello_from_c
43+
// declaration above.
44+
unsafe { f(arg) }
45+
}
46+
47+
// This definition has the type id "_ZTSFvvE"--this can be ignored for the
48+
// purposes of this example.
49+
fn main() {
50+
// This demonstrates an indirect call within Rust-only code using the same
51+
// encoding for hello_from_rust and the test at the indirect call site at
52+
// indirect_call (i.e., "_ZTSFvlE").
53+
indirect_call(hello_from_rust, c_long(5));
54+
55+
// This demonstrates an indirect call across the FFI boundary with the Rust
56+
// compiler and Clang using the same encoding for hello_from_c and the test
57+
// at the indirect call site at indirect_call (i.e., "_ZTSFvlE").
58+
indirect_call(hello_from_c, c_long(5));
59+
60+
// This demonstrates an indirect call to a function passed as a callback
61+
// across the FFI boundary with the Rust compiler and Clang the same
62+
// encoding for the passed-callback declaration and the test at the indirect
63+
// call site at indirect_call_from_c (i.e., "_ZTSFvlE").
64+
unsafe {
65+
indirect_call_from_c(hello_from_rust_again, c_long(5));
66+
}
67+
}

0 commit comments

Comments
 (0)