Skip to content

Commit a616a8b

Browse files
authored
Unrolled build for rust-lang#128942
Rollup merge of rust-lang#128942 - RalfJung:interpret-weak-memory, r=saethlin miri weak memory emulation: put previous value into initial store buffer Fixes rust-lang/miri#2164 by doing a read before each atomic write so that we can initialize the store buffer. The read suppresses memory access hooks and UB exceptions, to avoid otherwise influencing the program behavior. If the read fails, we store that as `None` in the store buffer, so that when an atomic read races with the first atomic write to some memory and previously the memory was uninitialized, we can report UB due to reading uninit memory. ``@cbeuw`` this changes a bit the way we initialize the store buffers. Not sure if you still remember all this code, but if you could have a look to make sure this still makes sense, that would be great. :) r? ``@saethlin``
2 parents bf662eb + 8b18c6b commit a616a8b

File tree

6 files changed

+153
-109
lines changed

6 files changed

+153
-109
lines changed

compiler/rustc_const_eval/src/interpret/memory.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
10141014
///
10151015
/// We do this so Miri's allocation access tracking does not show the validation
10161016
/// reads as spurious accesses.
1017-
pub(super) fn run_for_validation<R>(&self, f: impl FnOnce() -> R) -> R {
1017+
pub fn run_for_validation<R>(&self, f: impl FnOnce() -> R) -> R {
10181018
// This deliberately uses `==` on `bool` to follow the pattern
10191019
// `assert!(val.replace(new) == old)`.
10201020
assert!(

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

+10-9
Original file line numberDiff line numberDiff line change
@@ -617,9 +617,10 @@ pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> {
617617
// the *value* (including the associated provenance if this is an AtomicPtr) at this location.
618618
// Only metadata on the location itself is used.
619619
let scalar = this.allow_data_races_ref(move |this| this.read_scalar(place))?;
620-
this.buffered_atomic_read(place, atomic, scalar, || {
620+
let buffered_scalar = this.buffered_atomic_read(place, atomic, scalar, || {
621621
this.validate_atomic_load(place, atomic)
622-
})
622+
})?;
623+
Ok(buffered_scalar.ok_or_else(|| err_ub!(InvalidUninitBytes(None)))?)
623624
}
624625

625626
/// Perform an atomic write operation at the memory location.
@@ -632,14 +633,14 @@ pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> {
632633
let this = self.eval_context_mut();
633634
this.atomic_access_check(dest, AtomicAccessType::Store)?;
634635

636+
// Read the previous value so we can put it in the store buffer later.
637+
// The program didn't actually do a read, so suppress the memory access hooks.
638+
// This is also a very special exception where we just ignore an error -- if this read
639+
// was UB e.g. because the memory is uninitialized, we don't want to know!
640+
let old_val = this.run_for_validation(|| this.read_scalar(dest)).ok();
635641
this.allow_data_races_mut(move |this| this.write_scalar(val, dest))?;
636642
this.validate_atomic_store(dest, atomic)?;
637-
// FIXME: it's not possible to get the value before write_scalar. A read_scalar will cause
638-
// side effects from a read the program did not perform. So we have to initialise
639-
// the store buffer with the value currently being written
640-
// ONCE this is fixed please remove the hack in buffered_atomic_write() in weak_memory.rs
641-
// https://github.com/rust-lang/miri/issues/2164
642-
this.buffered_atomic_write(val, dest, atomic, val)
643+
this.buffered_atomic_write(val, dest, atomic, old_val)
643644
}
644645

645646
/// Perform an atomic RMW operation on a memory location.
@@ -768,7 +769,7 @@ pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> {
768769
// in the modification order.
769770
// Since `old` is only a value and not the store element, we need to separately
770771
// find it in our store buffer and perform load_impl on it.
771-
this.perform_read_on_buffered_latest(place, fail, old.to_scalar())?;
772+
this.perform_read_on_buffered_latest(place, fail)?;
772773
}
773774

774775
// Return the old value.

src/tools/miri/src/concurrency/weak_memory.rs

+76-88
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,10 @@
3939
//! to attach store buffers to atomic objects. However, Rust follows LLVM in that it only has
4040
//! 'atomic accesses'. Therefore Miri cannot know when and where atomic 'objects' are being
4141
//! created or destroyed, to manage its store buffers. Instead, we hence lazily create an
42-
//! atomic object on the first atomic access to a given region, and we destroy that object
43-
//! on the next non-atomic or imperfectly overlapping atomic access to that region.
42+
//! atomic object on the first atomic write to a given region, and we destroy that object
43+
//! on the next non-atomic or imperfectly overlapping atomic write to that region.
4444
//! These lazy (de)allocations happen in memory_accessed() on non-atomic accesses, and
45-
//! get_or_create_store_buffer() on atomic accesses. This mostly works well, but it does
46-
//! lead to some issues (<https://github.com/rust-lang/miri/issues/2164>).
45+
//! get_or_create_store_buffer_mut() on atomic writes.
4746
//!
4847
//! One consequence of this difference is that safe/sound Rust allows for more operations on atomic locations
4948
//! than the C++20 atomic API was intended to allow, such as non-atomically accessing
@@ -144,11 +143,9 @@ struct StoreElement {
144143

145144
/// The timestamp of the storing thread when it performed the store
146145
timestamp: VTimestamp,
147-
/// The value of this store
148-
// FIXME: this means the store must be fully initialized;
149-
// we will have to change this if we want to support atomics on
150-
// (partially) uninitialized data.
151-
val: Scalar,
146+
/// The value of this store. `None` means uninitialized.
147+
// FIXME: Currently, we cannot represent partial initialization.
148+
val: Option<Scalar>,
152149

153150
/// Metadata about loads from this store element,
154151
/// behind a RefCell to keep load op take &self
@@ -170,7 +167,7 @@ impl StoreBufferAlloc {
170167

171168
/// When a non-atomic access happens on a location that has been atomically accessed
172169
/// before without data race, we can determine that the non-atomic access fully happens
173-
/// after all the prior atomic accesses so the location no longer needs to exhibit
170+
/// after all the prior atomic writes so the location no longer needs to exhibit
174171
/// any weak memory behaviours until further atomic accesses.
175172
pub fn memory_accessed(&self, range: AllocRange, global: &DataRaceState) {
176173
if !global.ongoing_action_data_race_free() {
@@ -192,37 +189,29 @@ impl StoreBufferAlloc {
192189
}
193190
}
194191

195-
/// Gets a store buffer associated with an atomic object in this allocation,
196-
/// or creates one with the specified initial value if no atomic object exists yet.
197-
fn get_or_create_store_buffer<'tcx>(
192+
/// Gets a store buffer associated with an atomic object in this allocation.
193+
/// Returns `None` if there is no store buffer.
194+
fn get_store_buffer<'tcx>(
198195
&self,
199196
range: AllocRange,
200-
init: Scalar,
201-
) -> InterpResult<'tcx, Ref<'_, StoreBuffer>> {
197+
) -> InterpResult<'tcx, Option<Ref<'_, StoreBuffer>>> {
202198
let access_type = self.store_buffers.borrow().access_type(range);
203199
let pos = match access_type {
204200
AccessType::PerfectlyOverlapping(pos) => pos,
205-
AccessType::Empty(pos) => {
206-
let mut buffers = self.store_buffers.borrow_mut();
207-
buffers.insert_at_pos(pos, range, StoreBuffer::new(init));
208-
pos
209-
}
210-
AccessType::ImperfectlyOverlapping(pos_range) => {
211-
// Once we reach here we would've already checked that this access is not racy.
212-
let mut buffers = self.store_buffers.borrow_mut();
213-
buffers.remove_pos_range(pos_range.clone());
214-
buffers.insert_at_pos(pos_range.start, range, StoreBuffer::new(init));
215-
pos_range.start
216-
}
201+
// If there is nothing here yet, that means there wasn't an atomic write yet so
202+
// we can't return anything outdated.
203+
_ => return Ok(None),
217204
};
218-
Ok(Ref::map(self.store_buffers.borrow(), |buffer| &buffer[pos]))
205+
let store_buffer = Ref::map(self.store_buffers.borrow(), |buffer| &buffer[pos]);
206+
Ok(Some(store_buffer))
219207
}
220208

221-
/// Gets a mutable store buffer associated with an atomic object in this allocation
209+
/// Gets a mutable store buffer associated with an atomic object in this allocation,
210+
/// or creates one with the specified initial value if no atomic object exists yet.
222211
fn get_or_create_store_buffer_mut<'tcx>(
223212
&mut self,
224213
range: AllocRange,
225-
init: Scalar,
214+
init: Option<Scalar>,
226215
) -> InterpResult<'tcx, &mut StoreBuffer> {
227216
let buffers = self.store_buffers.get_mut();
228217
let access_type = buffers.access_type(range);
@@ -244,10 +233,8 @@ impl StoreBufferAlloc {
244233
}
245234

246235
impl<'tcx> StoreBuffer {
247-
fn new(init: Scalar) -> Self {
236+
fn new(init: Option<Scalar>) -> Self {
248237
let mut buffer = VecDeque::new();
249-
buffer.reserve(STORE_BUFFER_LIMIT);
250-
let mut ret = Self { buffer };
251238
let store_elem = StoreElement {
252239
// The thread index and timestamp of the initialisation write
253240
// are never meaningfully used, so it's fine to leave them as 0
@@ -257,11 +244,11 @@ impl<'tcx> StoreBuffer {
257244
is_seqcst: false,
258245
load_info: RefCell::new(LoadInfo::default()),
259246
};
260-
ret.buffer.push_back(store_elem);
261-
ret
247+
buffer.push_back(store_elem);
248+
Self { buffer }
262249
}
263250

264-
/// Reads from the last store in modification order
251+
/// Reads from the last store in modification order, if any.
265252
fn read_from_last_store(
266253
&self,
267254
global: &DataRaceState,
@@ -282,7 +269,7 @@ impl<'tcx> StoreBuffer {
282269
is_seqcst: bool,
283270
rng: &mut (impl rand::Rng + ?Sized),
284271
validate: impl FnOnce() -> InterpResult<'tcx>,
285-
) -> InterpResult<'tcx, (Scalar, LoadRecency)> {
272+
) -> InterpResult<'tcx, (Option<Scalar>, LoadRecency)> {
286273
// Having a live borrow to store_buffer while calling validate_atomic_load is fine
287274
// because the race detector doesn't touch store_buffer
288275

@@ -419,15 +406,15 @@ impl<'tcx> StoreBuffer {
419406
// In the language provided in the paper, an atomic store takes the value from a
420407
// non-atomic memory location.
421408
// But we already have the immediate value here so we don't need to do the memory
422-
// access
423-
val,
409+
// access.
410+
val: Some(val),
424411
is_seqcst,
425412
load_info: RefCell::new(LoadInfo::default()),
426413
};
427-
self.buffer.push_back(store_elem);
428-
if self.buffer.len() > STORE_BUFFER_LIMIT {
414+
if self.buffer.len() >= STORE_BUFFER_LIMIT {
429415
self.buffer.pop_front();
430416
}
417+
self.buffer.push_back(store_elem);
431418
if is_seqcst {
432419
// Every store that happens before this needs to be marked as SC
433420
// so that in a later SC load, only the last SC store (i.e. this one) or stores that
@@ -450,7 +437,12 @@ impl StoreElement {
450437
/// buffer regardless of subsequent loads by the same thread; if the earliest load of another
451438
/// thread doesn't happen before the current one, then no subsequent load by the other thread
452439
/// can happen before the current one.
453-
fn load_impl(&self, index: VectorIdx, clocks: &ThreadClockSet, is_seqcst: bool) -> Scalar {
440+
fn load_impl(
441+
&self,
442+
index: VectorIdx,
443+
clocks: &ThreadClockSet,
444+
is_seqcst: bool,
445+
) -> Option<Scalar> {
454446
let mut load_info = self.load_info.borrow_mut();
455447
load_info.sc_loaded |= is_seqcst;
456448
let _ = load_info.timestamps.try_insert(index, clocks.clock[index]);
@@ -479,7 +471,7 @@ pub(super) trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
479471
global.sc_write(threads);
480472
}
481473
let range = alloc_range(base_offset, place.layout.size);
482-
let buffer = alloc_buffers.get_or_create_store_buffer_mut(range, init)?;
474+
let buffer = alloc_buffers.get_or_create_store_buffer_mut(range, Some(init))?;
483475
buffer.read_from_last_store(global, threads, atomic == AtomicRwOrd::SeqCst);
484476
buffer.buffered_write(new_val, global, threads, atomic == AtomicRwOrd::SeqCst)?;
485477
}
@@ -492,47 +484,55 @@ pub(super) trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
492484
atomic: AtomicReadOrd,
493485
latest_in_mo: Scalar,
494486
validate: impl FnOnce() -> InterpResult<'tcx>,
495-
) -> InterpResult<'tcx, Scalar> {
487+
) -> InterpResult<'tcx, Option<Scalar>> {
496488
let this = self.eval_context_ref();
497-
if let Some(global) = &this.machine.data_race {
498-
let (alloc_id, base_offset, ..) = this.ptr_get_alloc_id(place.ptr(), 0)?;
499-
if let Some(alloc_buffers) = this.get_alloc_extra(alloc_id)?.weak_memory.as_ref() {
500-
if atomic == AtomicReadOrd::SeqCst {
501-
global.sc_read(&this.machine.threads);
502-
}
503-
let mut rng = this.machine.rng.borrow_mut();
504-
let buffer = alloc_buffers.get_or_create_store_buffer(
505-
alloc_range(base_offset, place.layout.size),
506-
latest_in_mo,
507-
)?;
508-
let (loaded, recency) = buffer.buffered_read(
509-
global,
510-
&this.machine.threads,
511-
atomic == AtomicReadOrd::SeqCst,
512-
&mut *rng,
513-
validate,
514-
)?;
515-
if global.track_outdated_loads && recency == LoadRecency::Outdated {
516-
this.emit_diagnostic(NonHaltingDiagnostic::WeakMemoryOutdatedLoad {
517-
ptr: place.ptr(),
518-
});
489+
'fallback: {
490+
if let Some(global) = &this.machine.data_race {
491+
let (alloc_id, base_offset, ..) = this.ptr_get_alloc_id(place.ptr(), 0)?;
492+
if let Some(alloc_buffers) = this.get_alloc_extra(alloc_id)?.weak_memory.as_ref() {
493+
if atomic == AtomicReadOrd::SeqCst {
494+
global.sc_read(&this.machine.threads);
495+
}
496+
let mut rng = this.machine.rng.borrow_mut();
497+
let Some(buffer) = alloc_buffers
498+
.get_store_buffer(alloc_range(base_offset, place.layout.size))?
499+
else {
500+
// No old writes available, fall back to base case.
501+
break 'fallback;
502+
};
503+
let (loaded, recency) = buffer.buffered_read(
504+
global,
505+
&this.machine.threads,
506+
atomic == AtomicReadOrd::SeqCst,
507+
&mut *rng,
508+
validate,
509+
)?;
510+
if global.track_outdated_loads && recency == LoadRecency::Outdated {
511+
this.emit_diagnostic(NonHaltingDiagnostic::WeakMemoryOutdatedLoad {
512+
ptr: place.ptr(),
513+
});
514+
}
515+
516+
return Ok(loaded);
519517
}
520-
521-
return Ok(loaded);
522518
}
523519
}
524520

525521
// Race detector or weak memory disabled, simply read the latest value
526522
validate()?;
527-
Ok(latest_in_mo)
523+
Ok(Some(latest_in_mo))
528524
}
529525

526+
/// Add the given write to the store buffer. (Does not change machine memory.)
527+
///
528+
/// `init` says with which value to initialize the store buffer in case there wasn't a store
529+
/// buffer for this memory range before.
530530
fn buffered_atomic_write(
531531
&mut self,
532532
val: Scalar,
533533
dest: &MPlaceTy<'tcx>,
534534
atomic: AtomicWriteOrd,
535-
init: Scalar,
535+
init: Option<Scalar>,
536536
) -> InterpResult<'tcx> {
537537
let this = self.eval_context_mut();
538538
let (alloc_id, base_offset, ..) = this.ptr_get_alloc_id(dest.ptr(), 0)?;
@@ -545,23 +545,8 @@ pub(super) trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
545545
global.sc_write(threads);
546546
}
547547

548-
// UGLY HACK: in write_scalar_atomic() we don't know the value before our write,
549-
// so init == val always. If the buffer is fresh then we would've duplicated an entry,
550-
// so we need to remove it.
551-
// See https://github.com/rust-lang/miri/issues/2164
552-
let was_empty = matches!(
553-
alloc_buffers
554-
.store_buffers
555-
.borrow()
556-
.access_type(alloc_range(base_offset, dest.layout.size)),
557-
AccessType::Empty(_)
558-
);
559548
let buffer = alloc_buffers
560549
.get_or_create_store_buffer_mut(alloc_range(base_offset, dest.layout.size), init)?;
561-
if was_empty {
562-
buffer.buffer.pop_front();
563-
}
564-
565550
buffer.buffered_write(val, global, threads, atomic == AtomicWriteOrd::SeqCst)?;
566551
}
567552

@@ -576,7 +561,6 @@ pub(super) trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
576561
&self,
577562
place: &MPlaceTy<'tcx>,
578563
atomic: AtomicReadOrd,
579-
init: Scalar,
580564
) -> InterpResult<'tcx> {
581565
let this = self.eval_context_ref();
582566

@@ -587,8 +571,12 @@ pub(super) trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
587571
let size = place.layout.size;
588572
let (alloc_id, base_offset, ..) = this.ptr_get_alloc_id(place.ptr(), 0)?;
589573
if let Some(alloc_buffers) = this.get_alloc_extra(alloc_id)?.weak_memory.as_ref() {
590-
let buffer = alloc_buffers
591-
.get_or_create_store_buffer(alloc_range(base_offset, size), init)?;
574+
let Some(buffer) =
575+
alloc_buffers.get_store_buffer(alloc_range(base_offset, size))?
576+
else {
577+
// No store buffer, nothing to do.
578+
return Ok(());
579+
};
592580
buffer.read_from_last_store(
593581
global,
594582
&this.machine.threads,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//@compile-flags: -Zmiri-ignore-leaks -Zmiri-preemption-rate=0
2+
3+
// Tests showing weak memory behaviours are exhibited. All tests
4+
// return true when the desired behaviour is seen.
5+
// This is scheduler and pseudo-RNG dependent, so each test is
6+
// run multiple times until one try returns true.
7+
// Spurious failure is possible, if you are really unlucky with
8+
// the RNG and always read the latest value from the store buffer.
9+
#![feature(new_uninit)]
10+
11+
use std::sync::atomic::*;
12+
use std::thread::spawn;
13+
14+
#[allow(dead_code)]
15+
#[derive(Copy, Clone)]
16+
struct EvilSend<T>(pub T);
17+
18+
unsafe impl<T> Send for EvilSend<T> {}
19+
unsafe impl<T> Sync for EvilSend<T> {}
20+
21+
// We can't create static items because we need to run each test multiple times.
22+
fn static_uninit_atomic() -> &'static AtomicUsize {
23+
unsafe { Box::leak(Box::new_uninit()).assume_init_ref() }
24+
}
25+
26+
fn relaxed() {
27+
let x = static_uninit_atomic();
28+
let j1 = spawn(move || {
29+
x.store(1, Ordering::Relaxed);
30+
});
31+
32+
let j2 = spawn(move || x.load(Ordering::Relaxed)); //~ERROR: using uninitialized data
33+
34+
j1.join().unwrap();
35+
j2.join().unwrap();
36+
}
37+
38+
pub fn main() {
39+
// If we try often enough, we should hit UB.
40+
for _ in 0..100 {
41+
relaxed();
42+
}
43+
}

0 commit comments

Comments
 (0)