diff --git a/src/alloc_addresses/mod.rs b/src/alloc_addresses/mod.rs index 05d3444a4e..0a6f32fad0 100644 --- a/src/alloc_addresses/mod.rs +++ b/src/alloc_addresses/mod.rs @@ -12,6 +12,7 @@ use rustc_middle::ty::TyCtxt; pub use self::address_generator::AddressGenerator; use self::reuse_pool::ReusePool; +use crate::alloc::MiriAllocParams; use crate::concurrency::VClock; use crate::diagnostics::SpanDedupDiagnostic; use crate::*; @@ -162,18 +163,41 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { this.get_alloc_bytes_unchecked_raw(alloc_id)? } } - AllocKind::Function | AllocKind::VTable => { - // Allocate some dummy memory to get a unique address for this function/vtable. - let alloc_bytes = MiriAllocBytes::from_bytes( - &[0u8; 1], - Align::from_bytes(1).unwrap(), - params, - ); - let ptr = alloc_bytes.as_ptr(); - // Leak the underlying memory to ensure it remains unique. - std::mem::forget(alloc_bytes); - ptr + #[cfg(all(unix, feature = "native-lib"))] + AllocKind::Function => { + if let Some(GlobalAlloc::Function { instance, .. }) = + this.tcx.try_get_global_alloc(alloc_id) + && let rustc_middle::ty::InstanceKind::Item(def_id) = instance.def + && let Some(closure) = crate::shims::native_lib::build_libffi_closure( + this, + this.tcx.fn_sig(def_id).skip_binder().skip_binder(), + ) + { + let closure = Box::leak(Box::new(closure)); + // Libffi returns a **reference** to a function ptr here + // (The actual argument type doesn't matter) + let fn_ptr = unsafe { + closure.instantiate_code_ptr::() + }; + // Therefore we need to dereference the reference to get the actual function pointer + let fn_ptr = *fn_ptr; + #[expect( + clippy::as_conversions, + reason = "No better way to cast a function ptr to a ptr" + )] + { + // After that we need to cast the function pointer to the + // expected pointer type. At this point we don't actually care about the + // type of this pointer + (fn_ptr as *const std::ffi::c_void).cast() + } + } else { + dummy_alloc(params) + } } + #[cfg(not(all(unix, feature = "native-lib")))] + AllocKind::Function => dummy_alloc(params), + AllocKind::VTable => dummy_alloc(params), AllocKind::TypeId | AllocKind::Dead => unreachable!(), }; // We don't have to expose this pointer yet, we do that in `prepare_for_native_call`. @@ -205,6 +229,15 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { } } +fn dummy_alloc(params: MiriAllocParams) -> *const u8 { + // Allocate some dummy memory to get a unique address for this function/vtable. + let alloc_bytes = MiriAllocBytes::from_bytes(&[0u8; 1], Align::from_bytes(1).unwrap(), params); + let ptr = alloc_bytes.as_ptr(); + // Leak the underlying memory to ensure it remains unique. + std::mem::forget(alloc_bytes); + ptr +} + impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // Returns the `AllocId` that corresponds to the specified addr, diff --git a/src/shims/mod.rs b/src/shims/mod.rs index e51ace2fd9..345e16b8da 100644 --- a/src/shims/mod.rs +++ b/src/shims/mod.rs @@ -6,7 +6,7 @@ mod backtrace; mod files; mod math; #[cfg(all(unix, feature = "native-lib"))] -mod native_lib; +pub mod native_lib; mod unix; mod windows; mod x86; diff --git a/src/shims/native_lib/mod.rs b/src/shims/native_lib/mod.rs index 445f9618e7..a7379457c2 100644 --- a/src/shims/native_lib/mod.rs +++ b/src/shims/native_lib/mod.rs @@ -1,6 +1,9 @@ //! Implements calling functions from a native library. +use std::cell::Cell; use std::ops::Deref; +use std::os::raw::c_void; +use std::ptr::NonNull; use std::sync::atomic::AtomicBool; use libffi::low::CodePtr; @@ -16,6 +19,10 @@ use self::helpers::ToSoft; mod ffi; +thread_local! { + static INTERP_CTX: Cell>> = const { Cell::new(None) }; +} + #[cfg_attr( not(all( target_os = "linux", @@ -27,6 +34,7 @@ mod ffi; pub mod trace; use self::ffi::OwnedArg; +use crate::diagnostics::DiagLevel; use crate::*; /// The final results of an FFI trace, containing every relevant event detected @@ -94,6 +102,7 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { trace::Supervisor::do_ffi(alloc, || { // Call the function (`ptr`) with arguments `libffi_args`, and obtain the return value // as the specified primitive integer type + INTERP_CTX.set(NonNull::new(std::ptr::from_ref(this).cast_mut().cast())); let scalar = match dest.layout.ty.kind() { // ints ty::Int(IntTy::I8) => { @@ -330,7 +339,9 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { // Read the bytes that make up this argument. We cannot use the normal getter as // those would fail if any part of the argument is uninitialized. Native code // is kind of outside the interpreter, after all... - Box::from(alloc.inspect_with_uninit_and_ptr_outside_interpreter(range)) + let ret: Box<[u8]> = + Box::from(alloc.inspect_with_uninit_and_ptr_outside_interpreter(range)); + ret } either::Either::Right(imm) => { let mut bytes: Box<[u8]> = vec![0; imm.layout.size.bytes_usize()].into(); @@ -439,11 +450,62 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { interp_ok(match layout.ty.kind() { // Scalar types have already been handled above. ty::Adt(adt_def, args) => self.adt_to_ffitype(layout.ty, *adt_def, args)?, - _ => throw_unsup_format!("unsupported argument type for native call: {}", layout.ty), + // Functions with no declared return type (i.e., the default return) + // have the output_type `Tuple([])`. + ty::Tuple(t_list) if (*t_list).deref().is_empty() => FfiType::void(), + _ => { + throw_unsup_format!("unsupported argument type for native call: {}", layout.ty) + } }) } } +pub fn build_libffi_closure<'tcx, 'this>( + this: &'this MiriInterpCx<'tcx>, + fn_ptr: rustc_middle::ty::FnSig<'tcx>, +) -> Option> { + let mut args = Vec::new(); + for input in fn_ptr.inputs().iter() { + let layout = match this.layout_of(*input) { + Ok(layout) => layout, + Err(e) => { + tracing::info!(?e, "Skip closure"); + return None; + } + }; + let ty = match this.ty_to_ffitype(layout).report_err() { + Ok(ty) => ty, + Err(e) => { + tracing::info!(?e, "Skip closure"); + return None; + } + }; + args.push(ty); + } + let res_type = fn_ptr.output(); + let res_type = { + let layout = match this.layout_of(res_type) { + Ok(layout) => layout, + Err(e) => { + tracing::info!(?e, "Skip closure"); + return None; + } + }; + match this.ty_to_ffitype(layout).report_err() { + Ok(ty) => ty, + Err(e) => { + tracing::info!(?e, "Skip closure"); + return None; + } + } + }; + let closure_builder = libffi::middle::Builder::new().args(args).res(res_type); + let data = CallbackData {}; + let data = Box::leak(Box::new(data)); + let closure = closure_builder.into_closure(callback_callback, data); + Some(closure) +} + impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {} pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { /// Call the native host function, with supplied arguments. @@ -536,3 +598,38 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { interp_ok(true) } } + +struct CallbackData {} + +unsafe extern "C" fn callback_callback( + _cif: &libffi::low::ffi_cif, + _result: &mut c_void, + _args: *const *const c_void, + _infos: &CallbackData, +) { + if let Some(this) = INTERP_CTX.get() { + let ctx = unsafe { this.cast::>().as_ref() }; + let stacktrace = ctx.generate_stacktrace(); + crate::diagnostics::report_msg( + DiagLevel::Error, + "tried to call a function pointer through the FFI boundary".into(), + vec!["this function called a function pointer calling back into Rust".into()], + vec![(None, "this is not supported yet by miri".into())], + Vec::new(), + &stacktrace, + None, + &ctx.machine, + ); + } else { + // There is no interpctx set which likely means we are running in a different thread + // than that one that called into the native library. At this point we fall back + // to just reporting an error via eprintln before aborting. + eprintln!( + "Tried to call a function pointer via FFI boundary. \ + That's not supported yet by miri" + ); + } + // We abort the execution at this point as we cannot return the + // expected value here. + std::process::exit(1); +} diff --git a/tests/native-lib/fail/call_function_ptr.notrace.stderr b/tests/native-lib/fail/call_function_ptr.notrace.stderr new file mode 100644 index 0000000000..7851cfc15f --- /dev/null +++ b/tests/native-lib/fail/call_function_ptr.notrace.stderr @@ -0,0 +1,61 @@ +warning: sharing memory with a native function called via FFI + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | call_fn_ptr(Some(nop)); + | ^^^^^^^^^^^^^^^^^^^^^^ sharing memory with a native function + | + = help: when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory + = help: in particular, Miri assumes that the native call initializes all memory it has access to + = help: Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory + = help: what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free + = note: BACKTRACE: + = note: inside `pass_fn_ptr` at tests/native-lib/fail/call_function_ptr.rs:LL:CC +note: inside `main` + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | pass_fn_ptr() + | ^^^^^^^^^^^^^ + +warning: sharing a function pointer with a native function called via FFI + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | call_fn_ptr(Some(nop)); + | ^^^^^^^^^^^^^^^^^^^^^^ sharing a function pointer with a native function + | + = help: calling Rust functions from C is not supported and will, in the best case, crash the program + = note: BACKTRACE: + = note: inside `pass_fn_ptr` at tests/native-lib/fail/call_function_ptr.rs:LL:CC +note: inside `main` + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | pass_fn_ptr() + | ^^^^^^^^^^^^^ + +error: tried to call a function pointer through the FFI boundary + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | call_fn_ptr(Some(nop)); + | ^^^^^^^^^^^^^^^^^^^^^^ this function called a function pointer calling back into Rust + | + = note: this is not supported yet by miri + = note: BACKTRACE: + = note: inside `pass_fn_ptr` at tests/native-lib/fail/call_function_ptr.rs:LL:CC +note: inside `main` + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | pass_fn_ptr() + | ^^^^^^^^^^^^^ + = note: inside `>::call_once - shim(fn())` at RUSTLIB/core/src/ops/function.rs:LL:CC + = note: inside `std::sys::backtrace::__rust_begin_short_backtrace::` at RUSTLIB/std/src/sys/backtrace.rs:LL:CC + = note: inside closure at RUSTLIB/std/src/rt.rs:LL:CC + = note: inside `std::ops::function::impls:: for &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>::call_once` at RUSTLIB/core/src/ops/function.rs:LL:CC + = note: inside `std::panicking::catch_unwind::do_call::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>` at RUSTLIB/std/src/panicking.rs:LL:CC + = note: inside `std::panicking::catch_unwind:: i32 + std::marker::Sync + std::panic::RefUnwindSafe>` at RUSTLIB/std/src/panicking.rs:LL:CC + = note: inside `std::panic::catch_unwind::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>` at RUSTLIB/std/src/panic.rs:LL:CC + = note: inside closure at RUSTLIB/std/src/rt.rs:LL:CC + = note: inside `std::panicking::catch_unwind::do_call::<{closure@std::rt::lang_start_internal::{closure#0}}, isize>` at RUSTLIB/std/src/panicking.rs:LL:CC + = note: inside `std::panicking::catch_unwind::` at RUSTLIB/std/src/panicking.rs:LL:CC + = note: inside `std::panic::catch_unwind::<{closure@std::rt::lang_start_internal::{closure#0}}, isize>` at RUSTLIB/std/src/panic.rs:LL:CC + = note: inside `std::rt::lang_start_internal` at RUSTLIB/std/src/rt.rs:LL:CC + = note: inside `std::rt::lang_start::<()>` at RUSTLIB/std/src/rt.rs:LL:CC + diff --git a/tests/native-lib/fail/call_function_ptr.rs b/tests/native-lib/fail/call_function_ptr.rs new file mode 100644 index 0000000000..6db361c382 --- /dev/null +++ b/tests/native-lib/fail/call_function_ptr.rs @@ -0,0 +1,21 @@ +//@revisions: trace notrace +//@[trace] only-target: x86_64-unknown-linux-gnu i686-unknown-linux-gnu +//@[trace] compile-flags: -Zmiri-native-lib-enable-tracing +//@compile-flags: -Zmiri-permissive-provenance + +fn main() { + pass_fn_ptr() +} + +fn pass_fn_ptr() { + extern "C" { + fn call_fn_ptr(s: Option); + } + + extern "C" fn nop() {} + + unsafe { + call_fn_ptr(None); // this one is fine + call_fn_ptr(Some(nop)); //~ ERROR: tried to call a function pointer through the FFI boundary + } +} diff --git a/tests/native-lib/fail/call_function_ptr.trace.stderr b/tests/native-lib/fail/call_function_ptr.trace.stderr new file mode 100644 index 0000000000..1289b66785 --- /dev/null +++ b/tests/native-lib/fail/call_function_ptr.trace.stderr @@ -0,0 +1,62 @@ +warning: sharing memory with a native function called via FFI + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | call_fn_ptr(Some(nop)); + | ^^^^^^^^^^^^^^^^^^^^^^ sharing memory with a native function + | + = help: when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis + = help: in particular, Miri assumes that the native call initializes all memory it has written to + = help: Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory + = help: what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free + = help: tracing memory accesses in native code is not yet fully implemented, so there can be further imprecisions beyond what is documented here + = note: BACKTRACE: + = note: inside `pass_fn_ptr` at tests/native-lib/fail/call_function_ptr.rs:LL:CC +note: inside `main` + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | pass_fn_ptr() + | ^^^^^^^^^^^^^ + +warning: sharing a function pointer with a native function called via FFI + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | call_fn_ptr(Some(nop)); + | ^^^^^^^^^^^^^^^^^^^^^^ sharing a function pointer with a native function + | + = help: calling Rust functions from C is not supported and will, in the best case, crash the program + = note: BACKTRACE: + = note: inside `pass_fn_ptr` at tests/native-lib/fail/call_function_ptr.rs:LL:CC +note: inside `main` + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | pass_fn_ptr() + | ^^^^^^^^^^^^^ + +error: tried to call a function pointer through the FFI boundary + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | call_fn_ptr(Some(nop)); + | ^^^^^^^^^^^^^^^^^^^^^^ this function called a function pointer calling back into Rust + | + = note: this is not supported yet by miri + = note: BACKTRACE: + = note: inside `pass_fn_ptr` at tests/native-lib/fail/call_function_ptr.rs:LL:CC +note: inside `main` + --> tests/native-lib/fail/call_function_ptr.rs:LL:CC + | +LL | pass_fn_ptr() + | ^^^^^^^^^^^^^ + = note: inside `>::call_once - shim(fn())` at RUSTLIB/core/src/ops/function.rs:LL:CC + = note: inside `std::sys::backtrace::__rust_begin_short_backtrace::` at RUSTLIB/std/src/sys/backtrace.rs:LL:CC + = note: inside closure at RUSTLIB/std/src/rt.rs:LL:CC + = note: inside `std::ops::function::impls:: for &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>::call_once` at RUSTLIB/core/src/ops/function.rs:LL:CC + = note: inside `std::panicking::catch_unwind::do_call::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>` at RUSTLIB/std/src/panicking.rs:LL:CC + = note: inside `std::panicking::catch_unwind:: i32 + std::marker::Sync + std::panic::RefUnwindSafe>` at RUSTLIB/std/src/panicking.rs:LL:CC + = note: inside `std::panic::catch_unwind::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>` at RUSTLIB/std/src/panic.rs:LL:CC + = note: inside closure at RUSTLIB/std/src/rt.rs:LL:CC + = note: inside `std::panicking::catch_unwind::do_call::<{closure@std::rt::lang_start_internal::{closure#0}}, isize>` at RUSTLIB/std/src/panicking.rs:LL:CC + = note: inside `std::panicking::catch_unwind::` at RUSTLIB/std/src/panicking.rs:LL:CC + = note: inside `std::panic::catch_unwind::<{closure@std::rt::lang_start_internal::{closure#0}}, isize>` at RUSTLIB/std/src/panic.rs:LL:CC + = note: inside `std::rt::lang_start_internal` at RUSTLIB/std/src/rt.rs:LL:CC + = note: inside `std::rt::lang_start::<()>` at RUSTLIB/std/src/rt.rs:LL:CC + diff --git a/tests/native-lib/ptr_read_access.c b/tests/native-lib/ptr_read_access.c index 5f071ca3d4..44ba13aa54 100644 --- a/tests/native-lib/ptr_read_access.c +++ b/tests/native-lib/ptr_read_access.c @@ -68,3 +68,10 @@ EXPORT uintptr_t do_one_deref(const int32_t ***ptr) { EXPORT void pass_fn_ptr(void f(void)) { (void)f; // suppress unused warning } + +/* Test: function_ptrs */ +EXPORT void call_fn_ptr(void f(void)) { + if (f != NULL) { + f(); + } +}