Skip to content

Commit 5974e7d

Browse files
committed
Auto merge of #2287 - RalfJung:field-retagging, r=RalfJung
stacked borrows: add option for recursive field retagging
2 parents 1b3332a + 955f961 commit 5974e7d

File tree

11 files changed

+219
-84
lines changed

11 files changed

+219
-84
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,9 @@ to Miri failing to detect cases of undefined behavior in a program.
369369
application instead of raising an error within the context of Miri (and halting
370370
execution). Note that code might not expect these operations to ever panic, so
371371
this flag can lead to strange (mis)behavior.
372+
* `-Zmiri-retag-fields` changes Stacked Borrows retagging to recurse into fields.
373+
This means that references in fields of structs/enums/tuples/arrays/... are retagged,
374+
and in particular, they are protected when passed as function arguments.
372375
* `-Zmiri-track-alloc-id=<id1>,<id2>,...` shows a backtrace when the given allocations are
373376
being allocated or freed. This helps in debugging memory leaks and
374377
use after free bugs. Specifying this argument multiple times does not overwrite the previous

rust-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
493c960a3e6cdd2e2fbe8b6ea130fadea05f1ab0
1+
ddcbba036aee08f0709f98a92a342a278eae5c05

src/bin/miri.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,8 @@ fn main() {
383383
miri_config.provenance_mode = ProvenanceMode::Permissive;
384384
} else if arg == "-Zmiri-mute-stdout-stderr" {
385385
miri_config.mute_stdout_stderr = true;
386+
} else if arg == "-Zmiri-retag-fields" {
387+
miri_config.retag_fields = true;
386388
} else if arg == "-Zmiri-track-raw-pointers" {
387389
eprintln!(
388390
"WARNING: `-Zmiri-track-raw-pointers` has no effect; it is enabled by default"

src/eval.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ pub struct MiriConfig {
124124
pub preemption_rate: f64,
125125
/// Report the current instruction being executed every N basic blocks.
126126
pub report_progress: Option<u32>,
127+
/// Whether Stacked Borrows retagging should recurse into fields of datatypes.
128+
pub retag_fields: bool,
127129
}
128130

129131
impl Default for MiriConfig {
@@ -154,6 +156,7 @@ impl Default for MiriConfig {
154156
mute_stdout_stderr: false,
155157
preemption_rate: 0.01, // 1%
156158
report_progress: None,
159+
retag_fields: false,
157160
}
158161
}
159162
}

src/machine.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
354354
Some(RefCell::new(stacked_borrows::GlobalStateInner::new(
355355
config.tracked_pointer_tags.clone(),
356356
config.tracked_call_ids.clone(),
357+
config.retag_fields,
357358
)))
358359
} else {
359360
None

src/stacked_borrows.rs

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ pub struct GlobalStateInner {
156156
tracked_pointer_tags: HashSet<SbTag>,
157157
/// The call ids to trace
158158
tracked_call_ids: HashSet<CallId>,
159+
/// Whether to recurse into datatypes when searching for pointers to retag.
160+
retag_fields: bool,
159161
}
160162

161163
/// We need interior mutable access to the global state.
@@ -204,14 +206,19 @@ impl fmt::Display for RefKind {
204206

205207
/// Utilities for initialization and ID generation
206208
impl GlobalStateInner {
207-
pub fn new(tracked_pointer_tags: HashSet<SbTag>, tracked_call_ids: HashSet<CallId>) -> Self {
209+
pub fn new(
210+
tracked_pointer_tags: HashSet<SbTag>,
211+
tracked_call_ids: HashSet<CallId>,
212+
retag_fields: bool,
213+
) -> Self {
208214
GlobalStateInner {
209215
next_ptr_tag: SbTag(NonZeroU64::new(1).unwrap()),
210216
base_ptr_tags: FxHashMap::default(),
211217
next_call_id: NonZeroU64::new(1).unwrap(),
212218
active_calls: FxHashSet::default(),
213219
tracked_pointer_tags,
214220
tracked_call_ids,
221+
retag_fields,
215222
}
216223
}
217224

@@ -1035,17 +1042,69 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
10351042
}
10361043
}
10371044

1038-
// We only reborrow "bare" references/boxes.
1039-
// Not traversing into fields helps with <https://github.com/rust-lang/unsafe-code-guidelines/issues/125>,
1040-
// but might also cost us optimization and analyses. We will have to experiment more with this.
1045+
// We need a visitor to visit all references. However, that requires
1046+
// a `MPlaceTy` (or `OpTy), so we have a fast path for reference types that
1047+
// avoids allocating.
1048+
10411049
if let Some((mutbl, protector)) = qualify(place.layout.ty, kind) {
10421050
// Fast path.
10431051
let val = this.read_immediate(&this.place_to_op(place)?)?;
10441052
let val = this.retag_reference(&val, mutbl, protector)?;
10451053
this.write_immediate(*val, place)?;
1054+
return Ok(());
10461055
}
10471056

1048-
Ok(())
1057+
// If we don't want to recurse, we are already done.
1058+
if !this.machine.stacked_borrows.as_mut().unwrap().get_mut().retag_fields {
1059+
return Ok(());
1060+
}
1061+
1062+
// Skip some types that have no further structure we might care about.
1063+
if matches!(
1064+
place.layout.ty.kind(),
1065+
ty::RawPtr(..)
1066+
| ty::Ref(..)
1067+
| ty::Int(..)
1068+
| ty::Uint(..)
1069+
| ty::Float(..)
1070+
| ty::Bool
1071+
| ty::Char
1072+
) {
1073+
return Ok(());
1074+
}
1075+
// Now go visit this thing.
1076+
let place = this.force_allocation(place)?;
1077+
1078+
let mut visitor = RetagVisitor { ecx: this, kind };
1079+
return visitor.visit_value(&place);
1080+
1081+
// The actual visitor.
1082+
struct RetagVisitor<'ecx, 'mir, 'tcx> {
1083+
ecx: &'ecx mut MiriEvalContext<'mir, 'tcx>,
1084+
kind: RetagKind,
1085+
}
1086+
impl<'ecx, 'mir, 'tcx> MutValueVisitor<'mir, 'tcx, Evaluator<'mir, 'tcx>>
1087+
for RetagVisitor<'ecx, 'mir, 'tcx>
1088+
{
1089+
type V = MPlaceTy<'tcx, Tag>;
1090+
1091+
#[inline(always)]
1092+
fn ecx(&mut self) -> &mut MiriEvalContext<'mir, 'tcx> {
1093+
&mut self.ecx
1094+
}
1095+
1096+
fn visit_value(&mut self, place: &MPlaceTy<'tcx, Tag>) -> InterpResult<'tcx> {
1097+
if let Some((mutbl, protector)) = qualify(place.layout.ty, self.kind) {
1098+
let val = self.ecx.read_immediate(&place.into())?;
1099+
let val = self.ecx.retag_reference(&val, mutbl, protector)?;
1100+
self.ecx.write_immediate(*val, &(*place).into())?;
1101+
} else {
1102+
// Maybe we need to go deeper.
1103+
self.walk_value(place)?;
1104+
}
1105+
Ok(())
1106+
}
1107+
}
10491108
}
10501109

10511110
/// After a stack frame got pushed, retag the return place so that we are sure
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// compile-flags: -Zmiri-retag-fields
2+
// error-pattern: incompatible item is protected
3+
struct Newtype<'a>(&'a mut i32);
4+
5+
fn dealloc_while_running(_n: Newtype<'_>, dealloc: impl FnOnce()) {
6+
dealloc();
7+
}
8+
9+
// Make sure that we protect references inside structs.
10+
fn main() {
11+
let ptr = Box::into_raw(Box::new(0i32));
12+
#[rustfmt::skip] // I like my newlines
13+
unsafe {
14+
dealloc_while_running(
15+
Newtype(&mut *ptr),
16+
|| drop(Box::from_raw(ptr)),
17+
)
18+
};
19+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
error: Undefined Behavior: not granting access to tag <TAG> because incompatible item is protected: [Unique for <TAG> (call ID)]
2+
--> RUSTLIB/alloc/src/boxed.rs:LL:CC
3+
|
4+
LL | Box(unsafe { Unique::new_unchecked(raw) }, alloc)
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not granting access to tag <TAG> because incompatible item is protected: [Unique for <TAG> (call ID)]
6+
|
7+
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
8+
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
9+
help: <TAG> was created by a retag at offsets [0x0..0x4]
10+
--> $DIR/newtype_retagging.rs:LL:CC
11+
|
12+
LL | let ptr = Box::into_raw(Box::new(0i32));
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
help: <TAG> was protected due to <TAG> which was created here
15+
--> $DIR/newtype_retagging.rs:LL:CC
16+
|
17+
LL | Newtype(&mut *ptr),
18+
| ^^^^^^^^^^^^^^^^^^
19+
help: this protector is live for this call
20+
--> $DIR/newtype_retagging.rs:LL:CC
21+
|
22+
LL | / fn dealloc_while_running(_n: Newtype<'_>, dealloc: impl FnOnce()) {
23+
LL | | dealloc();
24+
LL | | }
25+
| |_^
26+
= note: inside `std::boxed::Box::<i32>::from_raw_in` at RUSTLIB/alloc/src/boxed.rs:LL:CC
27+
= note: inside `std::boxed::Box::<i32>::from_raw` at RUSTLIB/alloc/src/boxed.rs:LL:CC
28+
note: inside closure at $DIR/newtype_retagging.rs:LL:CC
29+
--> $DIR/newtype_retagging.rs:LL:CC
30+
|
31+
LL | || drop(Box::from_raw(ptr)),
32+
| ^^^^^^^^^^^^^^^^^^
33+
note: inside `dealloc_while_running::<[closure@$DIR/newtype_retagging.rs:LL:CC]>` at $DIR/newtype_retagging.rs:LL:CC
34+
--> $DIR/newtype_retagging.rs:LL:CC
35+
|
36+
LL | dealloc();
37+
| ^^^^^^^^^
38+
note: inside `main` at $DIR/newtype_retagging.rs:LL:CC
39+
--> $DIR/newtype_retagging.rs:LL:CC
40+
|
41+
LL | / dealloc_while_running(
42+
LL | | Newtype(&mut *ptr),
43+
LL | | || drop(Box::from_raw(ptr)),
44+
LL | | )
45+
| |_________^
46+
47+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
48+
49+
error: aborting due to previous error
50+

tests/pass/stacked-borrows/interior_mutability.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use std::cell::{Cell, RefCell, UnsafeCell};
1+
// compile-flags: -Zmiri-retag-fields
2+
use std::cell::{Cell, Ref, RefCell, RefMut, UnsafeCell};
23
use std::mem::{self, MaybeUninit};
34

45
fn main() {
@@ -8,6 +9,10 @@ fn main() {
89
unsafe_cell_2phase();
910
unsafe_cell_deallocate();
1011
unsafe_cell_invalidate();
12+
refcell_basic();
13+
ref_protector();
14+
ref_mut_protector();
15+
rust_issue_68303();
1116
}
1217

1318
fn aliasing_mut_and_shr() {
@@ -99,3 +104,72 @@ fn unsafe_cell_invalidate() {
99104
// So using raw1 invalidates raw2.
100105
f(unsafe { mem::transmute(raw2) }, raw1);
101106
}
107+
108+
fn refcell_basic() {
109+
let c = RefCell::new(42);
110+
{
111+
let s1 = c.borrow();
112+
let _x: i32 = *s1;
113+
let s2 = c.borrow();
114+
let _x: i32 = *s1;
115+
let _y: i32 = *s2;
116+
let _x: i32 = *s1;
117+
let _y: i32 = *s2;
118+
}
119+
{
120+
let mut m = c.borrow_mut();
121+
let _z: i32 = *m;
122+
{
123+
let s: &i32 = &*m;
124+
let _x = *s;
125+
}
126+
*m = 23;
127+
let _z: i32 = *m;
128+
}
129+
{
130+
let s1 = c.borrow();
131+
let _x: i32 = *s1;
132+
let s2 = c.borrow();
133+
let _x: i32 = *s1;
134+
let _y: i32 = *s2;
135+
let _x: i32 = *s1;
136+
let _y: i32 = *s2;
137+
}
138+
}
139+
140+
// Adding a Stacked Borrows protector for `Ref` would break this
141+
fn ref_protector() {
142+
fn break_it(rc: &RefCell<i32>, r: Ref<'_, i32>) {
143+
// `r` has a shared reference, it is passed in as argument and hence
144+
// a protector is added that marks this memory as read-only for the entire
145+
// duration of this function.
146+
drop(r);
147+
// *oops* here we can mutate that memory.
148+
*rc.borrow_mut() = 2;
149+
}
150+
151+
let rc = RefCell::new(0);
152+
break_it(&rc, rc.borrow())
153+
}
154+
155+
fn ref_mut_protector() {
156+
fn break_it(rc: &RefCell<i32>, r: RefMut<'_, i32>) {
157+
// `r` has a shared reference, it is passed in as argument and hence
158+
// a protector is added that marks this memory as inaccessible for the entire
159+
// duration of this function
160+
drop(r);
161+
// *oops* here we can mutate that memory.
162+
*rc.borrow_mut() = 2;
163+
}
164+
165+
let rc = RefCell::new(0);
166+
break_it(&rc, rc.borrow_mut())
167+
}
168+
169+
/// Make sure we do not have bad enum layout optimizations.
170+
fn rust_issue_68303() {
171+
let optional = Some(RefCell::new(false));
172+
let mut handle = optional.as_ref().unwrap().borrow_mut();
173+
assert!(optional.is_some());
174+
*handle = true;
175+
}

tests/pass/stacked-borrows/refcell.rs

Lines changed: 0 additions & 77 deletions
This file was deleted.

0 commit comments

Comments
 (0)