Skip to content

Commit 43eeb11

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 080eb0d + 5a88938 commit 43eeb11

18 files changed

+262
-275
lines changed

src/concurrency/data_race.rs

Lines changed: 33 additions & 30 deletions
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
};

tests/fail/data_race/mixed_size_read.rs

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
error: Undefined Behavior: Race condition detected between (1) multiple differently-sized atomic loads, including one load on thread `unnamed-ID` and (2) 2-byte atomic store on thread `unnamed-ID` at ALLOC. (2) just happened here
2+
--> tests/fail/data_race/mixed_size_read_read_write.rs:LL:CC
3+
|
4+
LL | a16.store(0, Ordering::SeqCst);
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Race condition detected between (1) multiple differently-sized atomic loads, including one load on thread `unnamed-ID` and (2) 2-byte atomic store on thread `unnamed-ID` at ALLOC. (2) just happened here
6+
|
7+
help: and (1) occurred earlier here
8+
--> tests/fail/data_race/mixed_size_read_read_write.rs:LL:CC
9+
|
10+
LL | a16.load(Ordering::SeqCst);
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
= help: overlapping unsynchronized atomic accesses must use the same access size
13+
= help: see https://doc.rust-lang.org/nightly/std/sync/atomic/index.html#memory-model-for-atomic-accesses for more information about the Rust memory model
14+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
15+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
16+
= note: BACKTRACE (of the first span) on thread `unnamed-ID`:
17+
= note: inside closure at tests/fail/data_race/mixed_size_read_read_write.rs:LL:CC
18+
19+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
20+
21+
error: aborting due to 1 previous error
22+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
error: Undefined Behavior: Race condition detected between (1) multiple differently-sized atomic loads, including one load on thread `unnamed-ID` and (2) 1-byte atomic store on thread `unnamed-ID` at ALLOC. (2) just happened here
2+
--> tests/fail/data_race/mixed_size_read_read_write.rs:LL:CC
3+
|
4+
LL | a8[0].store(0, Ordering::SeqCst);
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Race condition detected between (1) multiple differently-sized atomic loads, including one load on thread `unnamed-ID` and (2) 1-byte atomic store on thread `unnamed-ID` at ALLOC. (2) just happened here
6+
|
7+
help: and (1) occurred earlier here
8+
--> tests/fail/data_race/mixed_size_read_read_write.rs:LL:CC
9+
|
10+
LL | a16.load(Ordering::SeqCst);
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
= help: overlapping unsynchronized atomic accesses must use the same access size
13+
= help: see https://doc.rust-lang.org/nightly/std/sync/atomic/index.html#memory-model-for-atomic-accesses for more information about the Rust memory model
14+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
15+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
16+
= note: BACKTRACE (of the first span) on thread `unnamed-ID`:
17+
= note: inside closure at tests/fail/data_race/mixed_size_read_read_write.rs:LL:CC
18+
19+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
20+
21+
error: aborting due to 1 previous error
22+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//@compile-flags: -Zmiri-preemption-rate=0.0 -Zmiri-disable-weak-memory-emulation
2+
// Avoid accidental synchronization via address reuse inside `thread::spawn`.
3+
//@compile-flags: -Zmiri-address-reuse-cross-thread-rate=0
4+
// Two variants: the atomic store matches the size of the first or second atomic load.
5+
//@revisions: match_first_load match_second_load
6+
7+
use std::sync::atomic::{AtomicU16, AtomicU8, Ordering};
8+
use std::thread;
9+
10+
fn convert(a: &AtomicU16) -> &[AtomicU8; 2] {
11+
unsafe { std::mem::transmute(a) }
12+
}
13+
14+
// We can't allow mixed-size accesses; they are not possible in C++ and even
15+
// Intel says you shouldn't do it.
16+
fn main() {
17+
let a = AtomicU16::new(0);
18+
let a16 = &a;
19+
let a8 = convert(a16);
20+
21+
thread::scope(|s| {
22+
s.spawn(|| {
23+
a16.load(Ordering::SeqCst);
24+
});
25+
s.spawn(|| {
26+
a8[0].load(Ordering::SeqCst);
27+
});
28+
s.spawn(|| {
29+
thread::yield_now(); // make sure this happens last
30+
if cfg!(match_first_load) {
31+
a16.store(0, Ordering::SeqCst);
32+
//~[match_first_load]^ ERROR: Race condition detected between (1) multiple differently-sized atomic loads, including one load on thread `unnamed-1` and (2) 2-byte atomic store on thread `unnamed-3`
33+
} else {
34+
a8[0].store(0, Ordering::SeqCst);
35+
//~[match_second_load]^ ERROR: Race condition detected between (1) multiple differently-sized atomic loads, including one load on thread `unnamed-1` and (2) 1-byte atomic store on thread `unnamed-3`
36+
}
37+
});
38+
});
39+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
error: Undefined Behavior: Race condition detected between (1) 1-byte atomic load on thread `unnamed-ID` and (2) 2-byte atomic store on thread `unnamed-ID` at ALLOC. (2) just happened here
2+
--> tests/fail/data_race/mixed_size_read_write.rs:LL:CC
3+
|
4+
LL | a16.store(1, Ordering::SeqCst);
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Race condition detected between (1) 1-byte atomic load on thread `unnamed-ID` and (2) 2-byte atomic store on thread `unnamed-ID` at ALLOC. (2) just happened here
6+
|
7+
help: and (1) occurred earlier here
8+
--> tests/fail/data_race/mixed_size_read_write.rs:LL:CC
9+
|
10+
LL | a8[0].load(Ordering::SeqCst);
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
= help: overlapping unsynchronized atomic accesses must use the same access size
13+
= help: see https://doc.rust-lang.org/nightly/std/sync/atomic/index.html#memory-model-for-atomic-accesses for more information about the Rust memory model
14+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
15+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
16+
= note: BACKTRACE (of the first span) on thread `unnamed-ID`:
17+
= note: inside closure at tests/fail/data_race/mixed_size_read_write.rs:LL:CC
18+
19+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
20+
21+
error: aborting due to 1 previous error
22+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//@compile-flags: -Zmiri-preemption-rate=0.0 -Zmiri-disable-weak-memory-emulation
2+
// Avoid accidental synchronization via address reuse inside `thread::spawn`.
3+
//@compile-flags: -Zmiri-address-reuse-cross-thread-rate=0
4+
// Two revisions, depending on which access goes first.
5+
//@revisions: read_write write_read
6+
7+
use std::sync::atomic::{AtomicU16, AtomicU8, Ordering};
8+
use std::thread;
9+
10+
fn convert(a: &AtomicU16) -> &[AtomicU8; 2] {
11+
unsafe { std::mem::transmute(a) }
12+
}
13+
14+
// We can't allow mixed-size accesses; they are not possible in C++ and even
15+
// Intel says you shouldn't do it.
16+
fn main() {
17+
let a = AtomicU16::new(0);
18+
let a16 = &a;
19+
let a8 = convert(a16);
20+
21+
thread::scope(|s| {
22+
s.spawn(|| {
23+
if cfg!(read_write) {
24+
// Let the other one go first.
25+
thread::yield_now();
26+
}
27+
a16.store(1, Ordering::SeqCst);
28+
//~[read_write]^ ERROR: Race condition detected between (1) 1-byte atomic load on thread `unnamed-2` and (2) 2-byte atomic store on thread `unnamed-1`
29+
});
30+
s.spawn(|| {
31+
if cfg!(write_read) {
32+
// Let the other one go first.
33+
thread::yield_now();
34+
}
35+
a8[0].load(Ordering::SeqCst);
36+
//~[write_read]^ ERROR: Race condition detected between (1) 2-byte atomic store on thread `unnamed-1` and (2) 1-byte atomic load on thread `unnamed-2`
37+
});
38+
});
39+
}

tests/fail/data_race/mixed_size_read.stderr renamed to tests/fail/data_race/mixed_size_read_write.write_read.stderr

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
error: Undefined Behavior: Race condition detected between (1) 2-byte atomic load on thread `unnamed-ID` and (2) 1-byte atomic load on thread `unnamed-ID` at ALLOC. (2) just happened here
2-
--> tests/fail/data_race/mixed_size_read.rs:LL:CC
1+
error: Undefined Behavior: Race condition detected between (1) 2-byte atomic store on thread `unnamed-ID` and (2) 1-byte atomic load on thread `unnamed-ID` at ALLOC. (2) just happened here
2+
--> tests/fail/data_race/mixed_size_read_write.rs:LL:CC
33
|
44
LL | a8[0].load(Ordering::SeqCst);
5-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Race condition detected between (1) 2-byte atomic load on thread `unnamed-ID` and (2) 1-byte atomic load on thread `unnamed-ID` at ALLOC. (2) just happened here
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Race condition detected between (1) 2-byte atomic store on thread `unnamed-ID` and (2) 1-byte atomic load on thread `unnamed-ID` at ALLOC. (2) just happened here
66
|
77
help: and (1) occurred earlier here
8-
--> tests/fail/data_race/mixed_size_read.rs:LL:CC
8+
--> tests/fail/data_race/mixed_size_read_write.rs:LL:CC
99
|
10-
LL | a16.load(Ordering::SeqCst);
11-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
10+
LL | a16.store(1, Ordering::SeqCst);
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1212
= help: overlapping unsynchronized atomic accesses must use the same access size
1313
= help: see https://doc.rust-lang.org/nightly/std/sync/atomic/index.html#memory-model-for-atomic-accesses for more information about the Rust memory model
1414
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
1515
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
1616
= note: BACKTRACE (of the first span) on thread `unnamed-ID`:
17-
= note: inside closure at tests/fail/data_race/mixed_size_read.rs:LL:CC
17+
= note: inside closure at tests/fail/data_race/mixed_size_read_write.rs:LL:CC
1818

1919
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
2020

0 commit comments

Comments
 (0)