Fix FFI-unwind unsoundness with mixed panic mode

This commit is contained in:
Gary Guo 2022-05-18 03:51:52 +01:00
parent 09d52bc5d4
commit 6ef2033884
11 changed files with 212 additions and 14 deletions

View File

@ -944,6 +944,7 @@ fn analysis(tcx: TyCtxt<'_>, (): ()) -> Result<()> {
if !tcx.sess.opts.debugging_opts.thir_unsafeck { if !tcx.sess.opts.debugging_opts.thir_unsafeck {
rustc_mir_transform::check_unsafety::check_unsafety(tcx, def_id); rustc_mir_transform::check_unsafety::check_unsafety(tcx, def_id);
} }
tcx.ensure().has_ffi_unwind_calls(def_id);
if tcx.hir().body_const_context(def_id).is_some() { if tcx.hir().body_const_context(def_id).is_some() {
tcx.ensure() tcx.ensure()

View File

@ -3230,6 +3230,7 @@ declare_lint_pass! {
UNEXPECTED_CFGS, UNEXPECTED_CFGS,
DEPRECATED_WHERE_CLAUSE_LOCATION, DEPRECATED_WHERE_CLAUSE_LOCATION,
TEST_UNSTABLE_LINT, TEST_UNSTABLE_LINT,
FFI_UNWIND_CALLS,
] ]
} }
@ -3895,3 +3896,42 @@ declare_lint! {
"this unstable lint is only for testing", "this unstable lint is only for testing",
@feature_gate = sym::test_unstable_lint; @feature_gate = sym::test_unstable_lint;
} }
declare_lint! {
/// The `ffi_unwind_calls` lint detects calls to foreign functions or function pointers with
/// `C-unwind` or other FFI-unwind ABIs.
///
/// ### Example
///
/// ```rust,ignore (need FFI)
/// #![feature(ffi_unwind_calls)]
/// #![feature(c_unwind)]
///
/// # mod impl {
/// # #[no_mangle]
/// # pub fn "C-unwind" fn foo() {}
/// # }
///
/// extern "C-unwind" {
/// fn foo();
/// }
///
/// fn bar() {
/// unsafe { foo(); }
/// let ptr: unsafe extern "C-unwind" fn() = foo;
/// unsafe { ptr(); }
/// }
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// For crates containing such calls, if they are compiled with `-C panic=unwind` then the
/// produced library cannot be linked with crates compiled with `-C panic=abort`. For crates
/// that desire this ability it is therefore necessary to avoid such calls.
pub FFI_UNWIND_CALLS,
Allow,
"call to foreign functions or function pointers with FFI-unwind ABI",
@feature_gate = sym::c_unwind;
}

View File

@ -744,7 +744,7 @@ impl<'a> CrateLoader<'a> {
if !data.is_panic_runtime() { if !data.is_panic_runtime() {
self.sess.err(&format!("the crate `{}` is not a panic runtime", name)); self.sess.err(&format!("the crate `{}` is not a panic runtime", name));
} }
if data.panic_strategy() != desired_strategy { if data.panic_strategy() != Some(desired_strategy) {
self.sess.err(&format!( self.sess.err(&format!(
"the crate `{}` does not have the panic \ "the crate `{}` does not have the panic \
strategy `{}`", strategy `{}`",

View File

@ -60,7 +60,6 @@ use rustc_middle::ty::TyCtxt;
use rustc_session::config::CrateType; use rustc_session::config::CrateType;
use rustc_session::cstore::CrateDepKind; use rustc_session::cstore::CrateDepKind;
use rustc_session::cstore::LinkagePreference::{self, RequireDynamic, RequireStatic}; use rustc_session::cstore::LinkagePreference::{self, RequireDynamic, RequireStatic};
use rustc_target::spec::PanicStrategy;
pub(crate) fn calculate(tcx: TyCtxt<'_>) -> Dependencies { pub(crate) fn calculate(tcx: TyCtxt<'_>) -> Dependencies {
tcx.sess tcx.sess
@ -367,7 +366,7 @@ fn verify_ok(tcx: TyCtxt<'_>, list: &[Linkage]) {
prev_name, cur_name prev_name, cur_name
)); ));
} }
panic_runtime = Some((cnum, tcx.panic_strategy(cnum))); panic_runtime = Some((cnum, tcx.panic_strategy(cnum).unwrap()));
} }
} }
@ -397,18 +396,14 @@ fn verify_ok(tcx: TyCtxt<'_>, list: &[Linkage]) {
if let Linkage::NotLinked = *linkage { if let Linkage::NotLinked = *linkage {
continue; continue;
} }
if desired_strategy == PanicStrategy::Abort {
continue;
}
let cnum = CrateNum::new(i + 1); let cnum = CrateNum::new(i + 1);
if tcx.is_compiler_builtins(cnum) { if tcx.is_compiler_builtins(cnum) {
continue; continue;
} }
let found_strategy = tcx.panic_strategy(cnum); if let Some(found_strategy) = tcx.panic_strategy(cnum) && desired_strategy != found_strategy {
if desired_strategy != found_strategy {
sess.err(&format!( sess.err(&format!(
"the crate `{}` is compiled with the \ "the crate `{}` requires \
panic strategy `{}` which is \ panic strategy `{}` which is \
incompatible with this crate's \ incompatible with this crate's \
strategy of `{}`", strategy of `{}`",

View File

@ -1759,7 +1759,7 @@ impl CrateMetadata {
self.dep_kind.with_lock(|dep_kind| *dep_kind = f(*dep_kind)) self.dep_kind.with_lock(|dep_kind| *dep_kind = f(*dep_kind))
} }
pub(crate) fn panic_strategy(&self) -> PanicStrategy { pub(crate) fn panic_strategy(&self) -> Option<PanicStrategy> {
self.root.panic_strategy self.root.panic_strategy
} }

View File

@ -665,7 +665,7 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
triple: tcx.sess.opts.target_triple.clone(), triple: tcx.sess.opts.target_triple.clone(),
hash: tcx.crate_hash(LOCAL_CRATE), hash: tcx.crate_hash(LOCAL_CRATE),
stable_crate_id: tcx.def_path_hash(LOCAL_CRATE.as_def_id()).stable_crate_id(), stable_crate_id: tcx.def_path_hash(LOCAL_CRATE.as_def_id()).stable_crate_id(),
panic_strategy: tcx.sess.panic_strategy(), panic_strategy: tcx.required_panic_strategy(()),
panic_in_drop_strategy: tcx.sess.opts.debugging_opts.panic_in_drop, panic_in_drop_strategy: tcx.sess.opts.debugging_opts.panic_in_drop,
edition: tcx.sess.edition(), edition: tcx.sess.edition(),
has_global_allocator: tcx.has_global_allocator(LOCAL_CRATE), has_global_allocator: tcx.has_global_allocator(LOCAL_CRATE),

View File

@ -217,7 +217,7 @@ pub(crate) struct CrateRoot {
extra_filename: String, extra_filename: String,
hash: Svh, hash: Svh,
stable_crate_id: StableCrateId, stable_crate_id: StableCrateId,
panic_strategy: PanicStrategy, panic_strategy: Option<PanicStrategy>,
panic_in_drop_strategy: PanicStrategy, panic_in_drop_strategy: PanicStrategy,
edition: Edition, edition: Edition,
has_global_allocator: bool, has_global_allocator: bool,

View File

@ -1365,9 +1365,16 @@ rustc_queries! {
desc { "query a crate is `#![profiler_runtime]`" } desc { "query a crate is `#![profiler_runtime]`" }
separate_provide_extern separate_provide_extern
} }
query panic_strategy(_: CrateNum) -> PanicStrategy { query has_ffi_unwind_calls(key: LocalDefId) -> bool {
desc { |tcx| "check if `{}` contains FFI-unwind calls", tcx.def_path_str(key.to_def_id()) }
cache_on_disk_if { true }
}
query required_panic_strategy(_: ()) -> Option<PanicStrategy> {
desc { "compute the required panic strategy for the current crate" }
}
query panic_strategy(_: CrateNum) -> Option<PanicStrategy> {
fatal_cycle fatal_cycle
desc { "query a crate's configured panic strategy" } desc { "query a crate's required panic strategy" }
separate_provide_extern separate_provide_extern
} }
query panic_in_drop_strategy(_: CrateNum) -> PanicStrategy { query panic_in_drop_strategy(_: CrateNum) -> PanicStrategy {

View File

@ -0,0 +1,148 @@
use rustc_hir::def::DefKind;
use rustc_hir::def_id::LocalDefId;
use rustc_middle::mir::*;
use rustc_middle::ty::layout;
use rustc_middle::ty::query::Providers;
use rustc_middle::ty::{self, TyCtxt};
use rustc_session::lint::builtin::FFI_UNWIND_CALLS;
use rustc_target::spec::abi::Abi;
use rustc_target::spec::PanicStrategy;
fn abi_can_unwind(abi: Abi) -> bool {
use Abi::*;
match abi {
C { unwind }
| System { unwind }
| Cdecl { unwind }
| Stdcall { unwind }
| Fastcall { unwind }
| Vectorcall { unwind }
| Thiscall { unwind }
| Aapcs { unwind }
| Win64 { unwind }
| SysV64 { unwind } => unwind,
PtxKernel
| Msp430Interrupt
| X86Interrupt
| AmdGpuKernel
| EfiApi
| AvrInterrupt
| AvrNonBlockingInterrupt
| CCmseNonSecureCall
| Wasm
| RustIntrinsic
| PlatformIntrinsic
| Unadjusted => false,
Rust | RustCall | RustCold => true,
}
}
// Check if the body of this def_id can possibly leak a foreign unwind into Rust code.
fn has_ffi_unwind_calls(tcx: TyCtxt<'_>, local_def_id: LocalDefId) -> bool {
debug!("has_ffi_unwind_calls({local_def_id:?})");
// Only perform check on functions because constants cannot call FFI functions.
let def_id = local_def_id.to_def_id();
let kind = tcx.def_kind(def_id);
let is_function = match kind {
DefKind::Fn | DefKind::AssocFn | DefKind::Ctor(..) => true,
_ => tcx.is_closure(def_id),
};
if !is_function {
return false;
}
let body = &*tcx.mir_built(ty::WithOptConstParam::unknown(local_def_id)).borrow();
let body_ty = tcx.type_of(def_id);
let body_abi = match body_ty.kind() {
ty::FnDef(..) => body_ty.fn_sig(tcx).abi(),
ty::Closure(..) => Abi::RustCall,
ty::Generator(..) => Abi::Rust,
_ => span_bug!(body.span, "unexpected body ty: {:?}", body_ty),
};
let body_can_unwind = layout::fn_can_unwind(tcx, Some(def_id), body_abi);
// Foreign unwinds cannot leak past functions that themselves cannot unwind.
if !body_can_unwind {
return false;
}
let mut tainted = false;
for block in body.basic_blocks() {
if block.is_cleanup {
continue;
}
let Some(terminator) = &block.terminator else { continue };
let TerminatorKind::Call { func, .. } = &terminator.kind else { continue };
let ty = func.ty(body, tcx);
let sig = ty.fn_sig(tcx);
// Rust calls cannot themselves create foreign unwinds.
if let Abi::Rust | Abi::RustCall | Abi::RustCold = sig.abi() {
continue;
};
let fn_def_id = match ty.kind() {
ty::FnPtr(_) => None,
&ty::FnDef(def_id, _) => {
// Rust calls cannot themselves create foreign unwinds.
if !tcx.is_foreign_item(def_id) {
continue;
}
Some(def_id)
}
_ => bug!("invalid callee of type {:?}", ty),
};
if layout::fn_can_unwind(tcx, fn_def_id, sig.abi()) && abi_can_unwind(sig.abi()) {
// We have detected a call that can possibly leak foreign unwind.
//
// Because the function body itself can unwind, we are not aborting this function call
// upon unwind, so this call can possibly leak foreign unwind into Rust code if the
// panic runtime linked is panic-abort.
let lint_root = body.source_scopes[terminator.source_info.scope]
.local_data
.as_ref()
.assert_crate_local()
.lint_root;
let span = terminator.source_info.span;
tcx.struct_span_lint_hir(FFI_UNWIND_CALLS, lint_root, span, |lint| {
let msg = match fn_def_id {
Some(_) => "call to foreign function with FFI-unwind ABI",
None => "call to function pointer with FFI-unwind ABI",
};
let mut db = lint.build(msg);
db.span_label(span, msg);
db.emit();
});
tainted = true;
}
}
tainted
}
fn required_panic_strategy(tcx: TyCtxt<'_>, (): ()) -> Option<PanicStrategy> {
if tcx.sess.panic_strategy() == PanicStrategy::Abort {
return Some(PanicStrategy::Abort);
}
for def_id in tcx.hir().body_owners() {
if tcx.has_ffi_unwind_calls(def_id) {
return Some(PanicStrategy::Unwind);
}
}
// This crate can be linked with either runtime.
None
}
pub(crate) fn provide(providers: &mut Providers) {
*providers = Providers { has_ffi_unwind_calls, required_panic_strategy, ..*providers };
}

View File

@ -57,6 +57,7 @@ mod dest_prop;
pub mod dump_mir; pub mod dump_mir;
mod early_otherwise_branch; mod early_otherwise_branch;
mod elaborate_drops; mod elaborate_drops;
mod ffi_unwind_calls;
mod function_item_references; mod function_item_references;
mod generator; mod generator;
mod inline; mod inline;
@ -96,6 +97,7 @@ pub fn provide(providers: &mut Providers) {
check_unsafety::provide(providers); check_unsafety::provide(providers);
check_packed_ref::provide(providers); check_packed_ref::provide(providers);
coverage::query::provide(providers); coverage::query::provide(providers);
ffi_unwind_calls::provide(providers);
shim::provide(providers); shim::provide(providers);
*providers = Providers { *providers = Providers {
mir_keys, mir_keys,
@ -221,6 +223,9 @@ fn mir_const<'tcx>(
} }
} }
// has_ffi_unwind_calls query uses the raw mir, so make sure it is run.
tcx.ensure().has_ffi_unwind_calls(def.did);
let mut body = tcx.mir_built(def).steal(); let mut body = tcx.mir_built(def).steal();
rustc_middle::mir::dump_mir(tcx, None, "mir_map", &0, &body, |_, _| Ok(())); rustc_middle::mir::dump_mir(tcx, None, "mir_map", &0, &body, |_, _| Ok(()));

View File

@ -210,6 +210,8 @@
#![allow(unused_lifetimes)] #![allow(unused_lifetimes)]
// Tell the compiler to link to either panic_abort or panic_unwind // Tell the compiler to link to either panic_abort or panic_unwind
#![needs_panic_runtime] #![needs_panic_runtime]
// Ensure that std can be linked against panic_abort despite compiled with `-C panic=unwind`
#![cfg_attr(not(bootstrap), deny(ffi_unwind_calls))]
// std may use features in a platform-specific way // std may use features in a platform-specific way
#![allow(unused_features)] #![allow(unused_features)]
#![cfg_attr(test, feature(internal_output_capture, print_internals, update_panic_count))] #![cfg_attr(test, feature(internal_output_capture, print_internals, update_panic_count))]