Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/alloc_addresses/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::cell::RefCell;

use rustc_abi::{Align, Size};
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_middle::ty::TyCtxt;
use rustc_middle::ty::{InstanceKind, TyCtxt};

pub use self::address_generator::AddressGenerator;
use self::reuse_pool::ReusePool;
Expand Down Expand Up @@ -169,9 +169,39 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
Align::from_bytes(1).unwrap(),
params,
);
let ptr = alloc_bytes.as_ptr();
let mut ptr = alloc_bytes.as_ptr();
// Leak the underlying memory to ensure it remains unique.
std::mem::forget(alloc_bytes);
if let Some(GlobalAlloc::Function { instance, .. }) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above code is irrelevant for this code path. Split the function code path from the vtable code path and unwrap the function global alloc as all AllocKind::Function should be backed by a GlobalAlloc::Function I think

this.tcx.try_get_global_alloc(alloc_id)
{
#[cfg(all(unix, feature = "native-lib"))]
if let InstanceKind::Item(def_id) = instance.def {
let sig = this.tcx.fn_sig(def_id).skip_binder().skip_binder();
let closure =
crate::shims::native_lib::build_libffi_closure(this, sig)?;
if let Some(closure) = closure {
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::<unsafe extern "C" fn(*const std::ffi::c_void)>()
};
// 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
ptr = (fn_ptr as *const std::ffi::c_void).cast();
}
}
}
}
ptr
}
AllocKind::TypeId | AllocKind::Dead => unreachable!(),
Expand Down
2 changes: 1 addition & 1 deletion src/shims/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
105 changes: 103 additions & 2 deletions src/shims/native_lib/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Implements calling functions from a native library.

use std::ops::Deref;
use std::os::raw::c_void;
use std::sync::atomic::AtomicBool;

use libffi::low::CodePtr;
Expand Down Expand Up @@ -330,7 +331,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();
Expand Down Expand Up @@ -439,11 +442,67 @@ 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(),
Comment on lines +453 to +455
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed -- function pointers are scalar types.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function I happened to use for testing returned void. As this function is now also called to construct the return value types for the libffi closure type this was required to make my test case working.

That written: You are correct that this is not needed for a minimal support of callback over fro, it just happens to help with my particular test case and was rather straightforward to add.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function is now also called to construct the return value types for the libffi closure type

Ah, that makes sense.

Given that I think we should never return from that closure (see my other comments), I think we should not need this either.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still required as we need the FfiType while constructing the libffi Closure type to get the correct ABI as far as I understand.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could handle this separately in another PR and make all tests here have an already implemented return type. But not too important

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do this, but in that case I likely also need to change existing tests to make it work with the implemented feature as they also use void as return/argument type for the callback.

_ => {
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>,
) -> InterpResult<'tcx, Option<libffi::middle::Closure<'this>>> {
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 interp_ok(None);
}
};
let ty = match this.ty_to_ffitype(layout).report_err() {
Ok(ty) => ty,
Err(e) => {
tracing::info!(?e, "Skip closure");
return interp_ok(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 interp_ok(None);
}
};
match this.ty_to_ffitype(layout).report_err() {
Ok(ty) => ty,
Err(e) => {
tracing::info!(?e, "Skip closure");
return interp_ok(None);
}
}
};
let closure_builder = libffi::middle::Builder::new().args(args).res(res_type);
let data = CallbackData {
args: fn_ptr.inputs().to_vec(),
result: fn_ptr.output(),
this,
//link_name: *link_name,
};
let data = Box::leak(Box::new(data));
let closure = closure_builder.into_closure(callback_callback, data);
interp_ok(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.
Expand Down Expand Up @@ -536,3 +595,45 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
interp_ok(true)
}
}

struct CallbackData<'a, 'tcx> {
args: Vec<Ty<'tcx>>,
#[expect(dead_code, reason = "It's there for later")]
result: Ty<'tcx>,
this: &'a MiriInterpCx<'tcx>,
}

unsafe extern "C" fn callback_callback(
cif: &libffi::low::ffi_cif,
_result: &mut c_void,
args: *const *const c_void,
infos: &CallbackData<'_, '_>,
) {
debug_assert_eq!(cif.nargs.try_into(), Ok(infos.args.len()));
let mut rust_args = Vec::with_capacity(infos.args.len());
// We cast away the pointer to pointer to get a pointer to the actual argument
let mut args = args.cast::<c_void>();
for arg in &infos.args {
let scalar = match arg.kind() {
ty::RawPtr(..) => {
let ptr = StrictPointer::new(Provenance::Wildcard, Size::from_bytes(args.addr()));
// This offset moves the pointer to the next argument
args = unsafe { args.offset(1) };
Scalar::from_pointer(ptr, infos.this)
}
// the other types
_ => todo!(),
};
rust_args.push(scalar);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should remove this block as well or fill in more types instead? For now this is unused but as soon as this callback starts actually to continue the execution of the rust code here, this would be required as first step.


// We abort the execution at this point as we cannot return the
// expected value here.
eprintln!(
"Tried to call a function pointer via FFI boundary. \
That's not supported yet by miri\nThis function pointer was registered by a call to `{}` \
using an argument of the type `{}`",
"todo: fill in name", "todo: fill in type"
);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the place I'm currently rather unhappy with, but I don't know how to solve better.

I would like to emit a proper "error" here and not just print something. Is there a easy way to do this?

Also I would like to include the name of the function that registered the closure and the type of the argument, but I'm not sure how I get that information in src/alloc_address/mod.rs.

Maybe you have some pointer for both problems as well?

Also I expect that you want to tweak this message to something better, so consider that a placeholder until someone suggest a better wording.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via the InterpCx you should be able to report an error, but we def still need the exit as there is no way to leave this FFI code and go back to the original Rust/miri code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem will be passing in the InterpCx. We can not just use the this that was passed to build_libffi_closure -- that reference gets invalidated when build_libffi_closure returns.

We need some way to pass this value from call_native_with_args to the callback that gets invoked while this native code runs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to just put the context in a (thread local) global variable just before calling a native function and take it from there in the callback?

Copy link
Member

@RalfJung RalfJung Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like the easiest option. The closure may find itself called on a different thread, at which point we have to just hard abort without a nice error, but at least for the common case this should work (and anyway if the C code spawns threads that interact in any way with Rust code, this native code integration is pretty much completely broken).

std::process::exit(1);
}
35 changes: 35 additions & 0 deletions tests/native-lib/fail/call_function_ptr.notrace.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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)); // this one is not
| ^^^^^^^^^^^^^^^^^^^^^^ 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)); // this one is not
| ^^^^^^^^^^^^^^^^^^^^^^ 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()
| ^^^^^^^^^^^^^

Tried to call a function pointer via FFI boundary. That's not supported yet by miri
This function pointer was registered by a call to `todo: fill in name` using an argument of the type `todo: fill in type`
21 changes: 21 additions & 0 deletions tests/native-lib/fail/call_function_ptr.rs
Original file line number Diff line number Diff line change
@@ -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()>);
}

extern "C" fn nop() {}

unsafe {
call_fn_ptr(None); // this one is fine
call_fn_ptr(Some(nop)); // this one is not
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of now this test fails, even as it emits the "right" output as it doesn't emit an actual error, but just a eprintln + non-zero exist code.

}
}
36 changes: 36 additions & 0 deletions tests/native-lib/fail/call_function_ptr.trace.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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)); // this one is not
| ^^^^^^^^^^^^^^^^^^^^^^ 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)); // this one is not
| ^^^^^^^^^^^^^^^^^^^^^^ 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()
| ^^^^^^^^^^^^^

Tried to call a function pointer via FFI boundary. That's not supported yet by miri
This function pointer was registered by a call to `todo: fill in name` using an argument of the type `todo: fill in type`
7 changes: 7 additions & 0 deletions tests/native-lib/ptr_read_access.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Loading