Skip to content

Miri: fix handling of in-place argument and return place handling #145585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 19, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 9 additions & 3 deletions compiler/rustc_const_eval/src/interpret/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ use crate::{enter_trace_span, fluent_generated as fluent};
pub enum FnArg<'tcx, Prov: Provenance = CtfeProvenance> {
/// Pass a copy of the given operand.
Copy(OpTy<'tcx, Prov>),
/// Allow for the argument to be passed in-place: destroy the value originally stored at that place and
/// make the place inaccessible for the duration of the function call.
/// Allow for the argument to be passed in-place: destroy the value originally stored at that
/// place and make the place inaccessible for the duration of the function call. This *must* be
/// an in-memory place so that we can do the proper alias checks.
InPlace(MPlaceTy<'tcx, Prov>),
}

Expand Down Expand Up @@ -379,6 +380,11 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
}
}

// *Before* pushing the new frame, determine whether the return destination is in memory.
// Need to use `place_to_op` to be *sure* we get the mplace if there is one.
let destination_mplace = self.place_to_op(destination)?.as_mplace_or_imm().left();

// Push the "raw" frame -- this leaves locals uninitialized.
self.push_stack_frame_raw(instance, body, destination, cont)?;

// If an error is raised here, pop the frame again to get an accurate backtrace.
Expand Down Expand Up @@ -496,7 +502,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {

// Protect return place for in-place return value passing.
// We only need to protect anything if this is actually an in-memory place.
if let Left(mplace) = destination.as_mplace_or_local() {
if let Some(mplace) = destination_mplace {
M::protect_in_place_function_argument(self, &mplace)?;
}

Expand Down
6 changes: 6 additions & 0 deletions compiler/rustc_const_eval/src/interpret/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ impl<'tcx, Prov: Provenance> PlaceTy<'tcx, Prov> {
}

/// A place is either an mplace or some local.
///
/// Note that the return value can be different even for logically identical places!
/// Specifically, if a local is stored in-memory, this may return `Local` or `MPlaceTy`
/// depending on how the place was constructed. In other words, seeing `Local` here does *not*
/// imply that this place does not point to memory. Every caller must therefore always handle
/// both cases.
#[inline(always)]
pub fn as_mplace_or_local(
&self,
Expand Down
60 changes: 41 additions & 19 deletions compiler/rustc_const_eval/src/interpret/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use either::Either;
use rustc_abi::{FIRST_VARIANT, FieldIdx};
use rustc_data_structures::fx::FxHashSet;
use rustc_index::IndexSlice;
use rustc_middle::ty::{self, Instance, Ty};
use rustc_middle::{bug, mir, span_bug};
Expand Down Expand Up @@ -389,8 +390,9 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {

/// Evaluate the arguments of a function call
fn eval_fn_call_argument(
&self,
&mut self,
op: &mir::Operand<'tcx>,
move_definitely_disjoint: bool,
) -> InterpResult<'tcx, FnArg<'tcx, M::Provenance>> {
interp_ok(match op {
mir::Operand::Copy(_) | mir::Operand::Constant(_) => {
Expand All @@ -399,24 +401,19 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
FnArg::Copy(op)
}
mir::Operand::Move(place) => {
// If this place lives in memory, preserve its location.
// We call `place_to_op` which will be an `MPlaceTy` whenever there exists
// an mplace for this place. (This is in contrast to `PlaceTy::as_mplace_or_local`
// which can return a local even if that has an mplace.)
let place = self.eval_place(*place)?;
let op = self.place_to_op(&place)?;

match op.as_mplace_or_imm() {
Either::Left(mplace) => FnArg::InPlace(mplace),
Either::Right(_imm) => {
// This argument doesn't live in memory, so there's no place
// to make inaccessible during the call.
// We rely on there not being any stray `PlaceTy` that would let the
// caller directly access this local!
// This is also crucial for tail calls, where we want the `FnArg` to
// stay valid when the old stack frame gets popped.
FnArg::Copy(op)
if move_definitely_disjoint {
// We still have to ensure that no *other* pointers are used to access this place,
// so *if* it is in memory then we have to treat it as `InPlace`.
// Use `place_to_op` to guarantee that we notice it being in memory.
let op = self.place_to_op(&place)?;
match op.as_mplace_or_imm() {
Either::Left(mplace) => FnArg::InPlace(mplace),
Either::Right(_imm) => FnArg::Copy(op),
}
} else {
// We have to force this into memory to detect aliasing among `Move` arguments.
FnArg::InPlace(self.force_allocation(&place)?)
}
}
})
Expand All @@ -425,15 +422,40 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
/// Shared part of `Call` and `TailCall` implementation — finding and evaluating all the
/// necessary information about callee and arguments to make a call.
fn eval_callee_and_args(
&self,
&mut self,
terminator: &mir::Terminator<'tcx>,
func: &mir::Operand<'tcx>,
args: &[Spanned<mir::Operand<'tcx>>],
) -> InterpResult<'tcx, EvaluatedCalleeAndArgs<'tcx, M>> {
let func = self.eval_operand(func, None)?;

// Evaluating function call arguments. The tricky part here is dealing with `Move`
// arguments: we have to ensure no two such arguments alias. This would be most easily done
// by just forcing them all into memory and then doing the usual in-place argument
// protection, but then we'd force *a lot* of arguments into memory. So we do some syntactic
// pre-processing here where if all `move` arguments are syntactically distinct local
// variables (and none is indirect), we can skip the in-memory forcing.
let move_definitely_disjoint = 'move_definitely_disjoint: {
let mut previous_locals = FxHashSet::<mir::Local>::default();
for arg in args {
let mir::Operand::Move(place) = arg.node else {
continue; // we can skip non-`Move` arguments.
};
if place.is_indirect_first_projection() {
// An indirect `Move` argument could alias with anything else...
break 'move_definitely_disjoint false;
}
if !previous_locals.insert(place.local) {
// This local is the base for two arguments! They might overlap.
break 'move_definitely_disjoint false;
}
}
// We found no violation so they are all definitely disjoint.
true
};
let args = args
.iter()
.map(|arg| self.eval_fn_call_argument(&arg.node))
.map(|arg| self.eval_fn_call_argument(&arg.node, move_definitely_disjoint))
.collect::<InterpResult<'tcx, Vec<_>>>()?;

let fn_sig_binder = func.layout.ty.fn_sig(*self.tcx);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//@revisions: stack tree
//@[tree]compile-flags: -Zmiri-tree-borrows
// Validation forces more things into memory, which we can't have here.
//@compile-flags: -Zmiri-disable-validation
#![feature(custom_mir, core_intrinsics)]
use std::intrinsics::mir::*;

pub struct S(i32);

#[custom_mir(dialect = "runtime", phase = "optimized")]
fn main() {
mir! {
let _unit: ();
{
let staging = S(42); // This forces `staging` into memory...
let non_copy = staging; // ... so we move it to a non-inmemory local here.
// This specifically uses a type with scalar representation to tempt Miri to use the
// efficient way of storing local variables (outside adressable memory).
Call(_unit = callee(Move(non_copy), Move(non_copy)), ReturnTo(after_call), UnwindContinue())
//~[stack]^ ERROR: not granting access
//~[tree]| ERROR: /read access .* forbidden/
}
after_call = {
Return()
}
}
}

pub fn callee(x: S, mut y: S) {
// With the setup above, if `x` and `y` are both moved,
// then writing to `y` will change the value stored in `x`!
y.0 = 0;
assert_eq!(x.0, 42);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
error: Undefined Behavior: not granting access to tag <TAG> because that would remove [Unique for <TAG>] which is strongly protected
--> tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC
|
LL | Call(_unit = callee(Move(non_copy), Move(non_copy)), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <TAG> was created here, as the root tag for ALLOC
--> tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC
|
LL | Call(_unit = callee(Move(non_copy), Move(non_copy)), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: <TAG> is this argument
--> tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC
|
LL | y.0 = 0;
| ^^^^^^^
= note: BACKTRACE (of the first span):
= note: inside `main` at tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
error: Undefined Behavior: read access through <TAG> (root of the allocation) at ALLOC[0x0] is forbidden
--> tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC
|
LL | Call(_unit = callee(Move(non_copy), Move(non_copy)), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
= help: the accessed tag <TAG> (root of the allocation) is foreign to the protected tag <TAG> (i.e., it is not a child)
= help: this foreign read access would cause the protected tag <TAG> (currently Active) to become Disabled
= help: protected tags must never be Disabled
help: the accessed tag <TAG> was created here
--> tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC
|
LL | Call(_unit = callee(Move(non_copy), Move(non_copy)), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: the protected tag <TAG> was created here, in the initial state Reserved
--> tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC
|
LL | y.0 = 0;
| ^^^^^^^
help: the protected tag <TAG> later transitioned to Active due to a child write access at offsets [0x0..0x4]
--> tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC
|
LL | y.0 = 0;
| ^^^^^^^
= help: this transition corresponds to the first write to a 2-phase borrowed mutable reference
= note: BACKTRACE (of the first span):
= note: inside `main` at tests/fail/function_calls/arg_inplace_locals_alias.rs:LL:CC

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ LL | unsafe { ptr.read() };
note: inside `main`
--> tests/fail/function_calls/return_pointer_aliasing_read.rs:LL:CC
|
LL | Call(*ptr = myfun(ptr), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
LL | Call(_x = myfun(ptr), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Uninitialized memory occurred at ALLOC[0x0..0x4], in this allocation:
ALLOC (stack variable, size: 4, align: 4) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ use std::intrinsics::mir::*;
pub fn main() {
mir! {
{
let x = 0;
let ptr = &raw mut x;
let _x = 0;
let ptr = &raw mut _x;
// We arrange for `myfun` to have a pointer that aliases
// its return place. Even just reading from that pointer is UB.
Call(*ptr = myfun(ptr), ReturnTo(after_call), UnwindContinue())
Call(_x = myfun(ptr), ReturnTo(after_call), UnwindContinue())
}

after_call = {
Expand All @@ -25,7 +25,7 @@ pub fn main() {

fn myfun(ptr: *mut i32) -> i32 {
unsafe { ptr.read() };
//~[stack]^ ERROR: not granting access
//~[stack]^ ERROR: does not exist in the borrow stack
//~[tree]| ERROR: /read access .* forbidden/
//~[none]| ERROR: uninitialized
// Without an aliasing model, reads are "fine" but at least they return uninit data.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
error: Undefined Behavior: not granting access to tag <TAG> because that would remove [Unique for <TAG>] which is strongly protected
error: Undefined Behavior: attempting a read access using <TAG> at ALLOC[0x0], but that tag does not exist in the borrow stack for this location
--> tests/fail/function_calls/return_pointer_aliasing_read.rs:LL:CC
|
LL | unsafe { ptr.read() };
| ^^^^^^^^^^ Undefined Behavior occurred here
| ^^^^^^^^^^ this error occurs as part of an access at ALLOC[0x0..0x4]
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
Expand All @@ -11,12 +11,12 @@ help: <TAG> was created by a SharedReadWrite retag at offsets [0x0..0x4]
|
LL | / mir! {
LL | | {
LL | | let x = 0;
LL | | let ptr = &raw mut x;
LL | | let _x = 0;
LL | | let ptr = &raw mut _x;
... |
LL | | }
| |_____^
help: <TAG> is this argument
help: <TAG> was later invalidated at offsets [0x0..0x4] by a Unique in-place function argument/return passing protection
--> tests/fail/function_calls/return_pointer_aliasing_read.rs:LL:CC
|
LL | unsafe { ptr.read() };
Expand All @@ -26,8 +26,8 @@ LL | unsafe { ptr.read() };
note: inside `main`
--> tests/fail/function_calls/return_pointer_aliasing_read.rs:LL:CC
|
LL | Call(*ptr = myfun(ptr), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
LL | Call(_x = myfun(ptr), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: this error originates in the macro `::core::intrinsics::mir::__internal_remove_let` which comes from the expansion of the macro `mir` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ help: the accessed tag <TAG> was created here
|
LL | / mir! {
LL | | {
LL | | let x = 0;
LL | | let ptr = &raw mut x;
LL | | let _x = 0;
LL | | let ptr = &raw mut _x;
... |
LL | | }
| |_____^
Expand All @@ -34,8 +34,8 @@ LL | unsafe { ptr.read() };
note: inside `main`
--> tests/fail/function_calls/return_pointer_aliasing_read.rs:LL:CC
|
LL | Call(*ptr = myfun(ptr), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
LL | Call(_x = myfun(ptr), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: this error originates in the macro `::core::intrinsics::mir::__internal_remove_let` which comes from the expansion of the macro `mir` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub fn main() {
let ptr = &raw mut _x;
// We arrange for `myfun` to have a pointer that aliases
// its return place. Writing to that pointer is UB.
Call(*ptr = myfun(ptr), ReturnTo(after_call), UnwindContinue())
Call(_x = myfun(ptr), ReturnTo(after_call), UnwindContinue())
}

after_call = {
Expand All @@ -26,7 +26,7 @@ pub fn main() {
fn myfun(ptr: *mut i32) -> i32 {
// This overwrites the return place, which shouldn't be possible through another pointer.
unsafe { ptr.write(0) };
//~[stack]^ ERROR: strongly protected
//~[stack]^ ERROR: does not exist in the borrow stack
//~[tree]| ERROR: /write access .* forbidden/
13
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
error: Undefined Behavior: not granting access to tag <TAG> because that would remove [Unique for <TAG>] which is strongly protected
error: Undefined Behavior: attempting a write access using <TAG> at ALLOC[0x0], but that tag does not exist in the borrow stack for this location
--> tests/fail/function_calls/return_pointer_aliasing_write.rs:LL:CC
|
LL | unsafe { ptr.write(0) };
| ^^^^^^^^^^^^ Undefined Behavior occurred here
| ^^^^^^^^^^^^ this error occurs as part of an access at ALLOC[0x0..0x4]
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
Expand All @@ -16,7 +16,7 @@ LL | | let ptr = &raw mut _x;
... |
LL | | }
| |_____^
help: <TAG> is this argument
help: <TAG> was later invalidated at offsets [0x0..0x4] by a Unique in-place function argument/return passing protection
--> tests/fail/function_calls/return_pointer_aliasing_write.rs:LL:CC
|
LL | unsafe { ptr.write(0) };
Expand All @@ -26,8 +26,8 @@ LL | unsafe { ptr.write(0) };
note: inside `main`
--> tests/fail/function_calls/return_pointer_aliasing_write.rs:LL:CC
|
LL | Call(*ptr = myfun(ptr), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
LL | Call(_x = myfun(ptr), ReturnTo(after_call), UnwindContinue())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: this error originates in the macro `::core::intrinsics::mir::__internal_remove_let` which comes from the expansion of the macro `mir` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
Expand Down
Loading
Loading