Skip to content

Commit

Permalink
Rollup merge of #133211 - Strophox:miri-correct-state-update-ffi, r=R…
Browse files Browse the repository at this point in the history
…alfJung

Extend Miri to correctly pass mutable pointers through FFI

Based off of rust-lang/rust#129684, this PR further extends Miri to execute native calls that make use of pointers to *mutable* memory.
We adapt Miri's bookkeeping of internal state upon any FFI call that gives external code permission to mutate memory.

Native code may now possibly write and therefore initialize and change the pointer provenance of bytes it has access to: Such memory is assumed to be *initialized* afterwards and bytes are given *arbitrary (wildcard) provenance*. This enables programs that correctly use mutating FFI calls to run Miri without errors, at the cost of possibly missing Undefined Behaviour caused by incorrect usage of mutating FFI.

> <details>
>
> <summary> Simple example </summary>
>
> ```rust
> extern "C" {
>   fn init_int(ptr: *mut i32);
> }
>
> fn main() {
>   let mut x = std::mem::MaybeUninit::<i32>::uninit();
>   let x = unsafe {
>     init_int(x.as_mut_ptr());
>     x.assume_init()
>   };
>
>   println!("C initialized my memory to: {x}");
> }
> ```
> ```c
> void init_int(int *ptr) {
>   *ptr = 42;
> }
> ```
> should now show `C initialized my memory to: 42`.
>
> </details>

r? ``@RalfJung``
  • Loading branch information
matthiaskrgr authored Dec 6, 2024
2 parents d70031a + 272d7cf commit 64b7d3b
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 47 deletions.
10 changes: 6 additions & 4 deletions src/alloc_addresses/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,9 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {

impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
fn expose_ptr(&mut self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
let this = self.eval_context_mut();
let global_state = this.machine.alloc_addresses.get_mut();
fn expose_ptr(&self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
let this = self.eval_context_ref();
let mut global_state = this.machine.alloc_addresses.borrow_mut();
// In strict mode, we don't need this, so we can save some cycles by not tracking it.
if global_state.provenance_mode == ProvenanceMode::Strict {
return interp_ok(());
Expand All @@ -299,8 +299,10 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
return interp_ok(());
}
trace!("Exposing allocation id {alloc_id:?}");
let global_state = this.machine.alloc_addresses.get_mut();
global_state.exposed.insert(alloc_id);
// Release the global state before we call `expose_tag`, which may call `get_alloc_info_extra`,
// which may need access to the global state.
drop(global_state);
if this.machine.borrow_tracker.is_some() {
this.expose_tag(alloc_id, tag)?;
}
Expand Down
4 changes: 2 additions & 2 deletions src/borrow_tracker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
}
}

fn expose_tag(&mut self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
let this = self.eval_context_mut();
fn expose_tag(&self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
let this = self.eval_context_ref();
let method = this.machine.borrow_tracker.as_ref().unwrap().borrow().borrow_tracker_method;
match method {
BorrowTrackerMethod::StackedBorrows => this.sb_expose_tag(alloc_id, tag),
Expand Down
4 changes: 2 additions & 2 deletions src/borrow_tracker/stacked_borrows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1011,8 +1011,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
}

/// Mark the given tag as exposed. It was found on a pointer with the given AllocId.
fn sb_expose_tag(&mut self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
let this = self.eval_context_mut();
fn sb_expose_tag(&self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
let this = self.eval_context_ref();

// Function pointers and dead objects don't have an alloc_extra so we ignore them.
// This is okay because accessing them is UB anyway, no need for any Stacked Borrows checks.
Expand Down
4 changes: 2 additions & 2 deletions src/borrow_tracker/tree_borrows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,8 +532,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
}

/// Mark the given tag as exposed. It was found on a pointer with the given AllocId.
fn tb_expose_tag(&mut self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
let this = self.eval_context_mut();
fn tb_expose_tag(&self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
let this = self.eval_context_ref();

// Function pointers and dead objects don't have an alloc_extra so we ignore them.
// This is okay because accessing them is UB anyway, no need for any Tree Borrows checks.
Expand Down
7 changes: 5 additions & 2 deletions src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ impl interpret::Provenance for Provenance {
/// We use absolute addresses in the `offset` of a `StrictPointer`.
const OFFSET_IS_ADDR: bool = true;

/// Miri implements wildcard provenance.
const WILDCARD: Option<Self> = Some(Provenance::Wildcard);

fn get_alloc_id(self) -> Option<AllocId> {
match self {
Provenance::Concrete { alloc_id, .. } => Some(alloc_id),
Expand Down Expand Up @@ -1242,8 +1245,8 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
/// Called on `ptr as usize` casts.
/// (Actually computing the resulting `usize` doesn't need machine help,
/// that's just `Scalar::try_to_int`.)
fn expose_ptr(ecx: &mut InterpCx<'tcx, Self>, ptr: StrictPointer) -> InterpResult<'tcx> {
match ptr.provenance {
fn expose_provenance(ecx: &InterpCx<'tcx, Self>, provenance: Self::Provenance) -> InterpResult<'tcx> {
match provenance {
Provenance::Concrete { alloc_id, tag } => ecx.expose_ptr(alloc_id, tag),
Provenance::Wildcard => {
// No need to do anything for wildcard pointers as
Expand Down
50 changes: 34 additions & 16 deletions src/shims/native_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ use std::ops::Deref;

use libffi::high::call as ffi;
use libffi::low::CodePtr;
use rustc_abi::{BackendRepr, HasDataLayout};
use rustc_middle::ty::{self as ty, IntTy, UintTy};
use rustc_abi::{BackendRepr, HasDataLayout, Size};
use rustc_middle::{
mir::interpret::Pointer,
ty::{self as ty, IntTy, UintTy},
};
use rustc_span::Symbol;

use crate::*;
Expand Down Expand Up @@ -75,6 +78,11 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
unsafe { ffi::call::<()>(ptr, libffi_args.as_slice()) };
return interp_ok(ImmTy::uninit(dest.layout));
}
ty::RawPtr(..) => {
let x = unsafe { ffi::call::<*const ()>(ptr, libffi_args.as_slice()) };
let ptr = Pointer::new(Provenance::Wildcard, Size::from_bytes(x.addr()));
Scalar::from_pointer(ptr, this)
}
_ => throw_unsup_format!("unsupported return type for native call: {:?}", link_name),
};
interp_ok(ImmTy::from_scalar(scalar, dest.layout))
Expand Down Expand Up @@ -152,8 +160,26 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
if !matches!(arg.layout.backend_repr, BackendRepr::Scalar(_)) {
throw_unsup_format!("only scalar argument types are support for native calls")
}
libffi_args.push(imm_to_carg(this.read_immediate(arg)?, this)?);
let imm = this.read_immediate(arg)?;
libffi_args.push(imm_to_carg(&imm, this)?);
// If we are passing a pointer, prepare the memory it points to.
if matches!(arg.layout.ty.kind(), ty::RawPtr(..)) {
let ptr = imm.to_scalar().to_pointer(this)?;
let Some(prov) = ptr.provenance else {
// Pointer without provenance may not access any memory.
continue;
};
// We use `get_alloc_id` for its best-effort behaviour with Wildcard provenance.
let Some(alloc_id) = prov.get_alloc_id() else {
// Wildcard pointer, whatever it points to must be already exposed.
continue;
};
this.prepare_for_native_call(alloc_id, prov)?;
}
}

// FIXME: In the future, we should also call `prepare_for_native_call` on all previously
// exposed allocations, since C may access any of them.

// Convert them to `libffi::high::Arg` type.
let libffi_args = libffi_args
Expand Down Expand Up @@ -220,7 +246,7 @@ impl<'a> CArg {

/// Extract the scalar value from the result of reading a scalar from the machine,
/// and convert it to a `CArg`.
fn imm_to_carg<'tcx>(v: ImmTy<'tcx>, cx: &impl HasDataLayout) -> InterpResult<'tcx, CArg> {
fn imm_to_carg<'tcx>(v: &ImmTy<'tcx>, cx: &impl HasDataLayout) -> InterpResult<'tcx, CArg> {
interp_ok(match v.layout.ty.kind() {
// If the primitive provided can be converted to a type matching the type pattern
// then create a `CArg` of this primitive value with the corresponding `CArg` constructor.
Expand All @@ -238,18 +264,10 @@ fn imm_to_carg<'tcx>(v: ImmTy<'tcx>, cx: &impl HasDataLayout) -> InterpResult<'t
ty::Uint(UintTy::U64) => CArg::UInt64(v.to_scalar().to_u64()?),
ty::Uint(UintTy::Usize) =>
CArg::USize(v.to_scalar().to_target_usize(cx)?.try_into().unwrap()),
ty::RawPtr(_, mutability) => {
// Arbitrary mutable pointer accesses are not currently supported in Miri.
if mutability.is_mut() {
throw_unsup_format!(
"unsupported mutable pointer type for native call: {}",
v.layout.ty
);
} else {
let s = v.to_scalar().to_pointer(cx)?.addr();
// This relies on the `expose_provenance` in `addr_from_alloc_id`.
CArg::RawPtr(std::ptr::with_exposed_provenance_mut(s.bytes_usize()))
}
ty::RawPtr(..) => {
let s = v.to_scalar().to_pointer(cx)?.addr();
// This relies on the `expose_provenance` in `addr_from_alloc_id`.
CArg::RawPtr(std::ptr::with_exposed_provenance_mut(s.bytes_usize()))
}
_ => throw_unsup_format!("unsupported argument type for native call: {}", v.layout.ty),
})
Expand Down
27 changes: 12 additions & 15 deletions tests/native-lib/pass/ptr_read_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@
//@only-on-host

fn main() {
test_pointer();

test_simple();

test_nested();

test_static();
test_access_pointer();
test_access_simple();
test_access_nested();
test_access_static();
}

// Test void function that dereferences a pointer and prints its contents from C.
fn test_pointer() {
/// Test function that dereferences an int pointer and prints its contents from C.
fn test_access_pointer() {
extern "C" {
fn print_pointer(ptr: *const i32);
}
Expand All @@ -23,8 +20,8 @@ fn test_pointer() {
unsafe { print_pointer(&x) };
}

// Test function that dereferences a simple struct pointer and accesses a field.
fn test_simple() {
/// Test function that dereferences a simple struct pointer and accesses a field.
fn test_access_simple() {
#[repr(C)]
struct Simple {
field: i32,
Expand All @@ -39,8 +36,8 @@ fn test_simple() {
assert_eq!(unsafe { access_simple(&simple) }, -42);
}

// Test function that dereferences nested struct pointers and accesses fields.
fn test_nested() {
/// Test function that dereferences nested struct pointers and accesses fields.
fn test_access_nested() {
use std::ptr::NonNull;

#[derive(Debug, PartialEq, Eq)]
Expand All @@ -61,8 +58,8 @@ fn test_nested() {
assert_eq!(unsafe { access_nested(&nested_2) }, 97);
}

// Test function that dereferences static struct pointers and accesses fields.
fn test_static() {
/// Test function that dereferences a static struct pointer and accesses fields.
fn test_access_static() {
#[repr(C)]
struct Static {
value: i32,
Expand Down
Loading

0 comments on commit 64b7d3b

Please sign in to comment.