Skip to main content

rustc_mir_transform/
abort_unwinding_calls.rs

1use rustc_abi::ExternAbi;
2use rustc_ast::InlineAsmOptions;
3use rustc_middle::mir::*;
4use rustc_middle::span_bug;
5use rustc_middle::ty::{self, TyCtxt, layout};
6use rustc_span::sym;
7use rustc_target::spec::PanicStrategy;
8
9use crate::PassPolicy;
10
11/// A pass that runs which is targeted at ensuring that codegen guarantees about
12/// unwinding are upheld for compilations of panic=abort programs.
13///
14/// When compiling with panic=abort codegen backends generally want to assume
15/// that all Rust-defined functions do not unwind, and it's UB if they actually
16/// do unwind. Foreign functions, however, can be declared as "may unwind" via
17/// their ABI (e.g. `extern "C-unwind"`). To uphold the guarantees that
18/// Rust-defined functions never unwind a well-behaved Rust program needs to
19/// catch unwinding from foreign functions and force them to abort.
20///
21/// This pass walks over all functions calls which may possibly unwind,
22/// and if any are found sets their cleanup to a block that aborts the process.
23/// This forces all unwinds, in panic=abort mode happening in foreign code, to
24/// trigger a process abort.
25#[derive(PartialEq)]
26pub(super) struct AbortUnwindingCalls;
27
28impl<'tcx> crate::MirPass<'tcx> for AbortUnwindingCalls {
29    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
30        let def_id = body.source.def_id();
31        let kind = tcx.def_kind(def_id);
32
33        // We don't simplify the MIR of constants at this time because that
34        // namely results in a cyclic query when we call `tcx.type_of` below.
35        if !kind.is_fn_like() {
36            return;
37        }
38
39        // Represent whether this compilation target fundamentally doesn't
40        // support unwinding at all at an ABI level. If this the target has no
41        // support for unwinding then cleanup actions, for example, are all
42        // unnecessary and can be considered unreachable.
43        //
44        // Currently this is only true for wasm targets on panic=abort when the
45        // `exception-handling` target feature is disabled. In such a
46        // configuration it's illegal to emit exception-related instructions so
47        // it's not possible to unwind.
48        let target_supports_unwinding = !(tcx.sess.target.is_like_wasm
49            && tcx.sess.panic_strategy() == PanicStrategy::Abort
50            && !tcx.asm_target_features(def_id).contains(&sym::exception_handling));
51
52        // Here we test for this function itself whether its ABI allows
53        // unwinding or not.
54        let body_ty = tcx.type_of(def_id).skip_binder();
55        let body_abi = match body_ty.kind() {
56            ty::FnDef(..) => body_ty.fn_sig(tcx).abi(),
57            ty::Closure(..) => ExternAbi::RustCall,
58            ty::CoroutineClosure(..) => ExternAbi::RustCall,
59            ty::Coroutine(..) => ExternAbi::Rust,
60            ty::Error(_) => return,
61            _ => span_bug!(body.span, "unexpected body ty: {:?}", body_ty),
62        };
63        let body_can_unwind = layout::fn_can_unwind(tcx, Some(def_id), body_abi);
64
65        // Look in this function body for any basic blocks which are terminated
66        // with a function call, and whose function we're calling may unwind.
67        // This will filter to functions with `extern "C-unwind"` ABIs, for
68        // example.
69        for block in body.basic_blocks.as_mut() {
70            let Some(terminator) = &mut block.terminator else { continue };
71            let span = terminator.source_info.span;
72
73            // If we see an `UnwindResume` terminator inside a function then:
74            //
75            // * If the target doesn't support unwinding at all, then this is an
76            //   unreachable block.
77            // * If the body cannot unwind, we need to replace it with
78            //   `UnwindTerminate`.
79            if let TerminatorKind::UnwindResume = &terminator.kind {
80                if !target_supports_unwinding {
81                    terminator.kind = TerminatorKind::Unreachable;
82                } else if !body_can_unwind {
83                    terminator.kind = TerminatorKind::UnwindTerminate(UnwindTerminateReason::Abi);
84                }
85            }
86
87            if block.is_cleanup {
88                continue;
89            }
90
91            let call_can_unwind = match &terminator.kind {
92                TerminatorKind::Call { func, .. } => {
93                    let ty = func.ty(&body.local_decls, tcx);
94                    let sig = ty.fn_sig(tcx);
95                    let fn_def_id = match ty.kind() {
96                        ty::FnPtr(..) => None,
97                        &ty::FnDef(def_id, _) => Some(def_id),
98                        _ => span_bug!(span, "invalid callee of type {:?}", ty),
99                    };
100                    layout::fn_can_unwind(tcx, fn_def_id, sig.abi())
101                }
102                TerminatorKind::Drop { .. } => {
103                    tcx.sess.opts.unstable_opts.panic_in_drop == PanicStrategy::Unwind
104                        && layout::fn_can_unwind(tcx, None, ExternAbi::Rust)
105                }
106                TerminatorKind::Assert { .. } | TerminatorKind::FalseUnwind { .. } => {
107                    layout::fn_can_unwind(tcx, None, ExternAbi::Rust)
108                }
109                TerminatorKind::InlineAsm { options, .. } => {
110                    options.contains(InlineAsmOptions::MAY_UNWIND)
111                }
112                _ if terminator.unwind().is_some() => {
113                    span_bug!(span, "unexpected terminator that may unwind {:?}", terminator)
114                }
115                _ => continue,
116            };
117
118            if !call_can_unwind || !target_supports_unwinding {
119                // If this function call can't unwind, or if the target doesn't
120                // support unwinding at all, then there's no need for it
121                // to have a landing pad. This means that we can remove any cleanup
122                // registered for it (and turn it into `UnwindAction::Unreachable`).
123                let cleanup = block.terminator_mut().unwind_mut().unwrap();
124                *cleanup = UnwindAction::Unreachable;
125            } else if !body_can_unwind
126                && matches!(terminator.unwind(), Some(UnwindAction::Continue))
127            {
128                // Otherwise if this function can unwind, then if the outer function
129                // can also unwind there's nothing to do. If the outer function
130                // can't unwind, however, we need to ensure that any `UnwindAction::Continue`
131                // is replaced with terminate. For those with `UnwindAction::Cleanup`,
132                // cleanup will still happen, and terminate will happen afterwards handled by
133                // the `UnwindResume` -> `UnwindTerminate` terminator replacement.
134                let cleanup = block.terminator_mut().unwind_mut().unwrap();
135                *cleanup = UnwindAction::Terminate(UnwindTerminateReason::Abi);
136            }
137        }
138
139        // We may have invalidated some `cleanup` blocks so clean those up now.
140        super::simplify::remove_dead_blocks(body);
141    }
142
143    fn policy(&self, _sess: &rustc_session::Session) -> PassPolicy {
144        // Implements part of MIR semantics, turning effectively implicit aborts into explicit
145        // ones.
146        PassPolicy::Required
147    }
148}