Skip to content

Commit 5e4eab4

Browse files
authored
Rollup merge of #128778 - RalfJung:atomic-read-read-races, r=Mark-Simulacrum
atomics: allow atomic and non-atomic reads to race We currently define our atomics in terms of C++ `atomic_ref`. That has the unfortunate side-effect of making it UB for an atomic and a non-atomic read to race (concretely, [this code](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d1a743774e60923db33def7fe314d754) has UB). There's really no good reason for this, all the academic models of the C++ memory model I am aware of allow this -- C++ just disallows this because of their insistence on an "object model" with typed memory, where `atomic_ref` temporarily creates an "atomic object" that may not be accesses via regular non-atomic operations. So instead of tying our operations to `atomic_ref`, let us tie them directly to the underlying C++ memory model. I am not sure what is the best way to phrase this, so here's a first attempt. We also carve out an exception from the "no mixed-size atomic accesses" rule to permit mixed-size atomic reads -- given that we permit mixed-size non-atomic reads, it seems odd that this would be disallowed for atomic reads. However, when an atomic write races with any other atomic operation, they must use the same size. With this change, it is finally the case that every non-atomic access can be replaced by an atomic access without introducing UB. Cc `@rust-lang/opsem` `@chorman0773` `@m-ou-se` `@WaffleLapkin` `@Amanieu` Fixes rust-lang/unsafe-code-guidelines#483
2 parents 612796c + 96be76b commit 5e4eab4

20 files changed

+318
-310
lines changed

library/core/src/cell.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -1895,11 +1895,17 @@ impl<T: ?Sized + fmt::Display> fmt::Display for RefMut<'_, T> {
18951895
/// uniqueness guarantee for mutable references is unaffected. There is *no* legal way to obtain
18961896
/// aliasing `&mut`, not even with `UnsafeCell<T>`.
18971897
///
1898+
/// `UnsafeCell` does nothing to avoid data races; they are still undefined behavior. If multiple
1899+
/// threads have access to the same `UnsafeCell`, they must follow the usual rules of the
1900+
/// [concurrent memory model]: conflicting non-synchronized accesses must be done via the APIs in
1901+
/// [`core::sync::atomic`].
1902+
///
18981903
/// The `UnsafeCell` API itself is technically very simple: [`.get()`] gives you a raw pointer
18991904
/// `*mut T` to its contents. It is up to _you_ as the abstraction designer to use that raw pointer
19001905
/// correctly.
19011906
///
19021907
/// [`.get()`]: `UnsafeCell::get`
1908+
/// [concurrent memory model]: ../sync/atomic/index.html#memory-model-for-atomic-accesses
19031909
///
19041910
/// The precise Rust aliasing rules are somewhat in flux, but the main points are not contentious:
19051911
///
@@ -1922,10 +1928,6 @@ impl<T: ?Sized + fmt::Display> fmt::Display for RefMut<'_, T> {
19221928
/// live memory and the compiler is allowed to insert spurious reads if it can prove that this
19231929
/// memory has not yet been deallocated.
19241930
///
1925-
/// - At all times, you must avoid data races. If multiple threads have access to
1926-
/// the same `UnsafeCell`, then any writes must have a proper happens-before relation to all other
1927-
/// accesses (or use atomics).
1928-
///
19291931
/// To assist with proper design, the following scenarios are explicitly declared legal
19301932
/// for single-threaded code:
19311933
///

library/core/src/sync/atomic.rs

+50-31
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,42 @@
2424
//!
2525
//! ## Memory model for atomic accesses
2626
//!
27-
//! Rust atomics currently follow the same rules as [C++20 atomics][cpp], specifically `atomic_ref`.
28-
//! Basically, creating a *shared reference* to one of the Rust atomic types corresponds to creating
29-
//! an `atomic_ref` in C++; the `atomic_ref` is destroyed when the lifetime of the shared reference
30-
//! ends. A Rust atomic type that is exclusively owned or behind a mutable reference does *not*
31-
//! correspond to an “atomic object” in C++, since the underlying primitive can be mutably accessed,
32-
//! for example with `get_mut`, to perform non-atomic operations.
27+
//! Rust atomics currently follow the same rules as [C++20 atomics][cpp], specifically the rules
28+
//! from the [`intro.races`][cpp-intro.races] section, without the "consume" memory ordering. Since
29+
//! C++ uses an object-based memory model whereas Rust is access-based, a bit of translation work
30+
//! has to be done to apply the C++ rules to Rust: whenever C++ talks about "the value of an
31+
//! object", we understand that to mean the resulting bytes obtained when doing a read. When the C++
32+
//! standard talks about "the value of an atomic object", this refers to the result of doing an
33+
//! atomic load (via the operations provided in this module). A "modification of an atomic object"
34+
//! refers to an atomic store.
3335
//!
34-
//! [cpp]: https://en.cppreference.com/w/cpp/atomic
36+
//! The end result is *almost* equivalent to saying that creating a *shared reference* to one of the
37+
//! Rust atomic types corresponds to creating an `atomic_ref` in C++, with the `atomic_ref` being
38+
//! destroyed when the lifetime of the shared reference ends. The main difference is that Rust
39+
//! permits concurrent atomic and non-atomic reads to the same memory as those cause no issue in the
40+
//! C++ memory model, they are just forbidden in C++ because memory is partitioned into "atomic
41+
//! objects" and "non-atomic objects" (with `atomic_ref` temporarily converting a non-atomic object
42+
//! into an atomic object).
43+
//!
44+
//! The most important aspect of this model is that *data races* are undefined behavior. A data race
45+
//! is defined as conflicting non-synchronized accesses where at least one of the accesses is
46+
//! non-atomic. Here, accesses are *conflicting* if they affect overlapping regions of memory and at
47+
//! least one of them is a write. They are *non-synchronized* if neither of them *happens-before*
48+
//! the other, according to the happens-before order of the memory model.
3549
//!
36-
//! Each method takes an [`Ordering`] which represents the strength of
37-
//! the memory barrier for that operation. These orderings are the
38-
//! same as the [C++20 atomic orderings][1]. For more information see the [nomicon][2].
50+
//! The other possible cause of undefined behavior in the memory model are mixed-size accesses: Rust
51+
//! inherits the C++ limitation that non-synchronized conflicting atomic accesses may not partially
52+
//! overlap. In other words, every pair of non-synchronized atomic accesses must be either disjoint,
53+
//! access the exact same memory (including using the same access size), or both be reads.
3954
//!
40-
//! [1]: https://en.cppreference.com/w/cpp/atomic/memory_order
41-
//! [2]: ../../../nomicon/atomics.html
55+
//! Each atomic access takes an [`Ordering`] which defines how the operation interacts with the
56+
//! happens-before order. These orderings behave the same as the corresponding [C++20 atomic
57+
//! orderings][cpp_memory_order]. For more information, see the [nomicon].
4258
//!
43-
//! Since C++ does not support mixing atomic and non-atomic accesses, or non-synchronized
44-
//! different-sized accesses to the same data, Rust does not support those operations either.
45-
//! Note that both of those restrictions only apply if the accesses are non-synchronized.
59+
//! [cpp]: https://en.cppreference.com/w/cpp/atomic
60+
//! [cpp-intro.races]: https://timsong-cpp.github.io/cppwp/n4868/intro.multithread#intro.races
61+
//! [cpp_memory_order]: https://en.cppreference.com/w/cpp/atomic/memory_order
62+
//! [nomicon]: ../../../nomicon/atomics.html
4663
//!
4764
//! ```rust,no_run undefined_behavior
4865
//! use std::sync::atomic::{AtomicU16, AtomicU8, Ordering};
@@ -52,27 +69,29 @@
5269
//! let atomic = AtomicU16::new(0);
5370
//!
5471
//! thread::scope(|s| {
55-
//! // This is UB: mixing atomic and non-atomic accesses
56-
//! s.spawn(|| atomic.store(1, Ordering::Relaxed));
57-
//! s.spawn(|| unsafe { atomic.as_ptr().write(2) });
72+
//! // This is UB: conflicting non-synchronized accesses, at least one of which is non-atomic.
73+
//! s.spawn(|| atomic.store(1, Ordering::Relaxed)); // atomic store
74+
//! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); // non-atomic write
5875
//! });
5976
//!
6077
//! thread::scope(|s| {
61-
//! // This is UB: even reads are not allowed to be mixed
62-
//! s.spawn(|| atomic.load(Ordering::Relaxed));
63-
//! s.spawn(|| unsafe { atomic.as_ptr().read() });
78+
//! // This is fine: the accesses do not conflict (as none of them performs any modification).
79+
//! // In C++ this would be disallowed since creating an `atomic_ref` precludes
80+
//! // further non-atomic accesses, but Rust does not have that limitation.
81+
//! s.spawn(|| atomic.load(Ordering::Relaxed)); // atomic load
82+
//! s.spawn(|| unsafe { atomic.as_ptr().read() }); // non-atomic read
6483
//! });
6584
//!
6685
//! thread::scope(|s| {
67-
//! // This is fine, `join` synchronizes the code in a way such that atomic
68-
//! // and non-atomic accesses can't happen "at the same time"
69-
//! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed));
70-
//! handle.join().unwrap();
71-
//! s.spawn(|| unsafe { atomic.as_ptr().write(2) });
86+
//! // This is fine: `join` synchronizes the code in a way such that the atomic
87+
//! // store happens-before the non-atomic write.
88+
//! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed)); // atomic store
89+
//! handle.join().unwrap(); // synchronize
90+
//! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); // non-atomic write
7291
//! });
7392
//!
7493
//! thread::scope(|s| {
75-
//! // This is UB: using different-sized atomic accesses to the same data
94+
//! // This is UB: non-synchronized conflicting differently-sized atomic accesses.
7695
//! s.spawn(|| atomic.store(1, Ordering::Relaxed));
7796
//! s.spawn(|| unsafe {
7897
//! let differently_sized = transmute::<&AtomicU16, &AtomicU8>(&atomic);
@@ -81,8 +100,8 @@
81100
//! });
82101
//!
83102
//! thread::scope(|s| {
84-
//! // This is fine, `join` synchronizes the code in a way such that
85-
//! // differently-sized accesses can't happen "at the same time"
103+
//! // This is fine: `join` synchronizes the code in a way such that
104+
//! // the 1-byte store happens-before the 2-byte store.
86105
//! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed));
87106
//! handle.join().unwrap();
88107
//! s.spawn(|| unsafe {
@@ -137,7 +156,7 @@
137156
//!
138157
//! # Atomic accesses to read-only memory
139158
//!
140-
//! In general, *all* atomic accesses on read-only memory are Undefined Behavior. For instance, attempting
159+
//! In general, *all* atomic accesses on read-only memory are undefined behavior. For instance, attempting
141160
//! to do a `compare_exchange` that will definitely fail (making it conceptually a read-only
142161
//! operation) can still cause a segmentation fault if the underlying memory page is mapped read-only. Since
143162
//! atomic `load`s might be implemented using compare-exchange operations, even a `load` can fault
@@ -153,7 +172,7 @@
153172
//!
154173
//! As an exception from the general rule stated above, "sufficiently small" atomic loads with
155174
//! `Ordering::Relaxed` are implemented in a way that works on read-only memory, and are hence not
156-
//! Undefined Behavior. The exact size limit for what makes a load "sufficiently small" varies
175+
//! undefined behavior. The exact size limit for what makes a load "sufficiently small" varies
157176
//! depending on the target:
158177
//!
159178
//! | `target_arch` | Size limit |

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

+33-30
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ struct AtomicMemoryCellClocks {
191191
/// The size of accesses to this atomic location.
192192
/// We use this to detect non-synchronized mixed-size accesses. Since all accesses must be
193193
/// aligned to their size, this is sufficient to detect imperfectly overlapping accesses.
194-
size: Size,
194+
/// `None` indicates that we saw multiple different sizes, which is okay as long as all accesses are reads.
195+
size: Option<Size>,
195196
}
196197

197198
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -265,6 +266,14 @@ impl AccessType {
265266
let mut msg = String::new();
266267

267268
if let Some(size) = size {
269+
if size == Size::ZERO {
270+
// In this case there were multiple read accesss with different sizes and then a write.
271+
// We will be reporting *one* of the other reads, but we don't have enough information
272+
// to determine which one had which size.
273+
assert!(self == AccessType::AtomicLoad);
274+
assert!(ty.is_none());
275+
return format!("multiple differently-sized atomic loads, including one load");
276+
}
268277
msg.push_str(&format!("{}-byte {}", size.bytes(), msg))
269278
}
270279

@@ -305,8 +314,7 @@ impl AccessType {
305314
}
306315
}
307316

308-
/// Memory Cell vector clock metadata
309-
/// for data-race detection.
317+
/// Per-byte vector clock metadata for data-race detection.
310318
#[derive(Clone, PartialEq, Eq, Debug)]
311319
struct MemoryCellClocks {
312320
/// The vector-clock timestamp and the thread that did the last non-atomic write. We don't need
@@ -325,8 +333,8 @@ struct MemoryCellClocks {
325333
read: VClock,
326334

327335
/// Atomic access, acquire, release sequence tracking clocks.
328-
/// For non-atomic memory in the common case this
329-
/// value is set to None.
336+
/// For non-atomic memory this value is set to None.
337+
/// For atomic memory, each byte carries this information.
330338
atomic_ops: Option<Box<AtomicMemoryCellClocks>>,
331339
}
332340

@@ -336,7 +344,7 @@ impl AtomicMemoryCellClocks {
336344
read_vector: Default::default(),
337345
write_vector: Default::default(),
338346
sync_vector: Default::default(),
339-
size,
347+
size: Some(size),
340348
}
341349
}
342350
}
@@ -383,17 +391,23 @@ impl MemoryCellClocks {
383391
&mut self,
384392
thread_clocks: &ThreadClockSet,
385393
size: Size,
394+
write: bool,
386395
) -> Result<&mut AtomicMemoryCellClocks, DataRace> {
387396
match self.atomic_ops {
388397
Some(ref mut atomic) => {
389398
// We are good if the size is the same or all atomic accesses are before our current time.
390-
if atomic.size == size {
399+
if atomic.size == Some(size) {
391400
Ok(atomic)
392401
} else if atomic.read_vector <= thread_clocks.clock
393402
&& atomic.write_vector <= thread_clocks.clock
394403
{
395-
// This is now the new size that must be used for accesses here.
396-
atomic.size = size;
404+
// We are fully ordered after all previous accesses, so we can change the size.
405+
atomic.size = Some(size);
406+
Ok(atomic)
407+
} else if !write && atomic.write_vector <= thread_clocks.clock {
408+
// This is a read, and it is ordered after the last write. It's okay for the
409+
// sizes to mismatch, as long as no writes with a different size occur later.
410+
atomic.size = None;
397411
Ok(atomic)
398412
} else {
399413
Err(DataRace)
@@ -499,7 +513,7 @@ impl MemoryCellClocks {
499513
Ok(())
500514
}
501515

502-
/// Detect data-races with an atomic read, caused by a non-atomic access that does
516+
/// Detect data-races with an atomic read, caused by a non-atomic write that does
503517
/// not happen-before the atomic-read.
504518
fn atomic_read_detect(
505519
&mut self,
@@ -508,14 +522,10 @@ impl MemoryCellClocks {
508522
access_size: Size,
509523
) -> Result<(), DataRace> {
510524
trace!("Atomic read with vectors: {:#?} :: {:#?}", self, thread_clocks);
511-
let atomic = self.atomic_access(thread_clocks, access_size)?;
525+
let atomic = self.atomic_access(thread_clocks, access_size, /*write*/ false)?;
512526
atomic.read_vector.set_at_index(&thread_clocks.clock, index);
513-
// Make sure the last non-atomic write and all non-atomic reads were before this access.
514-
if self.write_was_before(&thread_clocks.clock) && self.read <= thread_clocks.clock {
515-
Ok(())
516-
} else {
517-
Err(DataRace)
518-
}
527+
// Make sure the last non-atomic write was before this access.
528+
if self.write_was_before(&thread_clocks.clock) { Ok(()) } else { Err(DataRace) }
519529
}
520530

521531
/// Detect data-races with an atomic write, either with a non-atomic read or with
@@ -527,7 +537,7 @@ impl MemoryCellClocks {
527537
access_size: Size,
528538
) -> Result<(), DataRace> {
529539
trace!("Atomic write with vectors: {:#?} :: {:#?}", self, thread_clocks);
530-
let atomic = self.atomic_access(thread_clocks, access_size)?;
540+
let atomic = self.atomic_access(thread_clocks, access_size, /*write*/ true)?;
531541
atomic.write_vector.set_at_index(&thread_clocks.clock, index);
532542
// Make sure the last non-atomic write and all non-atomic reads were before this access.
533543
if self.write_was_before(&thread_clocks.clock) && self.read <= thread_clocks.clock {
@@ -552,11 +562,9 @@ impl MemoryCellClocks {
552562
}
553563
thread_clocks.clock.index_mut(index).set_read_type(read_type);
554564
if self.write_was_before(&thread_clocks.clock) {
565+
// We must be ordered-after all atomic writes.
555566
let race_free = if let Some(atomic) = self.atomic() {
556-
// We must be ordered-after all atomic accesses, reads and writes.
557-
// This ensures we don't mix atomic and non-atomic accesses.
558567
atomic.write_vector <= thread_clocks.clock
559-
&& atomic.read_vector <= thread_clocks.clock
560568
} else {
561569
true
562570
};
@@ -957,9 +965,7 @@ impl VClockAlloc {
957965
let mut other_size = None; // if `Some`, this was a size-mismatch race
958966
let write_clock;
959967
let (other_access, other_thread, other_clock) =
960-
// First check the atomic-nonatomic cases. If it looks like multiple
961-
// cases apply, this one should take precedence, else it might look like
962-
// we are reporting races between two non-atomic reads.
968+
// First check the atomic-nonatomic cases.
963969
if !access.is_atomic() &&
964970
let Some(atomic) = mem_clocks.atomic() &&
965971
let Some(idx) = Self::find_gt_index(&atomic.write_vector, &active_clocks.clock)
@@ -977,10 +983,10 @@ impl VClockAlloc {
977983
} else if let Some(idx) = Self::find_gt_index(&mem_clocks.read, &active_clocks.clock) {
978984
(AccessType::NaRead(mem_clocks.read[idx].read_type()), idx, &mem_clocks.read)
979985
// Finally, mixed-size races.
980-
} else if access.is_atomic() && let Some(atomic) = mem_clocks.atomic() && atomic.size != access_size {
986+
} else if access.is_atomic() && let Some(atomic) = mem_clocks.atomic() && atomic.size != Some(access_size) {
981987
// This is only a race if we are not synchronized with all atomic accesses, so find
982988
// the one we are not synchronized with.
983-
other_size = Some(atomic.size);
989+
other_size = Some(atomic.size.unwrap_or(Size::ZERO));
984990
if let Some(idx) = Self::find_gt_index(&atomic.write_vector, &active_clocks.clock)
985991
{
986992
(AccessType::AtomicStore, idx, &atomic.write_vector)
@@ -1007,10 +1013,7 @@ impl VClockAlloc {
10071013
assert!(!involves_non_atomic);
10081014
Some("overlapping unsynchronized atomic accesses must use the same access size")
10091015
} else if access.is_read() && other_access.is_read() {
1010-
assert!(involves_non_atomic);
1011-
Some(
1012-
"overlapping atomic and non-atomic accesses must be synchronized, even if both are read-only",
1013-
)
1016+
panic!("there should be no same-size read-read races")
10141017
} else {
10151018
None
10161019
};

src/tools/miri/tests/fail/data_race/mixed_size_read.rs

-28
This file was deleted.

0 commit comments

Comments
 (0)