Skip to content

Commit 52c5daf

Browse files
authored
Merge pull request #4658 from RalfJung/weak-mem-na-reads
weak memory: fix non-atomic read clearing store buffer
2 parents a62e8f6 + e8233b5 commit 52c5daf

File tree

8 files changed

+136
-55
lines changed

8 files changed

+136
-55
lines changed

src/borrow_tracker/stacked_borrows/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,7 @@ trait EvalContextPrivExt<'tcx, 'ecx>: crate::MiriInterpCxExt<'tcx> {
749749
// Make sure the data race model also knows about this.
750750
// FIXME(genmc): Ensure this is still done in GenMC mode. Check for other places where GenMC may need to be informed.
751751
if let Some(data_race) = alloc_extra.data_race.as_vclocks_mut() {
752-
data_race.write(
752+
data_race.write_non_atomic(
753753
alloc_id,
754754
range,
755755
NaWriteType::Retag,
@@ -798,7 +798,7 @@ trait EvalContextPrivExt<'tcx, 'ecx>: crate::MiriInterpCxExt<'tcx> {
798798
assert_eq!(access, AccessKind::Read);
799799
// Make sure the data race model also knows about this.
800800
if let Some(data_race) = alloc_extra.data_race.as_vclocks_ref() {
801-
data_race.read(
801+
data_race.read_non_atomic(
802802
alloc_id,
803803
range,
804804
NaReadType::Retag,

src/borrow_tracker/tree_borrows/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
366366
// Also inform the data race model (but only if any bytes are actually affected).
367367
if range_in_alloc.size.bytes() > 0 {
368368
if let Some(data_race) = alloc_extra.data_race.as_vclocks_ref() {
369-
data_race.read(
369+
data_race.read_non_atomic(
370370
alloc_id,
371371
range_in_alloc,
372372
NaReadType::Retag,

src/concurrency/data_race.rs

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -648,18 +648,17 @@ impl MemoryCellClocks {
648648
thread_clocks.clock.index_mut(index).span = current_span;
649649
}
650650
thread_clocks.clock.index_mut(index).set_read_type(read_type);
651-
if self.write_was_before(&thread_clocks.clock) {
652-
// We must be ordered-after all atomic writes.
653-
let race_free = if let Some(atomic) = self.atomic() {
654-
atomic.write_vector <= thread_clocks.clock
655-
} else {
656-
true
657-
};
658-
self.read.set_at_index(&thread_clocks.clock, index);
659-
if race_free { Ok(()) } else { Err(DataRace) }
660-
} else {
661-
Err(DataRace)
651+
// Check synchronization with non-atomic writes.
652+
if !self.write_was_before(&thread_clocks.clock) {
653+
return Err(DataRace);
662654
}
655+
// Check synchronization with atomic writes.
656+
if !self.atomic().is_none_or(|atomic| atomic.write_vector <= thread_clocks.clock) {
657+
return Err(DataRace);
658+
}
659+
// Record this access.
660+
self.read.set_at_index(&thread_clocks.clock, index);
661+
Ok(())
663662
}
664663

665664
/// Detect races for non-atomic write operations at the current memory cell
@@ -675,24 +674,23 @@ impl MemoryCellClocks {
675674
if !current_span.is_dummy() {
676675
thread_clocks.clock.index_mut(index).span = current_span;
677676
}
678-
if self.write_was_before(&thread_clocks.clock) && self.read <= thread_clocks.clock {
679-
let race_free = if let Some(atomic) = self.atomic() {
680-
atomic.write_vector <= thread_clocks.clock
681-
&& atomic.read_vector <= thread_clocks.clock
682-
} else {
683-
true
684-
};
685-
self.write = (index, thread_clocks.clock[index]);
686-
self.write_type = write_type;
687-
if race_free {
688-
self.read.set_zero_vector();
689-
Ok(())
690-
} else {
691-
Err(DataRace)
692-
}
693-
} else {
694-
Err(DataRace)
677+
// Check synchronization with non-atomic accesses.
678+
if !(self.write_was_before(&thread_clocks.clock) && self.read <= thread_clocks.clock) {
679+
return Err(DataRace);
695680
}
681+
// Check synchronization with atomic accesses.
682+
if !self.atomic().is_none_or(|atomic| {
683+
atomic.write_vector <= thread_clocks.clock && atomic.read_vector <= thread_clocks.clock
684+
}) {
685+
return Err(DataRace);
686+
}
687+
// Record this access.
688+
self.write = (index, thread_clocks.clock[index]);
689+
self.write_type = write_type;
690+
self.read.set_zero_vector();
691+
// This is not an atomic location any more.
692+
self.atomic_ops = None;
693+
Ok(())
696694
}
697695
}
698696

@@ -758,14 +756,9 @@ pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> {
758756
let this = self.eval_context_mut();
759757
this.atomic_access_check(dest, AtomicAccessType::Store)?;
760758

761-
// Read the previous value so we can put it in the store buffer later.
762-
// The program didn't actually do a read, so suppress the memory access hooks.
763-
// This is also a very special exception where we just ignore an error -- if this read
764-
// was UB e.g. because the memory is uninitialized, we don't want to know!
765-
let old_val = this.run_for_validation_ref(|this| this.read_scalar(dest)).discard_err();
766-
767759
// Inform GenMC about the atomic store.
768760
if let Some(genmc_ctx) = this.machine.data_race.as_genmc_ref() {
761+
let old_val = this.run_for_validation_ref(|this| this.read_scalar(dest)).discard_err();
769762
if genmc_ctx.atomic_store(
770763
this,
771764
dest.ptr().addr(),
@@ -780,6 +773,9 @@ pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> {
780773
}
781774
return interp_ok(());
782775
}
776+
777+
// Read the previous value so we can put it in the store buffer later.
778+
let old_val = this.get_latest_nonatomic_val(dest);
783779
this.allow_data_races_mut(move |this| this.write_scalar(val, dest))?;
784780
this.validate_atomic_store(dest, atomic)?;
785781
this.buffered_atomic_write(val, dest, atomic, old_val)
@@ -1201,7 +1197,7 @@ impl VClockAlloc {
12011197
/// operation for which data-race detection is handled separately, for example
12021198
/// atomic read operations. The `ty` parameter is used for diagnostics, letting
12031199
/// the user know which type was read.
1204-
pub fn read<'tcx>(
1200+
pub fn read_non_atomic<'tcx>(
12051201
&self,
12061202
alloc_id: AllocId,
12071203
access_range: AllocRange,
@@ -1243,7 +1239,7 @@ impl VClockAlloc {
12431239
/// being created or if it is temporarily disabled during a racy read or write
12441240
/// operation. The `ty` parameter is used for diagnostics, letting
12451241
/// the user know which type was written.
1246-
pub fn write<'tcx>(
1242+
pub fn write_non_atomic<'tcx>(
12471243
&mut self,
12481244
alloc_id: AllocId,
12491245
access_range: AllocRange,
@@ -1540,6 +1536,35 @@ trait EvalContextPrivExt<'tcx>: MiriInterpCxExt<'tcx> {
15401536
)
15411537
}
15421538

1539+
/// Returns the most recent *non-atomic* value stored in the given place.
1540+
/// Errors if we don't need that (because we don't do store buffering) or if
1541+
/// the most recent value is in fact atomic.
1542+
fn get_latest_nonatomic_val(&self, place: &MPlaceTy<'tcx>) -> Result<Option<Scalar>, ()> {
1543+
let this = self.eval_context_ref();
1544+
// These cannot fail because `atomic_access_check` was done first.
1545+
let (alloc_id, offset, _prov) = this.ptr_get_alloc_id(place.ptr(), 0).unwrap();
1546+
let alloc_meta = &this.get_alloc_extra(alloc_id).unwrap().data_race;
1547+
if alloc_meta.as_weak_memory_ref().is_none() {
1548+
// No reason to read old value if we don't track store buffers.
1549+
return Err(());
1550+
}
1551+
let data_race = alloc_meta.as_vclocks_ref().unwrap();
1552+
// Only read old value if this is currently a non-atomic location.
1553+
for (_range, clocks) in data_race.alloc_ranges.borrow_mut().iter(offset, place.layout.size)
1554+
{
1555+
// If this had an atomic write that's not before the non-atomic write, that should
1556+
// already be in the store buffer. Initializing the store buffer now would use the
1557+
// wrong `sync_clock` so we better make sure that does not happen.
1558+
if clocks.atomic().is_some_and(|atomic| !(atomic.write_vector <= clocks.write())) {
1559+
return Err(());
1560+
}
1561+
}
1562+
// The program didn't actually do a read, so suppress the memory access hooks.
1563+
// This is also a very special exception where we just ignore an error -- if this read
1564+
// was UB e.g. because the memory is uninitialized, we don't want to know!
1565+
Ok(this.run_for_validation_ref(|this| this.read_scalar(place)).discard_err())
1566+
}
1567+
15431568
/// Generic atomic operation implementation
15441569
fn validate_atomic_op<A: Debug + Copy>(
15451570
&self,

src/concurrency/weak_memory.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,11 @@ impl StoreBufferAlloc {
177177
Self { store_buffers: RefCell::new(RangeObjectMap::new()) }
178178
}
179179

180-
/// When a non-atomic access happens on a location that has been atomically accessed
181-
/// before without data race, we can determine that the non-atomic access fully happens
180+
/// When a non-atomic write happens on a location that has been atomically accessed
181+
/// before without data race, we can determine that the non-atomic write fully happens
182182
/// after all the prior atomic writes so the location no longer needs to exhibit
183-
/// any weak memory behaviours until further atomic accesses.
184-
pub fn memory_accessed(&self, range: AllocRange, global: &DataRaceState) {
183+
/// any weak memory behaviours until further atomic writes.
184+
pub fn non_atomic_write(&self, range: AllocRange, global: &DataRaceState) {
185185
if !global.ongoing_action_data_race_free() {
186186
let mut buffers = self.store_buffers.borrow_mut();
187187
let access_type = buffers.access_type(range);
@@ -223,18 +223,23 @@ impl StoreBufferAlloc {
223223
fn get_or_create_store_buffer_mut<'tcx>(
224224
&mut self,
225225
range: AllocRange,
226-
init: Option<Scalar>,
226+
init: Result<Option<Scalar>, ()>,
227227
) -> InterpResult<'tcx, &mut StoreBuffer> {
228228
let buffers = self.store_buffers.get_mut();
229229
let access_type = buffers.access_type(range);
230230
let pos = match access_type {
231231
AccessType::PerfectlyOverlapping(pos) => pos,
232232
AccessType::Empty(pos) => {
233+
let init =
234+
init.expect("cannot have empty store buffer when previous write was atomic");
233235
buffers.insert_at_pos(pos, range, StoreBuffer::new(init));
234236
pos
235237
}
236238
AccessType::ImperfectlyOverlapping(pos_range) => {
237239
// Once we reach here we would've already checked that this access is not racy.
240+
let init = init.expect(
241+
"cannot have partially overlapping store buffer when previous write was atomic",
242+
);
238243
buffers.remove_pos_range(pos_range.clone());
239244
buffers.insert_at_pos(pos_range.start, range, StoreBuffer::new(init));
240245
pos_range.start
@@ -490,7 +495,7 @@ pub(super) trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
490495
}
491496
let range = alloc_range(base_offset, place.layout.size);
492497
let sync_clock = data_race_clocks.sync_clock(range);
493-
let buffer = alloc_buffers.get_or_create_store_buffer_mut(range, Some(init))?;
498+
let buffer = alloc_buffers.get_or_create_store_buffer_mut(range, Ok(Some(init)))?;
494499
// The RMW always reads from the most recent store.
495500
buffer.read_from_last_store(global, threads, atomic == AtomicRwOrd::SeqCst);
496501
buffer.buffered_write(
@@ -556,15 +561,16 @@ pub(super) trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
556561
/// Add the given write to the store buffer. (Does not change machine memory.)
557562
///
558563
/// `init` says with which value to initialize the store buffer in case there wasn't a store
559-
/// buffer for this memory range before.
564+
/// buffer for this memory range before. `Err(())` means the value is not available;
565+
/// `Ok(None)` means the memory does not contain a valid scalar.
560566
///
561567
/// Must be called *after* `validate_atomic_store` to ensure that `sync_clock` is up-to-date.
562568
fn buffered_atomic_write(
563569
&mut self,
564570
val: Scalar,
565571
dest: &MPlaceTy<'tcx>,
566572
atomic: AtomicWriteOrd,
567-
init: Option<Scalar>,
573+
init: Result<Option<Scalar>, ()>,
568574
) -> InterpResult<'tcx> {
569575
let this = self.eval_context_mut();
570576
let (alloc_id, base_offset, ..) = this.ptr_get_alloc_id(dest.ptr(), 0)?;

src/machine.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,14 +1536,11 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
15361536
genmc_ctx.memory_load(machine, ptr.addr(), range.size)?,
15371537
GlobalDataRaceHandler::Vclocks(_data_race) => {
15381538
let _trace = enter_trace_span!(data_race::before_memory_read);
1539-
let AllocDataRaceHandler::Vclocks(data_race, weak_memory) = &alloc_extra.data_race
1539+
let AllocDataRaceHandler::Vclocks(data_race, _weak_memory) = &alloc_extra.data_race
15401540
else {
15411541
unreachable!();
15421542
};
1543-
data_race.read(alloc_id, range, NaReadType::Read, None, machine)?;
1544-
if let Some(weak_memory) = weak_memory {
1545-
weak_memory.memory_accessed(range, machine.data_race.as_vclocks_ref().unwrap());
1546-
}
1543+
data_race.read_non_atomic(alloc_id, range, NaReadType::Read, None, machine)?;
15471544
}
15481545
}
15491546
if let Some(borrow_tracker) = &alloc_extra.borrow_tracker {
@@ -1579,9 +1576,10 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
15791576
else {
15801577
unreachable!()
15811578
};
1582-
data_race.write(alloc_id, range, NaWriteType::Write, None, machine)?;
1579+
data_race.write_non_atomic(alloc_id, range, NaWriteType::Write, None, machine)?;
15831580
if let Some(weak_memory) = weak_memory {
1584-
weak_memory.memory_accessed(range, machine.data_race.as_vclocks_ref().unwrap());
1581+
weak_memory
1582+
.non_atomic_write(range, machine.data_race.as_vclocks_ref().unwrap());
15851583
}
15861584
}
15871585
}
@@ -1612,7 +1610,7 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
16121610
GlobalDataRaceHandler::Vclocks(_global_state) => {
16131611
let _trace = enter_trace_span!(data_race::before_memory_deallocation);
16141612
let data_race = alloc_extra.data_race.as_vclocks_mut().unwrap();
1615-
data_race.write(
1613+
data_race.write_non_atomic(
16161614
alloc_id,
16171615
alloc_range(Size::ZERO, size),
16181616
NaWriteType::Deallocate,

tests/pass/0weak_memory/weak.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,20 @@ fn initialization_write(add_fence: bool) {
8888
check_all_outcomes([11, 22], || {
8989
let x = static_atomic(11);
9090

91+
if add_fence {
92+
// For the fun of it, let's make this location atomic and non-atomic again,
93+
// to ensure Miri updates the state properly for that.
94+
x.store(99, Relaxed);
95+
unsafe { std::ptr::from_ref(x).cast_mut().write(11.into()) };
96+
}
97+
9198
let wait = static_atomic(0);
9299

93100
let j1 = spawn(move || {
94101
x.store(22, Relaxed);
102+
// Since nobody else writes to `x`, we can non-atomically read it.
103+
// (This tests that we do not delete the store buffer here.)
104+
unsafe { std::ptr::from_ref(x).read() };
95105
// Relaxed is intentional. We want to test if the thread 2 reads the initialisation write
96106
// after a relaxed write
97107
wait.store(1, Relaxed);
File renamed without changes.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! This reproduces #4655 every single time
2+
//@ compile-flags: -Zmiri-fixed-schedule -Zmiri-disable-stacked-borrows
3+
use std::sync::atomic::{AtomicUsize, Ordering};
4+
use std::{ptr, thread};
5+
6+
const SIZE: usize = 256;
7+
8+
static mut ARRAY: [u8; SIZE] = [0; _];
9+
// Everything strictly less than this has been initialized by the sender.
10+
static POS: AtomicUsize = AtomicUsize::new(0);
11+
12+
fn main() {
13+
// Sender
14+
let th = std::thread::spawn(|| {
15+
for i in 0..SIZE {
16+
unsafe { ptr::write(&raw mut ARRAY[i], 1) };
17+
POS.store(i + 1, Ordering::Release);
18+
19+
thread::yield_now();
20+
21+
// We are the only writer, so we can do non-atomic reads as well.
22+
unsafe { (&raw const POS).cast::<usize>().read() };
23+
}
24+
});
25+
26+
// Receiver
27+
loop {
28+
let i = POS.load(Ordering::Acquire);
29+
30+
if i > 0 {
31+
unsafe { ptr::read(&raw const ARRAY[i - 1]) };
32+
}
33+
34+
if i == SIZE {
35+
break;
36+
}
37+
38+
thread::yield_now();
39+
}
40+
41+
th.join().unwrap();
42+
}

0 commit comments

Comments
 (0)