Skip to content

Commit 3e033bf

Browse files
committed
cfi: add diag mode support
Currently a Rust CFI failure only inserts a ud2. However, for clang we have the option for a helpful diagnostic message that explains the violation and is especially helpful for fixing it. This message works through hooking the UBSan runtime and calling into it with the necessary information for a helpful error message. In clang, this is enabled via -fno-sanitize-trap=cfi. This change adds the same behavior to rustc's CFI. Instead of a no-sanitize-trap flag, we added -Z cfi-mode={diag|trap}, with trap as the default. The diag mode will print the following error message for a violation: ``` tests/ui/sanitizer/cfi/fn-ptr-type-mismatch.rs:1:1: runtime error: control flow integrity check for type fn(i32, i32) -> i32 failed during indirect function call fn_ptr_type_mismatch.ecd806f409c5c1fc-cgu.0: note: fn_ptr_type_mismatch::add_one defined here SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior tests/ui/sanitizer/cfi/fn-ptr-type-mismatch.rs:1:1 ```
1 parent ab26b17 commit 3e033bf

7 files changed

Lines changed: 215 additions & 24 deletions

File tree

‎compiler/rustc_codegen_llvm/src/builder.rs‎

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use crate::type_of::LayoutLlvmExt;
4343
pub(crate) struct GenericBuilder<'a, 'll, CX: Borrow<SCx<'ll>>> {
4444
pub llbuilder: &'ll mut llvm::Builder<'ll>,
4545
pub cx: &'a GenericCx<'ll, CX>,
46+
pub span: rustc_span::Span,
4647
}
4748

4849
pub(crate) type SBuilder<'a, 'll> = GenericBuilder<'a, 'll, SCx<'ll>>;
@@ -93,7 +94,7 @@ impl<'a, 'll, CX: Borrow<SCx<'ll>>> GenericBuilder<'a, 'll, CX> {
9394
fn with_cx(scx: &'a GenericCx<'ll, CX>) -> Self {
9495
// Create a fresh builder from the simple context.
9596
let llbuilder = unsafe { llvm::LLVMCreateBuilderInContext(scx.deref().borrow().llcx) };
96-
GenericBuilder { llbuilder, cx: scx }
97+
GenericBuilder { llbuilder, cx: scx, span: rustc_span::DUMMY_SP }
9798
}
9899

99100
pub(crate) fn append_block(
@@ -303,7 +304,9 @@ impl<'a, 'll, 'tcx> BuilderMethods<'a, 'tcx> for Builder<'a, 'll, 'tcx> {
303304
unsafe { llvm::LLVMGetInsertBlock(self.llbuilder) }
304305
}
305306

306-
fn set_span(&mut self, _span: Span) {}
307+
fn set_span(&mut self, span: rustc_span::Span) {
308+
self.span = span;
309+
}
307310

308311
fn append_block(cx: &'a CodegenCx<'ll, 'tcx>, llfn: &'ll Value, name: &str) -> &'ll BasicBlock {
309312
unsafe {
@@ -1491,6 +1494,51 @@ impl<'a, 'll, 'tcx> Builder<'a, 'll, 'tcx> {
14911494
pub(crate) fn llfn(&self) -> &'ll Value {
14921495
unsafe { llvm::LLVMGetBasicBlockParent(self.llbb()) }
14931496
}
1497+
1498+
fn generate_ubsan_cfi_diag_data(
1499+
&mut self,
1500+
span: rustc_span::Span,
1501+
expected_ty: String,
1502+
check_kind: u8,
1503+
) -> &'ll Value {
1504+
let cx = self.cx();
1505+
let tcx = cx.tcx;
1506+
1507+
let loc = tcx.sess.source_map().lookup_char_pos(span.lo());
1508+
1509+
let filename_str = format!("{}\0", loc.file.name.prefer_local_unconditionally());
1510+
let filename_val = cx.const_bytes(filename_str.as_bytes());
1511+
let filename_ptr = cx.static_addr_of_impl(filename_val, Align::ONE, None);
1512+
1513+
// SourceLocation UBSan struct: { const char *filename, uint32_t line, uint32_t column }
1514+
let source_location = cx.const_struct(
1515+
&[
1516+
filename_ptr,
1517+
cx.const_u32(loc.line as u32),
1518+
// UBSan columns are 1-based
1519+
cx.const_u32(loc.col.0 as u32 + 1),
1520+
],
1521+
false, // packed = false
1522+
);
1523+
1524+
let ty_name = format!("{}\0", expected_ty);
1525+
let ty_name_val = cx.const_bytes(ty_name.as_bytes());
1526+
1527+
// TypeDescriptor UBSan struct: { uint16_t TypeKind, uint16_t TypeInfo, const char *TypeName }
1528+
let type_descriptor =
1529+
cx.const_struct(&[cx.const_i16(0xffffu16 as i16), cx.const_i16(0), ty_name_val], false);
1530+
1531+
let type_descriptor_ptr =
1532+
cx.static_addr_of_impl(type_descriptor, Align::from_bytes(2).unwrap(), None);
1533+
1534+
// CFICheckFailData UBSan struct: { uint8_t CheckKind, SourceLocation Loc, TypeDescriptor *Type }
1535+
let cfi_check_fail_data = cx
1536+
.const_struct(&[cx.const_u8(check_kind), source_location, type_descriptor_ptr], false);
1537+
let align = tcx.data_layout.aggregate_align;
1538+
1539+
// Returns the final opaque pointer to the struct to be passed to __ubsan_handle_cfi_check_fail
1540+
cx.static_addr_of_mut(cfi_check_fail_data, align, Some("__ubsan_cfi_check_fail_data"))
1541+
}
14941542
}
14951543

14961544
impl<'a, 'll, CX: Borrow<SCx<'ll>>> GenericBuilder<'a, 'll, CX> {
@@ -1905,8 +1953,55 @@ impl<'a, 'll, 'tcx> Builder<'a, 'll, 'tcx> {
19051953
if let Some(dbg_loc) = dbg_loc {
19061954
self.set_dbg_loc(dbg_loc);
19071955
}
1908-
self.abort();
1909-
self.unreachable();
1956+
1957+
match self.tcx.sess.opts.unstable_opts.cfi_mode {
1958+
rustc_session::config::CfiMode::Trap => {
1959+
self.abort();
1960+
self.unreachable();
1961+
}
1962+
rustc_session::config::CfiMode::Diag => {
1963+
let fty = self.cx.type_func(
1964+
&[self.cx.type_ptr(), self.cx.type_isize(), self.cx.type_isize()],
1965+
self.cx.type_void(),
1966+
);
1967+
let ubsan_handler = self.declare_cfn(
1968+
"__ubsan_handle_cfi_check_fail_abort",
1969+
llvm::UnnamedAddr::Global,
1970+
fty,
1971+
);
1972+
1973+
let mut expected_ty = String::from("fn(");
1974+
for (i, arg) in fn_abi.args.iter().enumerate() {
1975+
if i > 0 {
1976+
expected_ty.push_str(", ");
1977+
}
1978+
use std::fmt::Write;
1979+
write!(&mut expected_ty, "{}", arg.layout.ty).unwrap();
1980+
}
1981+
expected_ty.push(')');
1982+
if !fn_abi.ret.layout.ty.is_unit() {
1983+
use std::fmt::Write;
1984+
write!(&mut expected_ty, " -> {}", fn_abi.ret.layout.ty).unwrap();
1985+
}
1986+
1987+
// 4 for cfi-icall (indirect call)
1988+
let check_kind = 4;
1989+
let diag_data =
1990+
self.generate_ubsan_cfi_diag_data(self.span, expected_ty, check_kind);
1991+
1992+
let function_address = self.ptrtoint(llfn, self.cx.type_isize());
1993+
self.call(
1994+
fty,
1995+
None,
1996+
None,
1997+
ubsan_handler,
1998+
&[diag_data, function_address, self.const_usize(0)],
1999+
None,
2000+
None,
2001+
);
2002+
self.unreachable();
2003+
}
2004+
}
19102005

19112006
self.switch_to_block(bb_pass);
19122007
if let Some(dbg_loc) = dbg_loc {

‎compiler/rustc_codegen_ssa/src/back/link.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use rustc_middle::middle::debugger_visualizer::DebuggerVisualizerFile;
3535
use rustc_middle::middle::dependency_format::Linkage;
3636
use rustc_middle::middle::exported_symbols::SymbolExportKind;
3737
use rustc_session::config::{
38-
self, CFGuard, CrateType, DebugInfo, LinkerFeaturesCli, OutFileName, OutputFilenames,
38+
self, CFGuard, CfiMode, CrateType, DebugInfo, LinkerFeaturesCli, OutFileName, OutputFilenames,
3939
OutputType, PrintKind, SplitDwarfKind, Strip,
4040
};
4141
use rustc_session::lint::builtin::LINKER_MESSAGES;
@@ -1457,6 +1457,9 @@ fn add_sanitizer_libraries(
14571457
if sanitizer.contains(SanitizerSet::REALTIME) {
14581458
link_sanitizer_runtime(sess, flavor, linker, "rtsan");
14591459
}
1460+
if sanitizer.contains(SanitizerSet::CFI) && sess.opts.unstable_opts.cfi_mode == CfiMode::Diag {
1461+
link_sanitizer_runtime(sess, flavor, linker, "ubsan");
1462+
}
14601463
}
14611464

14621465
fn link_sanitizer_runtime(

‎compiler/rustc_session/src/config.rs‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ pub enum CFProtection {
8888
Full,
8989
}
9090

91+
/// The different settings that the `-Z cfi-mode` flag can have.
92+
#[derive(Clone, Copy, PartialEq, Hash, Debug)]
93+
pub enum CfiMode {
94+
/// Emit a trap instruction on CFI violation (default).
95+
Trap,
96+
97+
/// Emit a diagnostic on CFI violation instead of trapping.
98+
Diag,
99+
}
100+
91101
#[derive(Clone, Copy, Debug, PartialEq, Hash, StableHash, Encodable, Decodable)]
92102
pub enum OptLevel {
93103
/// `-Copt-level=0`
@@ -3062,14 +3072,14 @@ pub(crate) mod dep_tracking {
30623072
};
30633073

30643074
use super::{
3065-
AnnotateMoves, AutoDiff, BranchProtection, CFGuard, CFProtection, CodegenRetagOptions,
3066-
CoverageOptions, CrateType, DebugInfo, DebugInfoCompression, ErrorOutputType, FmtDebug,
3067-
FunctionReturn, InliningThreshold, InstrumentCoverage, InstrumentXRay, LinkerPluginLto,
3068-
LocationDetail, LtoCli, MirStripDebugInfo, NextSolverConfig, Offload, OptLevel,
3069-
OutFileName, OutputType, OutputTypes, PatchableFunctionEntry, Polonius, ResolveDocLinks,
3070-
SourceFileHashAlgorithm, SplitDwarfKind, SwitchWithOptPath, SymbolManglingVersion,
3071-
WasiExecModel,
3075+
AnnotateMoves, AutoDiff, BranchProtection, CFGuard, CFProtection, CfiMode, CoverageOptions,
3076+
CrateType, DebugInfo, DebugInfoCompression, ErrorOutputType, FmtDebug, FunctionReturn,
3077+
InliningThreshold, InstrumentCoverage, InstrumentXRay, LinkerPluginLto, LocationDetail,
3078+
LtoCli, MirStripDebugInfo, NextSolverConfig, Offload, OptLevel, OutFileName, OutputType,
3079+
OutputTypes, PatchableFunctionEntry, Polonius, ResolveDocLinks, SourceFileHashAlgorithm,
3080+
SplitDwarfKind, SwitchWithOptPath, SymbolManglingVersion, WasiExecModel,
30723081
};
3082+
use crate::config::CodegenRetagOptions;
30733083
use crate::lint;
30743084
use crate::utils::NativeLib;
30753085

@@ -3147,6 +3157,7 @@ pub(crate) mod dep_tracking {
31473157
SanitizerSet,
31483158
CFGuard,
31493159
CFProtection,
3160+
CfiMode,
31503161
TargetTuple,
31513162
Edition,
31523163
LinkerPluginLto,

‎compiler/rustc_session/src/options.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,7 @@ mod desc {
773773
pub(crate) const parse_cfguard: &str =
774774
"either a boolean (`yes`, `no`, `on`, `off`, etc), `checks`, or `nochecks`";
775775
pub(crate) const parse_cfprotection: &str = "`none`|`no`|`n` (default), `branch`, `return`, or `full`|`yes`|`y` (equivalent to `branch` and `return`)";
776+
pub(crate) const parse_cfi_mode: &str = "`trap` (default) or `diag`";
776777
pub(crate) const parse_debuginfo: &str = "either an integer (0, 1, 2), `none`, `line-directives-only`, `line-tables-only`, `limited`, or `full`";
777778
pub(crate) const parse_debuginfo_compression: &str = "one of `none`, `zlib`, or `zstd`";
778779
pub(crate) const parse_mir_strip_debuginfo: &str =
@@ -1307,6 +1308,15 @@ pub mod parse {
13071308
true
13081309
}
13091310

1311+
pub(crate) fn parse_cfi_mode(slot: &mut CfiMode, v: Option<&str>) -> bool {
1312+
*slot = match v {
1313+
Some("trap") => CfiMode::Trap,
1314+
Some("diag") => CfiMode::Diag,
1315+
_ => return false,
1316+
};
1317+
true
1318+
}
1319+
13101320
pub(crate) fn parse_debuginfo(slot: &mut DebugInfo, v: Option<&str>) -> bool {
13111321
match v {
13121322
Some("0") | Some("none") => *slot = DebugInfo::None,
@@ -2261,6 +2271,8 @@ options! {
22612271
"cache the results of derive proc macro invocations (potentially unsound!) (default: no"),
22622272
cf_protection: CFProtection = (CFProtection::None, parse_cfprotection, [TRACKED],
22632273
"instrument control-flow architecture protection"),
2274+
cfi_mode: CfiMode = (CfiMode::Trap, parse_cfi_mode, [TRACKED],
2275+
"set the CFI failure mode: `trap` (default) or `diag`"),
22642276
check_cfg_all_expected: bool = (false, parse_bool, [UNTRACKED],
22652277
"show all expected values in check-cfg diagnostics (default: no)"),
22662278
checksum_hash_algorithm: Option<SourceFileHashAlgorithm> = (None, parse_cargo_src_file_hash, [TRACKED],

‎src/bootstrap/src/core/build_steps/llvm.rs‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,21 +1520,28 @@ fn supported_sanitizers(
15201520
let darwin_libs = |os: &str, components: &[&str]| -> Vec<SanitizerRuntime> {
15211521
components
15221522
.iter()
1523-
.map(move |c| SanitizerRuntime {
1524-
cmake_target: format!("clang_rt.{c}_{os}_dynamic"),
1525-
path: out_dir.join(format!("build/lib/darwin/libclang_rt.{c}_{os}_dynamic.dylib")),
1526-
name: format!("librustc-{channel}_rt.{c}.dylib"),
1523+
.map(move |c| {
1524+
let cmake_c = if *c == "ubsan" { "ubsan_standalone" } else { *c };
1525+
SanitizerRuntime {
1526+
cmake_target: format!("clang_rt.{cmake_c}_{os}_dynamic"),
1527+
path: out_dir
1528+
.join(format!("build/lib/darwin/libclang_rt.{cmake_c}_{os}_dynamic.dylib")),
1529+
name: format!("librustc-{channel}_rt.{c}.dylib"),
1530+
}
15271531
})
15281532
.collect()
15291533
};
15301534

15311535
let common_libs = |os: &str, arch: &str, components: &[&str]| -> Vec<SanitizerRuntime> {
15321536
components
15331537
.iter()
1534-
.map(move |c| SanitizerRuntime {
1535-
cmake_target: format!("clang_rt.{c}-{arch}"),
1536-
path: out_dir.join(format!("build/lib/{os}/libclang_rt.{c}-{arch}.a")),
1537-
name: format!("librustc-{channel}_rt.{c}.a"),
1538+
.map(move |c| {
1539+
let cmake_c = if *c == "ubsan" { "ubsan_standalone" } else { *c };
1540+
SanitizerRuntime {
1541+
cmake_target: format!("clang_rt.{cmake_c}-{arch}"),
1542+
path: out_dir.join(format!("build/lib/{os}/libclang_rt.{cmake_c}-{arch}.a")),
1543+
name: format!("librustc-{channel}_rt.{c}.a"),
1544+
}
15381545
})
15391546
.collect()
15401547
};
@@ -1545,9 +1552,11 @@ fn supported_sanitizers(
15451552
"aarch64-apple-ios-sim" => darwin_libs("iossim", &["asan", "tsan", "rtsan"]),
15461553
"aarch64-apple-ios-macabi" => darwin_libs("osx", &["asan", "lsan", "tsan"]),
15471554
"aarch64-unknown-fuchsia" => common_libs("fuchsia", "aarch64", &["asan"]),
1548-
"aarch64-unknown-linux-gnu" => {
1549-
common_libs("linux", "aarch64", &["asan", "lsan", "msan", "tsan", "hwasan", "rtsan"])
1550-
}
1555+
"aarch64-unknown-linux-gnu" => common_libs(
1556+
"linux",
1557+
"aarch64",
1558+
&["asan", "lsan", "msan", "tsan", "hwasan", "rtsan", "ubsan"],
1559+
),
15511560
"aarch64-unknown-linux-ohos" => {
15521561
common_libs("linux", "aarch64", &["asan", "lsan", "msan", "tsan", "hwasan"])
15531562
}
@@ -1567,7 +1576,7 @@ fn supported_sanitizers(
15671576
"x86_64-unknown-linux-gnu" => common_libs(
15681577
"linux",
15691578
"x86_64",
1570-
&["asan", "dfsan", "lsan", "msan", "safestack", "tsan", "rtsan"],
1579+
&["asan", "dfsan", "lsan", "msan", "safestack", "tsan", "rtsan", "ubsan"],
15711580
),
15721581
"x86_64-unknown-linux-gnuasan" => common_libs("linux", "x86_64", &["asan"]),
15731582
"x86_64-unknown-linux-gnumsan" => common_libs("linux", "x86_64", &["msan"]),
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Verifies that pointer type membership tests for indirect calls are emitted.
2+
//
3+
//@ needs-sanitizer-cfi
4+
//@ compile-flags: -Clto -Cno-prepopulate-passes -Ctarget-feature=-crt-static -Zsanitizer=cfi -Zcfi-mode=diag -Copt-level=0 -C unsafe-allow-abi-mismatch=sanitizer
5+
6+
#![crate_type = "lib"]
7+
8+
pub fn foo(f: fn(i32) -> i32, arg: i32) -> i32 {
9+
// CHECK-LABEL: define{{.*}}foo{{.*}}!type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}}
10+
// CHECK: start:
11+
// CHECK: [[TT:%.+]] = call i1 @llvm.type.test(ptr {{%f|%0}}, metadata !"{{[[:print:]]+}}")
12+
// CHECK-NEXT: br i1 [[TT]], label %type_test.pass, label %type_test.fail
13+
// CHECK: type_test.pass:
14+
// CHECK-NEXT: {{%.+}} = call i32 %f(i32{{.*}} %arg)
15+
// CHECK: type_test.fail:
16+
// CHECK-NEXT: {{%.+}} = ptrtoint ptr {{%f|%0}} to i64
17+
// CHECK-NEXT: call void @__ubsan_handle_cfi_check_fail_abort(
18+
// CHECK-NEXT: unreachable
19+
f(arg)
20+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Verifies that calling a function pointer with a mismatched type triggers a
2+
// CFI violation and causes the process to trap.
3+
4+
//@ revisions: cfi kcfi
5+
// FIXME(#122848) Remove only-linux once OSX CFI binaries work
6+
//@ only-linux
7+
//@ ignore-backends: gcc
8+
//@ [cfi] needs-sanitizer-cfi
9+
//@ [cfi] needs-sanitizer-support
10+
//@ [kcfi] needs-sanitizer-kcfi
11+
//@ compile-flags: -C target-feature=-crt-static
12+
//@ compile-flags: -C unsafe-allow-abi-mismatch=sanitizer
13+
//@ [cfi] compile-flags: -C opt-level=0 -C codegen-units=1 -C lto
14+
//@ [cfi] compile-flags: -C prefer-dynamic=off
15+
//@ [cfi] compile-flags: -Z sanitizer=cfi
16+
//@ [cfi] compile-flags: -Z cfi-mode=diag
17+
//@ [kcfi] compile-flags: -Z sanitizer=kcfi
18+
//@ [kcfi] compile-flags: -C panic=abort -C prefer-dynamic=off
19+
//@ run-fail-or-crash
20+
21+
use std::hint::black_box;
22+
use std::mem;
23+
24+
fn add_one(x: i32) -> i32 {
25+
x + 1
26+
}
27+
28+
// Accept a function pointer as a parameter so that the indirect call cannot
29+
// be devirtualized by the compiler.
30+
#[inline(never)]
31+
fn call_with_mismatch(f: fn(i32) -> i32) {
32+
// Transmute fn(i32) -> i32 into fn(i32, i32) -> i32, creating a
33+
// function pointer type mismatch that CFI should catch.
34+
let g: fn(i32, i32) -> i32 = unsafe { mem::transmute(f) };
35+
// This indirect call should fail the CFI type check and trap.
36+
let _result = g(1, 2);
37+
}
38+
39+
fn main() {
40+
call_with_mismatch(black_box(add_one));
41+
}

0 commit comments

Comments
 (0)