Skip to content

Commit b11f82b

Browse files
committed
Auto merge of #3742 - Vanille-N:master, r=RalfJung
TB: Reserved + Protected + IM + lazy is a horrible combination that should not exist As discovered by `@JoJoDeveloping,` the result of having both Protector exceptions on lazy locations (protectors only protect initialized bytes) and interior mutability exceptions for protected tags (Reserved IM does not accept foreign writes when protected) leads to some very undesirable results, namely that we cannot do spurious writes even on protected activated locations. We propose that Protected Reserved IM should no longer exist and instead when a type is retagged as part of a `FnEntry` it is assumed to lose interior mutability. In fact, this was already being done implicitly because relevant transitions were guarded by an `if protected`, but the difference is that now it also applies to transitions that occur after the end of the protector.
2 parents 6079614 + c0c1028 commit b11f82b

File tree

7 files changed

+211
-6
lines changed

7 files changed

+211
-6
lines changed

src/borrow_tracker/tree_borrows/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,15 @@ impl<'tcx> NewPermission {
141141
) -> Option<Self> {
142142
let ty_is_freeze = pointee.is_freeze(*cx.tcx, cx.param_env());
143143
let ty_is_unpin = pointee.is_unpin(*cx.tcx, cx.param_env());
144+
let is_protected = kind == RetagKind::FnEntry;
145+
// As demonstrated by `tests/fail/tree_borrows/reservedim_spurious_write.rs`,
146+
// interior mutability and protectors interact poorly.
147+
// To eliminate the case of Protected Reserved IM we override interior mutability
148+
// in the case of a protected reference: protected references are always considered
149+
// "freeze".
144150
let initial_state = match mutability {
145-
Mutability::Mut if ty_is_unpin => Permission::new_reserved(ty_is_freeze),
151+
Mutability::Mut if ty_is_unpin =>
152+
Permission::new_reserved(ty_is_freeze || is_protected),
146153
Mutability::Not if ty_is_freeze => Permission::new_frozen(),
147154
// Raw pointers never enter this function so they are not handled.
148155
// However raw pointers are not the only pointers that take the parent
@@ -151,7 +158,7 @@ impl<'tcx> NewPermission {
151158
_ => return None,
152159
};
153160

154-
let protector = (kind == RetagKind::FnEntry).then_some(ProtectorKind::StrongProtector);
161+
let protector = is_protected.then_some(ProtectorKind::StrongProtector);
155162
Some(Self { zero_size: false, initial_state, protector })
156163
}
157164

src/borrow_tracker/tree_borrows/perms.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ enum PermissionPriv {
2222
/// - foreign-read then child-write is UB due to `conflicted`,
2323
/// - child-write then foreign-read is UB since child-write will activate and then
2424
/// foreign-read disables a protected `Active`, which is UB.
25+
///
26+
/// Note: since the discovery of `tests/fail/tree_borrows/reservedim_spurious_write.rs`,
27+
/// `ty_is_freeze` does not strictly mean that the type has no interior mutability,
28+
/// it could be an interior mutable type that lost its interior mutability privileges
29+
/// when retagged with a protector.
2530
Reserved { ty_is_freeze: bool, conflicted: bool },
2631
/// represents: a unique pointer;
2732
/// allows: child reads, child writes;
@@ -141,6 +146,12 @@ mod transition {
141146
/// non-protected interior mutable `Reserved` which stay the same.
142147
fn foreign_write(state: PermissionPriv, protected: bool) -> Option<PermissionPriv> {
143148
Some(match state {
149+
// FIXME: since the fix related to reservedim_spurious_write, it is now possible
150+
// to express these transitions of the state machine without an explicit dependency
151+
// on `protected`: because `ty_is_freeze: false` implies `!protected` then
152+
// the line handling `Reserved { .. } if protected` could be deleted.
153+
// This will however require optimizations to the exhaustive tests because
154+
// fewer initial conditions are valid.
144155
Reserved { .. } if protected => Disabled,
145156
res @ Reserved { ty_is_freeze: false, .. } => res,
146157
_ => Disabled,

tests/fail/tree_borrows/reserved/cell-protected-write.stderr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Warning: this tree is indicative only. Some tags may have been hidden.
55
| RsM | └─┬──<TAG=base>
66
| RsM | ├─┬──<TAG=x>
77
| RsM | │ └─┬──<TAG=caller:x>
8-
| RsM | │ └────<TAG=callee:x> Strongly protected
8+
| Rs | │ └────<TAG=callee:x> Strongly protected
99
| RsM | └────<TAG=y, callee:y, caller:y>
1010
──────────────────────────────────────────────────
1111
error: Undefined Behavior: write access through <TAG> (y, callee:y, caller:y) at ALLOC[0x0] is forbidden
@@ -16,14 +16,14 @@ LL | *y = 1;
1616
|
1717
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
1818
= help: the accessed tag <TAG> (y, callee:y, caller:y) is foreign to the protected tag <TAG> (callee:x) (i.e., it is not a child)
19-
= help: this foreign write access would cause the protected tag <TAG> (callee:x) (currently Reserved (interior mutable)) to become Disabled
19+
= help: this foreign write access would cause the protected tag <TAG> (callee:x) (currently Reserved) to become Disabled
2020
= help: protected tags must never be Disabled
2121
help: the accessed tag <TAG> was created here
2222
--> $DIR/cell-protected-write.rs:LL:CC
2323
|
2424
LL | let y = (&mut *n).get();
2525
| ^^^^^^^^^
26-
help: the protected tag <TAG> was created here, in the initial state Reserved (interior mutable)
26+
help: the protected tag <TAG> was created here, in the initial state Reserved
2727
--> $DIR/cell-protected-write.rs:LL:CC
2828
|
2929
LL | unsafe fn write_second(x: &mut UnsafeCell<u8>, y: *mut u8) {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Illustrating a problematic interaction between Reserved, interior mutability,
2+
// and protectors, that makes spurious writes fail in the previous model of Tree Borrows.
3+
// As for all similar tests, we disable preemption so that the error message is deterministic.
4+
//@compile-flags: -Zmiri-tree-borrows -Zmiri-preemption-rate=0
5+
//
6+
// One revision without spurious read (default source code) and one with spurious read.
7+
// Both are expected to be UB. Both revisions are expected to have the *same* error
8+
// because we are aligning the behavior of `without` to that of `with` so that the
9+
// spurious write is effectively a noop in the long term.
10+
//@revisions: without with
11+
12+
use std::cell::Cell;
13+
use std::sync::{Arc, Barrier};
14+
use std::thread;
15+
16+
// Here is the problematic interleaving:
17+
// - thread 1: retag and activate `x` (protected)
18+
// - thread 2: retag but do not initialize (lazy) `y` as Reserved with interior mutability
19+
// - thread 1: spurious write through `x` would go here
20+
// - thread 2: function exit (noop due to lazyness)
21+
// - thread 1: function exit (no permanent effect on `y` because it is now Reserved IM unprotected)
22+
// - thread 2: write through `y`
23+
// In the source code nothing happens to `y`
24+
25+
// `Send`able raw pointer wrapper.
26+
#[derive(Copy, Clone)]
27+
struct SendPtr(*mut u8);
28+
unsafe impl Send for SendPtr {}
29+
30+
type IdxBarrier = (usize, Arc<Barrier>);
31+
32+
// Barriers to enforce the interleaving.
33+
// This macro expects `synchronized!(thread, msg)` where `thread` is a `IdxBarrier`,
34+
// and `msg` is the message to be displayed when the thread reaches this point in the execution.
35+
macro_rules! synchronized {
36+
($thread:expr, $msg:expr) => {{
37+
let (thread_id, barrier) = &$thread;
38+
eprintln!("Thread {} executing: {}", thread_id, $msg);
39+
barrier.wait();
40+
}};
41+
}
42+
43+
fn main() {
44+
// The conflict occurs on one single location but the example involves
45+
// lazily initialized permissions. We will use `&mut Cell<()>` references
46+
// to `data` to achieve this.
47+
let mut data = 0u8;
48+
let ptr = SendPtr(std::ptr::addr_of_mut!(data));
49+
let barrier = Arc::new(Barrier::new(2));
50+
let bx = Arc::clone(&barrier);
51+
let by = Arc::clone(&barrier);
52+
53+
// Retag and activate `x`, wait until the other thread creates a lazy permission.
54+
// Then do a spurious write. Finally exit the function after the other thread.
55+
let thread_1 = thread::spawn(move || {
56+
let b = (1, bx);
57+
synchronized!(b, "start");
58+
let ptr = ptr;
59+
synchronized!(b, "retag x (&mut, protect)");
60+
fn inner(x: &mut u8, b: IdxBarrier) {
61+
*x = 42; // activate immediately
62+
synchronized!(b, "[lazy] retag y (&mut, protect, IM)");
63+
// A spurious write should be valid here because `x` is
64+
// `Active` and protected.
65+
if cfg!(with) {
66+
synchronized!(b, "spurious write x (executed)");
67+
*x = 64;
68+
} else {
69+
synchronized!(b, "spurious write x (skipped)");
70+
}
71+
synchronized!(b, "ret y");
72+
synchronized!(b, "ret x");
73+
}
74+
inner(unsafe { &mut *ptr.0 }, b.clone());
75+
synchronized!(b, "write y");
76+
synchronized!(b, "end");
77+
});
78+
79+
// Create a lazy Reserved with interior mutability.
80+
// Wait for the other thread's spurious write then observe the side effects
81+
// of that write.
82+
let thread_2 = thread::spawn(move || {
83+
let b = (2, by);
84+
synchronized!(b, "start");
85+
let ptr = ptr;
86+
synchronized!(b, "retag x (&mut, protect)");
87+
synchronized!(b, "[lazy] retag y (&mut, protect, IM)");
88+
fn inner(y: &mut Cell<()>, b: IdxBarrier) -> *mut u8 {
89+
synchronized!(b, "spurious write x");
90+
synchronized!(b, "ret y");
91+
// `y` is not retagged for any bytes, so the pointer we return
92+
// has its permission lazily initialized.
93+
y as *mut Cell<()> as *mut u8
94+
}
95+
// Currently `ptr` points to `data`.
96+
// We do a zero-sized retag so that its permission is lazy.
97+
let y_zst = unsafe { &mut *(ptr.0 as *mut Cell<()>) };
98+
let y = inner(y_zst, b.clone());
99+
synchronized!(b, "ret x");
100+
synchronized!(b, "write y");
101+
unsafe { *y = 13 } //~ERROR: /write access through .* is forbidden/
102+
synchronized!(b, "end");
103+
});
104+
105+
thread_1.join().unwrap();
106+
thread_2.join().unwrap();
107+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Thread 1 executing: start
2+
Thread 2 executing: start
3+
Thread 2 executing: retag x (&mut, protect)
4+
Thread 1 executing: retag x (&mut, protect)
5+
Thread 1 executing: [lazy] retag y (&mut, protect, IM)
6+
Thread 2 executing: [lazy] retag y (&mut, protect, IM)
7+
Thread 2 executing: spurious write x
8+
Thread 1 executing: spurious write x (executed)
9+
Thread 1 executing: ret y
10+
Thread 2 executing: ret y
11+
Thread 2 executing: ret x
12+
Thread 1 executing: ret x
13+
Thread 1 executing: write y
14+
Thread 2 executing: write y
15+
error: Undefined Behavior: write access through <TAG> at ALLOC[0x0] is forbidden
16+
--> $DIR/reservedim_spurious_write.rs:LL:CC
17+
|
18+
LL | unsafe { *y = 13 }
19+
| ^^^^^^^ write access through <TAG> at ALLOC[0x0] is forbidden
20+
|
21+
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
22+
= help: the accessed tag <TAG> has state Disabled which forbids this child write access
23+
help: the accessed tag <TAG> was created here, in the initial state Reserved
24+
--> $DIR/reservedim_spurious_write.rs:LL:CC
25+
|
26+
LL | fn inner(y: &mut Cell<()>, b: IdxBarrier) -> *mut u8 {
27+
| ^
28+
help: the accessed tag <TAG> later transitioned to Disabled due to a foreign write access at offsets [0x0..0x1]
29+
--> $DIR/reservedim_spurious_write.rs:LL:CC
30+
|
31+
LL | *x = 64;
32+
| ^^^^^^^
33+
= help: this transition corresponds to a loss of read and write permissions
34+
= note: BACKTRACE (of the first span) on thread `unnamed-ID`:
35+
= note: inside closure at $DIR/reservedim_spurious_write.rs:LL:CC
36+
37+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
38+
39+
error: aborting due to 1 previous error
40+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Thread 1 executing: start
2+
Thread 2 executing: start
3+
Thread 2 executing: retag x (&mut, protect)
4+
Thread 1 executing: retag x (&mut, protect)
5+
Thread 1 executing: [lazy] retag y (&mut, protect, IM)
6+
Thread 2 executing: [lazy] retag y (&mut, protect, IM)
7+
Thread 2 executing: spurious write x
8+
Thread 1 executing: spurious write x (skipped)
9+
Thread 1 executing: ret y
10+
Thread 2 executing: ret y
11+
Thread 2 executing: ret x
12+
Thread 1 executing: ret x
13+
Thread 1 executing: write y
14+
Thread 2 executing: write y
15+
error: Undefined Behavior: write access through <TAG> at ALLOC[0x0] is forbidden
16+
--> $DIR/reservedim_spurious_write.rs:LL:CC
17+
|
18+
LL | unsafe { *y = 13 }
19+
| ^^^^^^^ write access through <TAG> at ALLOC[0x0] is forbidden
20+
|
21+
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
22+
= help: the accessed tag <TAG> has state Disabled which forbids this child write access
23+
help: the accessed tag <TAG> was created here, in the initial state Reserved
24+
--> $DIR/reservedim_spurious_write.rs:LL:CC
25+
|
26+
LL | fn inner(y: &mut Cell<()>, b: IdxBarrier) -> *mut u8 {
27+
| ^
28+
help: the accessed tag <TAG> later transitioned to Disabled due to a protector release (acting as a foreign write access) on every location previously accessed by this tag
29+
--> $DIR/reservedim_spurious_write.rs:LL:CC
30+
|
31+
LL | }
32+
| ^
33+
= help: this transition corresponds to a loss of read and write permissions
34+
= note: BACKTRACE (of the first span) on thread `unnamed-ID`:
35+
= note: inside closure at $DIR/reservedim_spurious_write.rs:LL:CC
36+
37+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
38+
39+
error: aborting due to 1 previous error
40+

tests/pass/tree_borrows/reserved.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Warning: this tree is indicative only. Some tags may have been hidden.
66
| RsM | └─┬──<TAG=base>
77
| RsM | ├─┬──<TAG=x>
88
| RsM | │ └─┬──<TAG=caller:x>
9-
| RsCM| │ └────<TAG=callee:x>
9+
| RsC | │ └────<TAG=callee:x>
1010
| RsM | └────<TAG=y, caller:y, callee:y>
1111
──────────────────────────────────────────────────
1212
[interior mut] Foreign Read: Re* -> Re*

0 commit comments

Comments
 (0)