Skip to content

Commit 2894ff7

Browse files
committed
miri: treat non-memory local variables properly for data race detection
1 parent fa72f07 commit 2894ff7

15 files changed

+411
-13
lines changed

compiler/rustc_const_eval/src/interpret/machine.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,10 +549,29 @@ pub trait Machine<'tcx>: Sized {
549549
Ok(ReturnAction::Normal)
550550
}
551551

552+
/// Called immediately after an "immediate" local variable is read
553+
/// (i.e., this is called for reads that do not end up accessing addressable memory).
554+
#[inline(always)]
555+
fn after_local_read(_ecx: &InterpCx<'tcx, Self>, _local: mir::Local) -> InterpResult<'tcx> {
556+
Ok(())
557+
}
558+
559+
/// Called immediately after an "immediate" local variable is assigned a new value
560+
/// (i.e., this is called for writes that do not end up in memory).
561+
/// `storage_live` indicates whether this is the initial write upon `StorageLive`.
562+
#[inline(always)]
563+
fn after_local_write(
564+
_ecx: &mut InterpCx<'tcx, Self>,
565+
_local: mir::Local,
566+
_storage_live: bool,
567+
) -> InterpResult<'tcx> {
568+
Ok(())
569+
}
570+
552571
/// Called immediately after actual memory was allocated for a local
553572
/// but before the local's stack frame is updated to point to that memory.
554573
#[inline(always)]
555-
fn after_local_allocated(
574+
fn after_local_moved_to_memory(
556575
_ecx: &mut InterpCx<'tcx, Self>,
557576
_local: mir::Local,
558577
_mplace: &MPlaceTy<'tcx, Self::Provenance>,

compiler/rustc_const_eval/src/interpret/operand.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
705705
if matches!(op, Operand::Immediate(_)) {
706706
assert!(!layout.is_unsized());
707707
}
708+
M::after_local_read(self, local)?;
708709
Ok(OpTy { op, layout })
709710
}
710711

compiler/rustc_const_eval/src/interpret/place.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -506,15 +506,13 @@ where
506506
&self,
507507
local: mir::Local,
508508
) -> InterpResult<'tcx, PlaceTy<'tcx, M::Provenance>> {
509-
// Other parts of the system rely on `Place::Local` never being unsized.
510-
// So we eagerly check here if this local has an MPlace, and if yes we use it.
511509
let frame = self.frame();
512510
let layout = self.layout_of_local(frame, local, None)?;
513511
let place = if layout.is_sized() {
514512
// We can just always use the `Local` for sized values.
515513
Place::Local { local, offset: None, locals_addr: frame.locals_addr() }
516514
} else {
517-
// Unsized `Local` isn't okay (we cannot store the metadata).
515+
// Other parts of the system rely on `Place::Local` never being unsized.
518516
match frame.locals[local].access()? {
519517
Operand::Immediate(_) => bug!(),
520518
Operand::Indirect(mplace) => Place::Ptr(*mplace),
@@ -626,6 +624,8 @@ where
626624
Operand::Immediate(local_val) => {
627625
// Local can be updated in-place.
628626
*local_val = src;
627+
// Call the machine hook (the data race detector needs to know about this write).
628+
M::after_local_write(self, local, /*storage_live*/ false)?;
629629
// Double-check that the value we are storing and the local fit to each other.
630630
// (*After* doing the update for borrow checker reasons.)
631631
if cfg!(debug_assertions) {
@@ -944,7 +944,7 @@ where
944944
mplace.mplace,
945945
)?;
946946
}
947-
M::after_local_allocated(self, local, &mplace)?;
947+
M::after_local_moved_to_memory(self, local, &mplace)?;
948948
// Now we can call `access_mut` again, asserting it goes well, and actually
949949
// overwrite things. This points to the entire allocation, not just the part
950950
// the place refers to, i.e. we do this before we apply `offset`.

compiler/rustc_const_eval/src/interpret/stack.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,8 +534,11 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
534534
let dest_place = self.allocate_dyn(layout, MemoryKind::Stack, meta)?;
535535
Operand::Indirect(*dest_place.mplace())
536536
} else {
537-
assert!(!meta.has_meta()); // we're dropping the metadata
538537
// Just make this an efficient immediate.
538+
assert!(!meta.has_meta()); // we're dropping the metadata
539+
// Make sure the machine knows this "write" is happening. (This is important so that
540+
// races involving local variable allocation can be detected by Miri.)
541+
M::after_local_write(self, local, /*storage_live*/ true)?;
539542
// Note that not calling `layout_of` here does have one real consequence:
540543
// if the type is too big, we'll only notice this when the local is actually initialized,
541544
// which is a bit too late -- we should ideally notice this already here, when the memory

src/tools/miri/src/concurrency/data_race.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use std::{
4747
};
4848

4949
use rustc_ast::Mutability;
50+
use rustc_data_structures::fx::FxHashMap;
5051
use rustc_data_structures::fx::FxHashSet;
5152
use rustc_index::{Idx, IndexVec};
5253
use rustc_middle::{mir, ty::Ty};
@@ -1121,6 +1122,103 @@ impl VClockAlloc {
11211122
}
11221123
}
11231124

1125+
/// Vector clock state for a stack frame (tracking the local variables
1126+
/// that do not have an allocation yet).
1127+
#[derive(Debug, Default)]
1128+
pub struct FrameState {
1129+
local_clocks: RefCell<FxHashMap<mir::Local, LocalClocks>>,
1130+
}
1131+
1132+
/// Stripped-down version of [`MemoryCellClocks`] for the clocks we need to keep track
1133+
/// of in a local that does not yet have addressable memory -- and hence can only
1134+
/// be accessed from the thread its stack frame belongs to, and cannot be access atomically.
1135+
#[derive(Debug)]
1136+
struct LocalClocks {
1137+
write: VTimestamp,
1138+
write_type: NaWriteType,
1139+
read: VTimestamp,
1140+
}
1141+
1142+
impl Default for LocalClocks {
1143+
fn default() -> Self {
1144+
Self { write: VTimestamp::ZERO, write_type: NaWriteType::Allocate, read: VTimestamp::ZERO }
1145+
}
1146+
}
1147+
1148+
impl FrameState {
1149+
pub fn local_write(&self, local: mir::Local, storage_live: bool, machine: &MiriMachine<'_>) {
1150+
let current_span = machine.current_span();
1151+
let global = machine.data_race.as_ref().unwrap();
1152+
if global.race_detecting() {
1153+
let (index, mut thread_clocks) = global.active_thread_state_mut(&machine.threads);
1154+
// This should do the same things as `MemoryCellClocks::write_race_detect`.
1155+
if !current_span.is_dummy() {
1156+
thread_clocks.clock.index_mut(index).span = current_span;
1157+
}
1158+
let mut clocks = self.local_clocks.borrow_mut();
1159+
if storage_live {
1160+
let new_clocks = LocalClocks {
1161+
write: thread_clocks.clock[index],
1162+
write_type: NaWriteType::Allocate,
1163+
read: VTimestamp::ZERO,
1164+
};
1165+
// There might already be an entry in the map for this, if the local was previously
1166+
// live already.
1167+
clocks.insert(local, new_clocks);
1168+
} else {
1169+
// This can fail to exist if `race_detecting` was false when the allocation
1170+
// occurred, in which case we can backdate this to the beginning of time.
1171+
let clocks = clocks.entry(local).or_insert_with(Default::default);
1172+
clocks.write = thread_clocks.clock[index];
1173+
clocks.write_type = NaWriteType::Write;
1174+
}
1175+
}
1176+
}
1177+
1178+
pub fn local_read(&self, local: mir::Local, machine: &MiriMachine<'_>) {
1179+
let current_span = machine.current_span();
1180+
let global = machine.data_race.as_ref().unwrap();
1181+
if global.race_detecting() {
1182+
let (index, mut thread_clocks) = global.active_thread_state_mut(&machine.threads);
1183+
// This should do the same things as `MemoryCellClocks::read_race_detect`.
1184+
if !current_span.is_dummy() {
1185+
thread_clocks.clock.index_mut(index).span = current_span;
1186+
}
1187+
thread_clocks.clock.index_mut(index).set_read_type(NaReadType::Read);
1188+
// This can fail to exist if `race_detecting` was false when the allocation
1189+
// occurred, in which case we can backdate this to the beginning of time.
1190+
let mut clocks = self.local_clocks.borrow_mut();
1191+
let clocks = clocks.entry(local).or_insert_with(Default::default);
1192+
clocks.read = thread_clocks.clock[index];
1193+
}
1194+
}
1195+
1196+
pub fn local_moved_to_memory(
1197+
&self,
1198+
local: mir::Local,
1199+
alloc: &mut VClockAlloc,
1200+
machine: &MiriMachine<'_>,
1201+
) {
1202+
let global = machine.data_race.as_ref().unwrap();
1203+
if global.race_detecting() {
1204+
let (index, _thread_clocks) = global.active_thread_state_mut(&machine.threads);
1205+
// Get the time the last write actually happened. This can fail to exist if
1206+
// `race_detecting` was false when the write occurred, in that case we can backdate this
1207+
// to the beginning of time.
1208+
let local_clocks = self.local_clocks.borrow_mut().remove(&local).unwrap_or_default();
1209+
for (_mem_clocks_range, mem_clocks) in alloc.alloc_ranges.get_mut().iter_mut_all() {
1210+
// The initialization write for this already happened, just at the wrong timestamp.
1211+
// Check that the thread index matches what we expect.
1212+
assert_eq!(mem_clocks.write.0, index);
1213+
// Convert the local's clocks into memory clocks.
1214+
mem_clocks.write = (index, local_clocks.write);
1215+
mem_clocks.write_type = local_clocks.write_type;
1216+
mem_clocks.read = VClock::new_with_index(index, local_clocks.read);
1217+
}
1218+
}
1219+
}
1220+
}
1221+
11241222
impl<'tcx> EvalContextPrivExt<'tcx> for MiriInterpCx<'tcx> {}
11251223
trait EvalContextPrivExt<'tcx>: MiriInterpCxExt<'tcx> {
11261224
/// Temporarily allow data-races to occur. This should only be used in

src/tools/miri/src/concurrency/thread.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,9 @@ impl<'tcx> ThreadManager<'tcx> {
528528
}
529529

530530
/// Mutably borrow the stack of the active thread.
531-
fn active_thread_stack_mut(&mut self) -> &mut Vec<Frame<'tcx, Provenance, FrameExtra<'tcx>>> {
531+
pub fn active_thread_stack_mut(
532+
&mut self,
533+
) -> &mut Vec<Frame<'tcx, Provenance, FrameExtra<'tcx>>> {
532534
&mut self.threads[self.active_thread].stack
533535
}
534536
pub fn all_stacks(

src/tools/miri/src/concurrency/vector_clock.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,19 @@ impl Ord for VTimestamp {
130130
/// also this means that there is only one unique valid length
131131
/// for each set of vector clock values and hence the PartialEq
132132
/// and Eq derivations are correct.
133+
///
134+
/// This means we cannot represent a clock where the last entry is a timestamp-0 read that occurs
135+
/// because of a retag. That's fine, all it does is risk wrong diagnostics in a extreme corner case.
133136
#[derive(PartialEq, Eq, Default, Debug)]
134137
pub struct VClock(SmallVec<[VTimestamp; SMALL_VECTOR]>);
135138

136139
impl VClock {
137140
/// Create a new vector-clock containing all zeros except
138141
/// for a value at the given index
139142
pub(super) fn new_with_index(index: VectorIdx, timestamp: VTimestamp) -> VClock {
143+
if timestamp.time() == 0 {
144+
return VClock::default();
145+
}
140146
let len = index.index() + 1;
141147
let mut vec = smallvec::smallvec![VTimestamp::ZERO; len];
142148
vec[index.index()] = timestamp;

src/tools/miri/src/machine.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,24 +80,42 @@ pub struct FrameExtra<'tcx> {
8080
/// an additional bit of "salt" into the cache key. This salt is fixed per-frame
8181
/// so that within a call, a const will have a stable address.
8282
salt: usize,
83+
84+
/// Data race detector per-frame data.
85+
pub data_race: Option<data_race::FrameState>,
8386
}
8487

8588
impl<'tcx> std::fmt::Debug for FrameExtra<'tcx> {
8689
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8790
// Omitting `timing`, it does not support `Debug`.
88-
let FrameExtra { borrow_tracker, catch_unwind, timing: _, is_user_relevant: _, salt: _ } =
89-
self;
91+
let FrameExtra {
92+
borrow_tracker,
93+
catch_unwind,
94+
timing: _,
95+
is_user_relevant,
96+
salt,
97+
data_race,
98+
} = self;
9099
f.debug_struct("FrameData")
91100
.field("borrow_tracker", borrow_tracker)
92101
.field("catch_unwind", catch_unwind)
102+
.field("is_user_relevant", is_user_relevant)
103+
.field("salt", salt)
104+
.field("data_race", data_race)
93105
.finish()
94106
}
95107
}
96108

97109
impl VisitProvenance for FrameExtra<'_> {
98110
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
99-
let FrameExtra { catch_unwind, borrow_tracker, timing: _, is_user_relevant: _, salt: _ } =
100-
self;
111+
let FrameExtra {
112+
catch_unwind,
113+
borrow_tracker,
114+
timing: _,
115+
is_user_relevant: _,
116+
salt: _,
117+
data_race: _,
118+
} = self;
101119

102120
catch_unwind.visit_provenance(visit);
103121
borrow_tracker.visit_provenance(visit);
@@ -1416,6 +1434,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
14161434
timing,
14171435
is_user_relevant: ecx.machine.is_user_relevant(&frame),
14181436
salt: ecx.machine.rng.borrow_mut().gen::<usize>() % ADDRS_PER_ANON_GLOBAL,
1437+
data_race: ecx.machine.data_race.as_ref().map(|_| data_race::FrameState::default()),
14191438
};
14201439

14211440
Ok(frame.with_extra(extra))
@@ -1521,17 +1540,43 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
15211540
res
15221541
}
15231542

1524-
fn after_local_allocated(
1543+
fn after_local_read(ecx: &InterpCx<'tcx, Self>, local: mir::Local) -> InterpResult<'tcx> {
1544+
if let Some(data_race) = &ecx.frame().extra.data_race {
1545+
data_race.local_read(local, &ecx.machine);
1546+
}
1547+
Ok(())
1548+
}
1549+
1550+
fn after_local_write(
1551+
ecx: &mut InterpCx<'tcx, Self>,
1552+
local: mir::Local,
1553+
storage_live: bool,
1554+
) -> InterpResult<'tcx> {
1555+
if let Some(data_race) = &ecx.frame().extra.data_race {
1556+
data_race.local_write(local, storage_live, &ecx.machine);
1557+
}
1558+
Ok(())
1559+
}
1560+
1561+
fn after_local_moved_to_memory(
15251562
ecx: &mut InterpCx<'tcx, Self>,
15261563
local: mir::Local,
15271564
mplace: &MPlaceTy<'tcx>,
15281565
) -> InterpResult<'tcx> {
15291566
let Some(Provenance::Concrete { alloc_id, .. }) = mplace.ptr().provenance else {
15301567
panic!("after_local_allocated should only be called on fresh allocations");
15311568
};
1569+
// Record the span where this was allocated: the declaration of the local.
15321570
let local_decl = &ecx.frame().body().local_decls[local];
15331571
let span = local_decl.source_info.span;
15341572
ecx.machine.allocation_spans.borrow_mut().insert(alloc_id, (span, None));
1573+
// The data race system has to fix the clocks used for this write.
1574+
let (alloc_info, machine) = ecx.get_alloc_extra_mut(alloc_id)?;
1575+
if let Some(data_race) =
1576+
&machine.threads.active_thread_stack().last().unwrap().extra.data_race
1577+
{
1578+
data_race.local_moved_to_memory(local, alloc_info.data_race.as_mut().unwrap(), machine);
1579+
}
15351580
Ok(())
15361581
}
15371582

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//@compile-flags: -Zmiri-preemption-rate=0.0 -Zmiri-disable-weak-memory-emulation
2+
#![feature(core_intrinsics)]
3+
#![feature(custom_mir)]
4+
5+
use std::intrinsics::mir::*;
6+
use std::sync::atomic::Ordering::*;
7+
use std::sync::atomic::*;
8+
use std::thread::JoinHandle;
9+
10+
static P: AtomicPtr<u8> = AtomicPtr::new(core::ptr::null_mut());
11+
12+
fn spawn_thread() -> JoinHandle<()> {
13+
std::thread::spawn(|| {
14+
while P.load(Relaxed).is_null() {
15+
std::hint::spin_loop();
16+
}
17+
unsafe {
18+
// Initialize `*P`.
19+
let ptr = P.load(Relaxed);
20+
*ptr = 127;
21+
//~^ ERROR: Data race detected between (1) creating a new allocation on thread `main` and (2) non-atomic write on thread `unnamed-1`
22+
}
23+
})
24+
}
25+
26+
fn finish(t: JoinHandle<()>, val_ptr: *mut u8) {
27+
P.store(val_ptr, Relaxed);
28+
29+
// Wait for the thread to be done.
30+
t.join().unwrap();
31+
32+
// Read initialized value.
33+
assert_eq!(unsafe { *val_ptr }, 127);
34+
}
35+
36+
#[custom_mir(dialect = "runtime", phase = "optimized")]
37+
fn main() {
38+
mir! {
39+
let t;
40+
let val;
41+
let val_ptr;
42+
let _ret;
43+
{
44+
Call(t = spawn_thread(), ReturnTo(after_spawn), UnwindContinue())
45+
}
46+
after_spawn = {
47+
// This races with the write in the other thread.
48+
StorageLive(val);
49+
50+
val_ptr = &raw mut val;
51+
Call(_ret = finish(t, val_ptr), ReturnTo(done), UnwindContinue())
52+
}
53+
done = {
54+
Return()
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)