Skip to content

Commit 12300f5

Browse files
authored
Merge pull request #4314 from yoctocell/fine-grained-tracking
TB: Track permissions on the byte-level
2 parents 261e444 + 2107793 commit 12300f5

File tree

11 files changed

+391
-175
lines changed

11 files changed

+391
-175
lines changed

src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ impl DisplayFmt {
504504
if let Some(perm) = perm {
505505
format!(
506506
"{ac}{st}",
507-
ac = if perm.is_initialized() { self.accessed.yes } else { self.accessed.no },
507+
ac = if perm.is_accessed() { self.accessed.yes } else { self.accessed.no },
508508
st = perm.permission().short_name(),
509509
)
510510
} else {

src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs

Lines changed: 162 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use rustc_middle::mir::{Mutability, RetagKind};
33
use rustc_middle::ty::layout::HasTypingEnv;
44
use rustc_middle::ty::{self, Ty};
55

6+
use self::foreign_access_skipping::IdempotentForeignAccess;
7+
use self::tree::LocationState;
68
use crate::borrow_tracker::{GlobalState, GlobalStateInner, ProtectorKind};
79
use crate::concurrency::data_race::NaReadType;
810
use crate::*;
@@ -95,7 +97,7 @@ impl<'tcx> Tree {
9597
/// A tag just lost its protector.
9698
///
9799
/// This emits a special kind of access that is only applied
98-
/// to initialized locations, as a protection against other
100+
/// to accessed locations, as a protection against other
99101
/// tags not having been made aware of the existence of this
100102
/// protector.
101103
pub fn release_protector(
@@ -113,16 +115,19 @@ impl<'tcx> Tree {
113115

114116
/// Policy for a new borrow.
115117
#[derive(Debug, Clone, Copy)]
116-
struct NewPermission {
117-
/// Which permission should the pointer start with.
118-
initial_state: Permission,
118+
pub struct NewPermission {
119+
/// Permission for the frozen part of the range.
120+
freeze_perm: Permission,
121+
/// Whether a read access should be performed on the frozen part on a retag.
122+
freeze_access: bool,
123+
/// Permission for the non-frozen part of the range.
124+
nonfreeze_perm: Permission,
125+
/// Whether a read access should be performed on the non-frozen
126+
/// part on a retag.
127+
nonfreeze_access: bool,
119128
/// Whether this pointer is part of the arguments of a function call.
120129
/// `protector` is `Some(_)` for all pointers marked `noalias`.
121130
protector: Option<ProtectorKind>,
122-
/// Whether a read should be performed on a retag. This should be `false`
123-
/// for `Cell` because this could cause data races when using thread-safe
124-
/// data types like `Mutex<T>`.
125-
initial_read: bool,
126131
}
127132

128133
impl<'tcx> NewPermission {
@@ -133,27 +138,42 @@ impl<'tcx> NewPermission {
133138
kind: RetagKind,
134139
cx: &crate::MiriInterpCx<'tcx>,
135140
) -> Option<Self> {
136-
let ty_is_freeze = pointee.is_freeze(*cx.tcx, cx.typing_env());
137141
let ty_is_unpin = pointee.is_unpin(*cx.tcx, cx.typing_env());
138142
let is_protected = kind == RetagKind::FnEntry;
139-
// As demonstrated by `tests/fail/tree_borrows/reservedim_spurious_write.rs`,
140-
// interior mutability and protectors interact poorly.
141-
// To eliminate the case of Protected Reserved IM we override interior mutability
142-
// in the case of a protected reference: protected references are always considered
143-
// "freeze" in their reservation phase.
144-
let (initial_state, initial_read) = match mutability {
143+
let protector = is_protected.then_some(ProtectorKind::StrongProtector);
144+
145+
Some(match mutability {
145146
Mutability::Mut if ty_is_unpin =>
146-
(Permission::new_reserved(ty_is_freeze, is_protected), true),
147-
Mutability::Not if ty_is_freeze => (Permission::new_frozen(), true),
148-
Mutability::Not if !ty_is_freeze => (Permission::new_cell(), false),
149-
// Raw pointers never enter this function so they are not handled.
150-
// However raw pointers are not the only pointers that take the parent
151-
// tag, this also happens for `!Unpin` `&mut`s, which are excluded above.
147+
NewPermission {
148+
freeze_perm: Permission::new_reserved(
149+
/* ty_is_freeze */ true,
150+
is_protected,
151+
),
152+
freeze_access: true,
153+
nonfreeze_perm: Permission::new_reserved(
154+
/* ty_is_freeze */ false,
155+
is_protected,
156+
),
157+
// If we have a mutable reference, then the non-frozen part will
158+
// have state `ReservedIM` or `Reserved`, which can have an initial read access
159+
// performed on it because you cannot have multiple mutable borrows.
160+
nonfreeze_access: true,
161+
protector,
162+
},
163+
Mutability::Not =>
164+
NewPermission {
165+
freeze_perm: Permission::new_frozen(),
166+
freeze_access: true,
167+
nonfreeze_perm: Permission::new_cell(),
168+
// If it is a shared reference, then the non-frozen
169+
// part will have state `Cell`, which should not have an initial access,
170+
// as this can cause data races when using thread-safe data types like
171+
// `Mutex<T>`.
172+
nonfreeze_access: false,
173+
protector,
174+
},
152175
_ => return None,
153-
};
154-
155-
let protector = is_protected.then_some(ProtectorKind::StrongProtector);
156-
Some(Self { initial_state, protector, initial_read })
176+
})
157177
}
158178

159179
/// Compute permission for `Box`-like type (`Box` always, and also `Unique` if enabled).
@@ -168,13 +188,17 @@ impl<'tcx> NewPermission {
168188
pointee.is_unpin(*cx.tcx, cx.typing_env()).then_some(()).map(|()| {
169189
// Regular `Unpin` box, give it `noalias` but only a weak protector
170190
// because it is valid to deallocate it within the function.
171-
let ty_is_freeze = pointee.is_freeze(*cx.tcx, cx.typing_env());
172-
let protected = kind == RetagKind::FnEntry;
173-
let initial_state = Permission::new_reserved(ty_is_freeze, protected);
174-
Self {
175-
initial_state,
176-
protector: protected.then_some(ProtectorKind::WeakProtector),
177-
initial_read: true,
191+
let is_protected = kind == RetagKind::FnEntry;
192+
let protector = is_protected.then_some(ProtectorKind::WeakProtector);
193+
NewPermission {
194+
freeze_perm: Permission::new_reserved(/* ty_is_freeze */ true, is_protected),
195+
freeze_access: true,
196+
nonfreeze_perm: Permission::new_reserved(
197+
/* ty_is_freeze */ false,
198+
is_protected,
199+
),
200+
nonfreeze_access: true,
201+
protector,
178202
}
179203
})
180204
}
@@ -194,8 +218,6 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
194218
new_tag: BorTag,
195219
) -> InterpResult<'tcx, Option<Provenance>> {
196220
let this = self.eval_context_mut();
197-
// Make sure the new permission makes sense as the initial permission of a fresh tag.
198-
assert!(new_perm.initial_state.is_initial());
199221
// Ensure we bail out if the pointer goes out-of-bounds (see miri#1050).
200222
this.check_ptr_access(place.ptr(), ptr_size, CheckInAllocMsg::Dereferenceable)?;
201223

@@ -206,7 +228,13 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
206228
let global = this.machine.borrow_tracker.as_ref().unwrap().borrow();
207229
let ty = place.layout.ty;
208230
if global.tracked_pointer_tags.contains(&new_tag) {
209-
let kind_str = format!("initial state {} (pointee type {ty})", new_perm.initial_state);
231+
let ty_is_freeze = ty.is_freeze(*this.tcx, this.typing_env());
232+
let kind_str =
233+
if ty_is_freeze {
234+
format!("initial state {} (pointee type {ty})", new_perm.freeze_perm)
235+
} else {
236+
format!("initial state {}/{} outside/inside UnsafeCell (pointee type {ty})", new_perm.freeze_perm, new_perm.nonfreeze_perm)
237+
};
210238
this.emit_diagnostic(NonHaltingDiagnostic::CreatedPointerTag(
211239
new_tag.inner(),
212240
Some(kind_str),
@@ -285,43 +313,103 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
285313

286314
let span = this.machine.current_span();
287315
let alloc_extra = this.get_alloc_extra(alloc_id)?;
288-
let range = alloc_range(base_offset, ptr_size);
289316
let mut tree_borrows = alloc_extra.borrow_tracker_tb().borrow_mut();
290317

291-
// All reborrows incur a (possibly zero-sized) read access to the parent
292-
if new_perm.initial_read {
293-
tree_borrows.perform_access(
294-
orig_tag,
295-
Some((range, AccessKind::Read, diagnostics::AccessCause::Reborrow)),
296-
this.machine.borrow_tracker.as_ref().unwrap(),
297-
alloc_id,
298-
this.machine.current_span(),
299-
)?;
300-
}
318+
// Store initial permissions and their corresponding range.
319+
let mut perms_map: RangeMap<LocationState> = RangeMap::new(
320+
ptr_size,
321+
LocationState::new_accessed(Permission::new_disabled(), IdempotentForeignAccess::None), // this will be overwritten
322+
);
323+
// Keep track of whether the node has any part that allows for interior mutability.
324+
// FIXME: This misses `PhantomData<UnsafeCell<T>>` which could be considered a marker
325+
// for requesting interior mutability.
326+
let mut has_unsafe_cell = false;
327+
328+
// When adding a new node, the SIFA of its parents needs to be updated, potentially across
329+
// the entire memory range. For the parts that are being accessed below, the access itself
330+
// trivially takes care of that. However, we have to do some more work to also deal with
331+
// the parts that are not being accessed. Specifically what we do is that we
332+
// call `update_last_accessed_after_retag` on the SIFA of the permission set for the part of
333+
// memory outside `perm_map` -- so that part is definitely taken care of. The remaining concern
334+
// is the part of memory that is in the range of `perms_map`, but not accessed below.
335+
// There we have two cases:
336+
// * If we do have an `UnsafeCell` (`has_unsafe_cell` becomes true), then the non-accessed part
337+
// uses `nonfreeze_perm`, so the `nonfreeze_perm` initialized parts are also fine. We enforce
338+
// the `freeze_perm` parts to be accessed, and thus everything is taken care of.
339+
// * If there is no `UnsafeCell`, then `freeze_perm` is used everywhere (both inside and outside the initial range),
340+
// and we update everything to have the `freeze_perm`'s SIFA, so there are no issues. (And this assert below is not
341+
// actually needed in this case).
342+
assert!(new_perm.freeze_access);
343+
344+
let protected = new_perm.protector.is_some();
345+
this.visit_freeze_sensitive(place, ptr_size, |range, frozen| {
346+
has_unsafe_cell = has_unsafe_cell || !frozen;
347+
348+
// We are only ever `Frozen` inside the frozen bits.
349+
let (perm, access) = if frozen {
350+
(new_perm.freeze_perm, new_perm.freeze_access)
351+
} else {
352+
(new_perm.nonfreeze_perm, new_perm.nonfreeze_access)
353+
};
354+
355+
// Store initial permissions.
356+
for (_loc_range, loc) in perms_map.iter_mut(range.start, range.size) {
357+
let sifa = perm.strongest_idempotent_foreign_access(protected);
358+
// NOTE: Currently, `access` is false if and only if `perm` is Cell, so this `if`
359+
// doesn't not change whether any code is UB or not. We could just always use
360+
// `new_accessed` and everything would stay the same. But that seems conceptually
361+
// odd, so we keep the initial "accessed" bit of the `LocationState` in sync with whether
362+
// a read access is performed below.
363+
if access {
364+
*loc = LocationState::new_accessed(perm, sifa);
365+
} else {
366+
*loc = LocationState::new_non_accessed(perm, sifa);
367+
}
368+
}
369+
370+
// Some reborrows incur a read access to the parent.
371+
if access {
372+
// Adjust range to be relative to allocation start (rather than to `place`).
373+
let mut range_in_alloc = range;
374+
range_in_alloc.start += base_offset;
375+
376+
tree_borrows.perform_access(
377+
orig_tag,
378+
Some((range_in_alloc, AccessKind::Read, diagnostics::AccessCause::Reborrow)),
379+
this.machine.borrow_tracker.as_ref().unwrap(),
380+
alloc_id,
381+
this.machine.current_span(),
382+
)?;
383+
384+
// Also inform the data race model (but only if any bytes are actually affected).
385+
if range.size.bytes() > 0 {
386+
if let Some(data_race) = alloc_extra.data_race.as_vclocks_ref() {
387+
data_race.read(
388+
alloc_id,
389+
range_in_alloc,
390+
NaReadType::Retag,
391+
Some(place.layout.ty),
392+
&this.machine,
393+
)?
394+
}
395+
}
396+
}
397+
interp_ok(())
398+
})?;
399+
301400
// Record the parent-child pair in the tree.
302401
tree_borrows.new_child(
402+
base_offset,
303403
orig_tag,
304404
new_tag,
305-
new_perm.initial_state,
306-
range,
405+
perms_map,
406+
// Allow lazily writing to surrounding data if we found an `UnsafeCell`.
407+
if has_unsafe_cell { new_perm.nonfreeze_perm } else { new_perm.freeze_perm },
408+
protected,
307409
span,
308-
new_perm.protector.is_some(),
309410
)?;
310411
drop(tree_borrows);
311412

312-
// Also inform the data race model (but only if any bytes are actually affected).
313-
if range.size.bytes() > 0 && new_perm.initial_read {
314-
if let Some(data_race) = alloc_extra.data_race.as_vclocks_ref() {
315-
data_race.read(
316-
alloc_id,
317-
range,
318-
NaReadType::Retag,
319-
Some(place.layout.ty),
320-
&this.machine,
321-
)?;
322-
}
323-
}
324-
325413
interp_ok(Some(Provenance::Concrete { alloc_id, tag: new_tag }))
326414
}
327415

@@ -508,15 +596,21 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
508596
fn tb_protect_place(&mut self, place: &MPlaceTy<'tcx>) -> InterpResult<'tcx, MPlaceTy<'tcx>> {
509597
let this = self.eval_context_mut();
510598

511-
// Note: if we were to inline `new_reserved` below we would find out that
512-
// `ty_is_freeze` is eventually unused because it appears in a `ty_is_freeze || true`.
513-
// We are nevertheless including it here for clarity.
514-
let ty_is_freeze = place.layout.ty.is_freeze(*this.tcx, this.typing_env());
515599
// Retag it. With protection! That is the entire point.
516600
let new_perm = NewPermission {
517-
initial_state: Permission::new_reserved(ty_is_freeze, /* protected */ true),
601+
// Note: If we are creating a protected Reserved, which can
602+
// never be ReservedIM, the value of the `ty_is_freeze`
603+
// argument doesn't matter
604+
// (`ty_is_freeze || true` in `new_reserved` will always be `true`).
605+
freeze_perm: Permission::new_reserved(
606+
/* ty_is_freeze */ true, /* protected */ true,
607+
),
608+
freeze_access: true,
609+
nonfreeze_perm: Permission::new_reserved(
610+
/* ty_is_freeze */ false, /* protected */ true,
611+
),
612+
nonfreeze_access: true,
518613
protector: Some(ProtectorKind::StrongProtector),
519-
initial_read: true,
520614
};
521615
this.tb_retag_place(place, new_perm)
522616
}

src/tools/miri/src/borrow_tracker/tree_borrows/perms.rs

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ impl PermissionPriv {
9494
}
9595

9696
/// Reject `ReservedIM` that cannot exist in the presence of a protector.
97+
#[cfg(test)]
9798
fn compatible_with_protector(&self) -> bool {
9899
// FIXME(TB-Cell): It is unclear what to do here.
99100
// `Cell` will occur with a protector but won't provide the guarantees
@@ -253,10 +254,6 @@ impl Permission {
253254
pub fn is_disabled(&self) -> bool {
254255
self.inner == Disabled
255256
}
256-
/// Check if `self` is the post-child-write state of a pointer (is `Active`).
257-
pub fn is_active(&self) -> bool {
258-
self.inner == Active
259-
}
260257
/// Check if `self` is the never-allow-writes-again state of a pointer (is `Frozen`).
261258
pub fn is_frozen(&self) -> bool {
262259
self.inner == Frozen
@@ -289,6 +286,11 @@ impl Permission {
289286
/// is a protector is relevant because being protected takes priority over being
290287
/// interior mutable)
291288
pub fn new_reserved(ty_is_freeze: bool, protected: bool) -> Self {
289+
// As demonstrated by `tests/fail/tree_borrows/reservedim_spurious_write.rs`,
290+
// interior mutability and protectors interact poorly.
291+
// To eliminate the case of Protected Reserved IM we override interior mutability
292+
// in the case of a protected reference: protected references are always considered
293+
// "freeze" in their reservation phase.
292294
if ty_is_freeze || protected { Self::new_reserved_frz() } else { Self::new_reserved_im() }
293295
}
294296

@@ -309,6 +311,7 @@ impl Permission {
309311
}
310312

311313
/// Reject `ReservedIM` that cannot exist in the presence of a protector.
314+
#[cfg(test)]
312315
pub fn compatible_with_protector(&self) -> bool {
313316
self.inner.compatible_with_protector()
314317
}
@@ -393,11 +396,6 @@ impl PermTransition {
393396
self.from <= self.to
394397
}
395398

396-
pub fn from(from: Permission, to: Permission) -> Option<Self> {
397-
let t = Self { from: from.inner, to: to.inner };
398-
t.is_possible().then_some(t)
399-
}
400-
401399
pub fn is_noop(self) -> bool {
402400
self.from == self.to
403401
}
@@ -407,11 +405,6 @@ impl PermTransition {
407405
(starting_point.inner == self.from).then_some(Permission { inner: self.to })
408406
}
409407

410-
/// Extract starting point of a transition
411-
pub fn started(self) -> Permission {
412-
Permission { inner: self.from }
413-
}
414-
415408
/// Determines if this transition would disable the permission.
416409
pub fn produces_disabled(self) -> bool {
417410
self.to == Disabled

0 commit comments

Comments
 (0)