use rustc_abi::ExternAbi; use rustc_ast::InlineAsmOptions; use rustc_middle::mir::*; use rustc_middle::span_bug; use rustc_middle::ty::{self, TyCtxt, layout}; use rustc_span::sym; use rustc_target::spec::PanicStrategy; /// A pass that runs which is targeted at ensuring that codegen guarantees about /// unwinding are upheld for compilations of panic=abort programs. /// /// When compiling with panic=abort codegen backends generally want to assume /// that all Rust-defined functions do not unwind, and it's UB if they actually /// do unwind. Foreign functions, however, can be declared as "may unwind" via /// their ABI (e.g. `extern "C-unwind"`). To uphold the guarantees that /// Rust-defined functions never unwind a well-behaved Rust program needs to /// catch unwinding from foreign functions and force them to abort. /// /// This pass walks over all functions calls which may possibly unwind, /// and if any are found sets their cleanup to a block that aborts the process. /// This forces all unwinds, in panic=abort mode happening in foreign code, to /// trigger a process abort. #[derive(PartialEq)] pub(super) struct AbortUnwindingCalls; impl<'tcx> crate::MirPass<'tcx> for AbortUnwindingCalls { fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { let def_id = body.source.def_id(); let kind = tcx.def_kind(def_id); // We don't simplify the MIR of constants at this time because that // namely results in a cyclic query when we call `tcx.type_of` below. if !kind.is_fn_like() { return; } // Represent whether this compilation target fundamentally doesn't // support unwinding at all at an ABI level. If this the target has no // support for unwinding then cleanup actions, for example, are all // unnecessary and can be considered unreachable. // // Currently this is only true for wasm targets on panic=abort when the // `exception-handling` target feature is disabled. In such a // configuration it's illegal to emit exception-related instructions so // it's not possible to unwind. let target_supports_unwinding = !(tcx.sess.target.is_like_wasm && tcx.sess.panic_strategy() == PanicStrategy::Abort && !tcx.asm_target_features(def_id).contains(&sym::exception_handling)); // Here we test for this function itself whether its ABI allows // unwinding or not. let body_ty = tcx.type_of(def_id).skip_binder(); let body_abi = match body_ty.kind() { ty::FnDef(..) => body_ty.fn_sig(tcx).abi(), ty::Closure(..) => ExternAbi::RustCall, ty::CoroutineClosure(..) => ExternAbi::RustCall, ty::Coroutine(..) => ExternAbi::Rust, ty::Error(_) => return, _ => span_bug!(body.span, "unexpected body ty: {:?}", body_ty), }; let body_can_unwind = layout::fn_can_unwind(tcx, Some(def_id), body_abi); // Look in this function body for any basic blocks which are terminated // with a function call, and whose function we're calling may unwind. // This will filter to functions with `extern "C-unwind"` ABIs, for // example. for block in body.basic_blocks.as_mut() { let Some(terminator) = &mut block.terminator else { continue }; let span = terminator.source_info.span; // If we see an `UnwindResume` terminator inside a function then: // // * If the target doesn't support unwinding at all, then this is an // unreachable block. // * If the body cannot unwind, we need to replace it with // `UnwindTerminate`. if let TerminatorKind::UnwindResume = &terminator.kind { if !target_supports_unwinding { terminator.kind = TerminatorKind::Unreachable; } else if !body_can_unwind { terminator.kind = TerminatorKind::UnwindTerminate(UnwindTerminateReason::Abi); } } if block.is_cleanup { continue; } let call_can_unwind = match &terminator.kind { TerminatorKind::Call { func, .. } => { let ty = func.ty(&body.local_decls, tcx); let sig = ty.fn_sig(tcx); let fn_def_id = match ty.kind() { ty::FnPtr(..) => None, &ty::FnDef(def_id, _) => Some(def_id), _ => span_bug!(span, "invalid callee of type {:?}", ty), }; layout::fn_can_unwind(tcx, fn_def_id, sig.abi()) } TerminatorKind::Drop { .. } => { tcx.sess.opts.unstable_opts.panic_in_drop == PanicStrategy::Unwind && layout::fn_can_unwind(tcx, None, ExternAbi::Rust) } TerminatorKind::Assert { .. } | TerminatorKind::FalseUnwind { .. } => { layout::fn_can_unwind(tcx, None, ExternAbi::Rust) } TerminatorKind::InlineAsm { options, .. } => { options.contains(InlineAsmOptions::MAY_UNWIND) } _ if terminator.unwind().is_some() => { span_bug!(span, "unexpected terminator that may unwind {:?}", terminator) } _ => continue, }; if !call_can_unwind || !target_supports_unwinding { // If this function call can't unwind, or if the target doesn't // support unwinding at all, then there's no need for it // to have a landing pad. This means that we can remove any cleanup // registered for it (and turn it into `UnwindAction::Unreachable`). let cleanup = block.terminator_mut().unwind_mut().unwrap(); *cleanup = UnwindAction::Unreachable; } else if !body_can_unwind && matches!(terminator.unwind(), Some(UnwindAction::Continue)) { // Otherwise if this function can unwind, then if the outer function // can also unwind there's nothing to do. If the outer function // can't unwind, however, we need to ensure that any `UnwindAction::Continue` // is replaced with terminate. For those with `UnwindAction::Cleanup`, // cleanup will still happen, and terminate will happen afterwards handled by // the `UnwindResume` -> `UnwindTerminate` terminator replacement. let cleanup = block.terminator_mut().unwind_mut().unwrap(); *cleanup = UnwindAction::Terminate(UnwindTerminateReason::Abi); } } // We may have invalidated some `cleanup` blocks so clean those up now. super::simplify::remove_dead_blocks(body); } fn is_required(&self) -> bool { true } }