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