rustc_mir_transform/ffi_unwind_calls.rs
1use rustc_abi::ExternAbi;
2use rustc_ast::InlineAsmOptions;
3use rustc_hir::def_id::{LOCAL_CRATE, LocalDefId};
4use rustc_middle::mir::*;
5use rustc_middle::query::{LocalCrate, Providers};
6use rustc_middle::ty::{self, TyCtxt, layout};
7use rustc_middle::{bug, span_bug};
8use rustc_session::lint::builtin::FFI_UNWIND_CALLS;
9use rustc_target::spec::PanicStrategy;
10use tracing::debug;
11
12use crate::errors;
13
14// Check if the body of this def_id can possibly leak a foreign unwind into Rust code.
15fn has_ffi_unwind_calls(tcx: TyCtxt<'_>, local_def_id: LocalDefId) -> bool {
16 debug!("has_ffi_unwind_calls({local_def_id:?})");
17
18 // Only perform check on functions because constants cannot call FFI functions.
19 let def_id = local_def_id.to_def_id();
20 let kind = tcx.def_kind(def_id);
21 if !kind.is_fn_like() {
22 return false;
23 }
24
25 let body = &*tcx.mir_built(local_def_id).borrow();
26
27 let body_ty = tcx.type_of(def_id).skip_binder();
28 let body_abi = match body_ty.kind() {
29 ty::FnDef(..) => body_ty.fn_sig(tcx).abi(),
30 ty::Closure(..) => ExternAbi::RustCall,
31 ty::CoroutineClosure(..) => ExternAbi::RustCall,
32 ty::Coroutine(..) => ExternAbi::Rust,
33 ty::Error(_) => return false,
34 _ => span_bug!(body.span, "unexpected body ty: {:?}", body_ty),
35 };
36 let body_can_unwind = layout::fn_can_unwind(tcx, Some(def_id), body_abi);
37
38 // Foreign unwinds cannot leak past functions that themselves cannot unwind.
39 if !body_can_unwind {
40 return false;
41 }
42
43 let mut tainted = false;
44
45 for block in body.basic_blocks.iter() {
46 if block.is_cleanup {
47 continue;
48 }
49 let Some(terminator) = &block.terminator else { continue };
50
51 if let TerminatorKind::InlineAsm { options, .. } = &terminator.kind {
52 if options.contains(InlineAsmOptions::MAY_UNWIND) {
53 // We have detected an inline asm block that can possibly leak foreign unwind.
54 //
55 // Because the function body itself can unwind, we are not aborting this function call
56 // upon unwind, so this call can possibly leak foreign unwind into Rust code if the
57 // panic runtime linked is panic-abort.
58
59 let lint_root = body.source_scopes[terminator.source_info.scope]
60 .local_data
61 .as_ref()
62 .unwrap_crate_local()
63 .lint_root;
64 let span = terminator.source_info.span;
65
66 tcx.emit_node_span_lint(
67 FFI_UNWIND_CALLS,
68 lint_root,
69 span,
70 errors::AsmUnwindCall { span },
71 );
72
73 tainted = true;
74 }
75 continue;
76 }
77
78 let TerminatorKind::Call { func, .. } = &terminator.kind else { continue };
79
80 let ty = func.ty(body, tcx);
81 let sig = ty.fn_sig(tcx);
82
83 // Rust calls cannot themselves create foreign unwinds.
84 // We assume this is true for intrinsics as well.
85 if sig.abi().is_rustic_abi() {
86 continue;
87 };
88
89 let fn_def_id = match ty.kind() {
90 ty::FnPtr(..) => None,
91 &ty::FnDef(def_id, _) => {
92 // Rust calls cannot themselves create foreign unwinds (even if they use a non-Rust
93 // ABI). So the leak of the foreign unwind into Rust can only be elsewhere, not
94 // here.
95 if !tcx.is_foreign_item(def_id) {
96 continue;
97 }
98 Some(def_id)
99 }
100 _ => bug!("invalid callee of type {:?}", ty),
101 };
102
103 if layout::fn_can_unwind(tcx, fn_def_id, sig.abi()) {
104 // We have detected a call that can possibly leak foreign unwind.
105 //
106 // Because the function body itself can unwind, we are not aborting this function call
107 // upon unwind, so this call can possibly leak foreign unwind into Rust code if the
108 // panic runtime linked is panic-abort.
109
110 let lint_root = body.source_scopes[terminator.source_info.scope]
111 .local_data
112 .as_ref()
113 .unwrap_crate_local()
114 .lint_root;
115 let span = terminator.source_info.span;
116
117 let foreign = fn_def_id.is_some();
118 tcx.emit_node_span_lint(
119 FFI_UNWIND_CALLS,
120 lint_root,
121 span,
122 errors::FfiUnwindCall { span, foreign },
123 );
124
125 tainted = true;
126 }
127 }
128
129 tainted
130}
131
132fn required_panic_strategy(tcx: TyCtxt<'_>, _: LocalCrate) -> Option<PanicStrategy> {
133 let local_strategy = tcx.sess.panic_strategy();
134
135 if tcx.is_panic_runtime(LOCAL_CRATE) {
136 return Some(local_strategy);
137 }
138
139 match local_strategy {
140 PanicStrategy::Abort | PanicStrategy::ImmediateAbort => return Some(local_strategy),
141 _ => {}
142 }
143
144 for def_id in tcx.hir_body_owners() {
145 if tcx.has_ffi_unwind_calls(def_id) {
146 // Given that this crate is compiled in `-C panic=unwind`, the `AbortUnwindingCalls`
147 // MIR pass will not be run on FFI-unwind call sites, therefore a foreign exception
148 // can enter Rust through these sites.
149 //
150 // On the other hand, crates compiled with `-C panic=abort` expects that all Rust
151 // functions cannot unwind (whether it's caused by Rust panic or foreign exception),
152 // and this expectation mismatch can cause unsoundness (#96926).
153 //
154 // To address this issue, we enforce that if FFI-unwind calls are used in a crate
155 // compiled with `panic=unwind`, then the final panic strategy must be `panic=unwind`.
156 // This will ensure that no crates will have wrong unwindability assumption.
157 //
158 // It should be noted that it is okay to link `panic=unwind` into a `panic=abort`
159 // program if it contains no FFI-unwind calls. In such case foreign exception can only
160 // enter Rust in a `panic=abort` crate, which will lead to an abort. There will also
161 // be no exceptions generated from Rust, so the assumption which `panic=abort` crates
162 // make, that no Rust function can unwind, indeed holds for crates compiled with
163 // `panic=unwind` as well. In such case this function returns `None`, indicating that
164 // the crate does not require a particular final panic strategy, and can be freely
165 // linked to crates with either strategy (we need such ability for libstd and its
166 // dependencies).
167 return Some(PanicStrategy::Unwind);
168 }
169 }
170
171 // This crate can be linked with either runtime.
172 None
173}
174
175pub(crate) fn provide(providers: &mut Providers) {
176 *providers = Providers { has_ffi_unwind_calls, required_panic_strategy, ..*providers };
177}