Skip to content

Commit 401d897

Browse files
committed
Auto merge of #3054 - Vanille-N:spurious-fail, r=RalfJung
Issue discovered in TB: spurious reads are not (yet) possible in a concurrent setting We discovered a week ago that in general, the current model of TB does not allow spurious reads because although reads provably never invalidate other reads, they migh invalidate writes. Consider the code ```rs fn f1(x: &u8) {} fn f2(y: &mut u8) -> &mut u8 { &mut *y } let mut data = 0; let _ = thread::spawn(|| { f1(&mut data) }; let _ = thread::spawn(|| { let y = f2(&mut data); *y = 42; }); ``` of which one possible interleaving is ```rs 1: retag x (&, protect) // x: [P]Frozen 2: retag y (&mut, protect) // y: [P]Reserved, x: [P]Frozen 1: return f1 // x: [P]Frozen -> Frozen, y: [P]Reserved 2: return f2 // x: Frozen, y: [P]Reserved -> Reserved 2: write y // x: Disabled, y: Active ``` that does not have UB. Assume enough barriers to force this specific interleaving, and consider that the compiler could choose to insert a spurious read throug `x` during the call to `f1` which would produce ```rs 1: retag x (&, protect) // x: [P]Frozen 2: retag y (&mut, protect) // y: [P]Reserved, x: [P]Frozen 1: spurious read x // x: [P]Frozen, y: [P]Reserved -> [P]Frozen 1: return f1 // x: [P]Frozen -> Frozen, y: [P]Frozen 2: return f2 // x: Frozen, y: [P]Frozen -> Frozen 2: write y // UB ``` Thus the target of the optimization (with a spurious read) has UB when the source did not. This is bad. SB is not affected because the code would be UB as early as `retag y`, this happens because we're trying to be a bit more subtle than that, and because the effects of a foreign read on a protected `&mut` bleed outside of the boundaries of the protector. Fortunately we have a fix planned, but in the meantime here are some `#[should_panic]` exhaustive tests to illustrate the issue. The error message printed by the `#[should_panic]` tests flags the present issue in slightly more general terms: it says that the sequence `retag x (&, protect); retag y (&mut, protect);` produces the configuration `C_source := x: [P]Frozen, x: [P]Reserved`, and that inserting a spurious read through `x` turns it into `C_target := x: [P]Frozen, y: [P]Reserved`. It then says that `C_source` is distinguishable from `C_target`, which means that there exists a sequence of instructions applied to both that triggers UB in `C_target` but not in `C_source`. It happens that one such sequence is `1: return f1; 2: return f2; 2: write y;` as shown above, but it is not the only one, as for example the interleaving `1: return f1; 2: write y;` is also problematic.
2 parents 13510ac + 69272a8 commit 401d897

File tree

6 files changed

+917
-146
lines changed

6 files changed

+917
-146
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! Exhaustive testing utilities.
2+
//! (These are used in Tree Borrows `#[test]`s for thorough verification
3+
//! of the behavior of the state machine of permissions,
4+
//! but the contents of this file are extremely generic)
5+
#![cfg(test)]
6+
7+
pub trait Exhaustive: Sized {
8+
fn exhaustive() -> Box<dyn Iterator<Item = Self>>;
9+
}
10+
11+
macro_rules! precondition {
12+
($cond:expr) => {
13+
if !$cond {
14+
continue;
15+
}
16+
};
17+
}
18+
pub(crate) use precondition;
19+
20+
// Trivial impls of `Exhaustive` for the standard types with 0, 1 and 2 elements respectively.
21+
22+
impl Exhaustive for ! {
23+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
24+
Box::new(std::iter::empty())
25+
}
26+
}
27+
28+
impl Exhaustive for () {
29+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
30+
Box::new(std::iter::once(()))
31+
}
32+
}
33+
34+
impl Exhaustive for bool {
35+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
36+
Box::new(vec![true, false].into_iter())
37+
}
38+
}
39+
40+
// Some container impls for `Exhaustive`.
41+
42+
impl<T> Exhaustive for Option<T>
43+
where
44+
T: Exhaustive + 'static,
45+
{
46+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
47+
Box::new(std::iter::once(None).chain(T::exhaustive().map(Some)))
48+
}
49+
}
50+
51+
impl<T1, T2> Exhaustive for (T1, T2)
52+
where
53+
T1: Exhaustive + Clone + 'static,
54+
T2: Exhaustive + 'static,
55+
{
56+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
57+
Box::new(T1::exhaustive().flat_map(|t1| T2::exhaustive().map(move |t2| (t1.clone(), t2))))
58+
}
59+
}
60+
61+
impl<T> Exhaustive for [T; 1]
62+
where
63+
T: Exhaustive + 'static,
64+
{
65+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
66+
Box::new(T::exhaustive().map(|t| [t]))
67+
}
68+
}
69+
70+
impl<T> Exhaustive for [T; 2]
71+
where
72+
T: Exhaustive + Clone + 'static,
73+
{
74+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
75+
Box::new(T::exhaustive().flat_map(|t1| T::exhaustive().map(move |t2| [t1.clone(), t2])))
76+
}
77+
}
78+
79+
impl<T> Exhaustive for [T; 3]
80+
where
81+
T: Exhaustive + Clone + 'static,
82+
{
83+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
84+
Box::new(
85+
<[T; 2]>::exhaustive()
86+
.flat_map(|[t1, t2]| T::exhaustive().map(move |t3| [t1.clone(), t2.clone(), t3])),
87+
)
88+
}
89+
}
90+
91+
impl<T> Exhaustive for [T; 4]
92+
where
93+
T: Exhaustive + Clone + 'static,
94+
{
95+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
96+
Box::new(<[T; 2]>::exhaustive().flat_map(|[t1, t2]| {
97+
<[T; 2]>::exhaustive().map(move |[t3, t4]| [t1.clone(), t2.clone(), t3, t4])
98+
}))
99+
}
100+
}
101+
102+
impl<T> Exhaustive for [T; 5]
103+
where
104+
T: Exhaustive + Clone + 'static,
105+
{
106+
fn exhaustive() -> Box<dyn Iterator<Item = Self>> {
107+
Box::new(<[T; 2]>::exhaustive().flat_map(|[t1, t2]| {
108+
<[T; 3]>::exhaustive().map(move |[t3, t4, t5]| [t1.clone(), t2.clone(), t3, t4, t5])
109+
}))
110+
}
111+
}

src/borrow_tracker/tree_borrows/mod.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ pub mod diagnostics;
1515
mod perms;
1616
mod tree;
1717
mod unimap;
18+
19+
#[cfg(test)]
20+
mod exhaustive;
21+
1822
use perms::Permission;
1923
pub use tree::Tree;
2024

@@ -271,6 +275,10 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
271275
diagnostics::AccessCause::Reborrow,
272276
)?;
273277
// Record the parent-child pair in the tree.
278+
// FIXME: We should eventually ensure that the following `assert` holds, because
279+
// some "exhaustive" tests consider only the initial configurations that satisfy it.
280+
// The culprit is `Permission::new_active` in `tb_protect_place`.
281+
//assert!(new_perm.initial_state.is_initial());
274282
tree_borrows.new_child(orig_tag, new_tag, new_perm.initial_state, range, span)?;
275283
drop(tree_borrows);
276284

@@ -283,7 +291,7 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
283291
// interleaving, but wether UB happens can depend on whether a write occurs in the
284292
// future...
285293
let is_write = new_perm.initial_state.is_active()
286-
|| (new_perm.initial_state.is_reserved() && new_perm.protector.is_some());
294+
|| (new_perm.initial_state.is_reserved(None) && new_perm.protector.is_some());
287295
if is_write {
288296
// Need to get mutable access to alloc_extra.
289297
// (Cannot always do this as we can do read-only reborrowing on read-only allocations.)

0 commit comments

Comments
 (0)