Skip to content
Closed
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
18 changes: 17 additions & 1 deletion compiler/rustc_codegen_ssa/src/mir/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1799,7 +1799,23 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
args: &[ArgAbi<'tcx, Ty<'tcx>>],
lifetime_ends_after_call: &mut Vec<(Bx::Value, Size)>,
) -> usize {
let tuple = self.codegen_operand(bx, operand);
let mut tuple = self.codegen_operand(bx, operand);

// Mirrors the safety-copy in `codegen_call_terminator` (see #45996): if the
// tuple is a non-immediate `Copy` or `Constant` operand, its backing memory
// may be shared (e.g. it points into `.rodata` for a promoted constant), but
// the `extern "rust-call"` callee owns its argument storage by ABI and is
// free to write through it. Copy the tuple into a fresh alloca first so that
// the fields projected out of it below point at caller-owned memory.
if let &mir::Operand::Copy(_) | &mir::Operand::Constant(_) = operand
&& let Ref(PlaceValue { llextra: None, .. }) = tuple.val
{
let tmp = PlaceRef::alloca(bx, tuple.layout);
bx.lifetime_start(tmp.val.llval, tmp.layout.size);
tuple.store_with_annotation(bx, tmp);
tuple.val = Ref(tmp.val);
lifetime_ends_after_call.push((tmp.val.llval, tmp.layout.size));
}

// Handle both by-ref and immediate tuples.
if let Ref(place_val) = tuple.val {
Expand Down
61 changes: 61 additions & 0 deletions tests/ui/closures/rust-call-const-arg-alias.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Regression test for <https://github.com/rust-lang/rust/issues/155241>.
//!
//! When a closure with the `extern "rust-call"` ABI is invoked with a constant
//! tuple whose layout is passed indirectly, `codegen_arguments_untupled` used
//! to hand the callee a pointer straight into the caller's operand (typically
//! promoted into `.rodata`). Because the callee owns its argument storage by
//! ABI contract, a mutation inside the closure would write through a read-only
//! pointer and abort with SIGBUS / `STATUS_HEAP_CORRUPTION`.
//!
//! The safety-copy added in PR #45996 for `codegen_call_terminator` was never
//! ported to the tupled `rust-call` path; this test locks in that fix.
//
//@ run-pass
//@ revisions: opt0 opt3
//@[opt0] compile-flags: -Copt-level=0
//@[opt3] compile-flags: -Copt-level=3

#![feature(fn_traits)]

use std::hint::black_box;

#[derive(Copy, Clone)]
struct Thing {
x: usize,
y: usize,
z: usize,
}

// A tuple large enough to be passed indirectly (BackendRepr::Memory).
const VALUE: (Thing,) = (Thing { x: 0, y: 0, z: 0 },);

fn main() {
// The original 2017 reproducer: invoke the closure through `Fn::call`,
// which forces the tupled `rust-call` code path in the caller.
let observed = (|mut thing: Thing| {
thing.z = 1;
// Prevent the optimizer from eliminating the write entirely.
black_box(&thing);
thing.z
})
.call(VALUE);
assert_eq!(observed, 1);

// Same shape, but exercised through `FnMut::call_mut` to cover the other
// trait method that lowers to the same tupled path.
let mut sum = 0usize;
(|t: Thing| {
sum = sum.wrapping_add(t.x).wrapping_add(t.y).wrapping_add(t.z);
})
.call_mut(VALUE);
assert_eq!(sum, 0);

// And via `FnOnce::call_once` for good measure.
let taken = (|mut t: Thing| {
t.z = 42;
black_box(&t);
t.z
})
.call_once(VALUE);
assert_eq!(taken, 42);
}
Loading