Skip to content

Commit d1174a9

Browse files
committed
Auto merge of #3013 - RalfJung:tree-borrow-data-races, r=RalfJung
tree borrows: consider some retags as writes for the purpose of data races Turns out not all retags can be freely reordered. Those that cannot should be considered writes for the data race model, to aid with optimizations. Only the last commit is the actual change; the ones before that are some refactoring I couldn't stop myself from doing.
2 parents 3d9e8f1 + 36716dc commit d1174a9

20 files changed

+346
-201
lines changed

Diff for: src/tools/miri/src/borrow_tracker/stacked_borrows/mod.rs

+12-25
Original file line numberDiff line numberDiff line change
@@ -604,16 +604,15 @@ impl<'mir: 'ecx, 'tcx: 'mir, 'ecx> EvalContextPrivExt<'mir, 'tcx, 'ecx>
604604
{
605605
}
606606
trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'mir, 'tcx> {
607-
/// Returns the `AllocId` the reborrow was done in, if some actual borrow stack manipulation
608-
/// happened.
607+
/// Returns the provenance that should be used henceforth.
609608
fn sb_reborrow(
610609
&mut self,
611610
place: &MPlaceTy<'tcx, Provenance>,
612611
size: Size,
613612
new_perm: NewPermission,
614613
new_tag: BorTag,
615614
retag_info: RetagInfo, // diagnostics info about this retag
616-
) -> InterpResult<'tcx, Option<AllocId>> {
615+
) -> InterpResult<'tcx, Option<Provenance>> {
617616
let this = self.eval_context_mut();
618617
// Ensure we bail out if the pointer goes out-of-bounds (see miri#1050).
619618
this.check_ptr_access_align(place.ptr, size, Align::ONE, CheckInAllocMsg::InboundsTest)?;
@@ -695,11 +694,14 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
695694
// pointer tagging for example all calls to get_unchecked on them are invalid.
696695
if let Ok((alloc_id, base_offset, orig_tag)) = this.ptr_try_get_alloc_id(place.ptr) {
697696
log_creation(this, Some((alloc_id, base_offset, orig_tag)))?;
698-
return Ok(Some(alloc_id));
697+
// Still give it the new provenance, it got retagged after all.
698+
return Ok(Some(Provenance::Concrete { alloc_id, tag: new_tag }));
699+
} else {
700+
// This pointer doesn't come with an AllocId. :shrug:
701+
log_creation(this, None)?;
702+
// Provenance unchanged.
703+
return Ok(place.ptr.provenance);
699704
}
700-
// This pointer doesn't come with an AllocId. :shrug:
701-
log_creation(this, None)?;
702-
return Ok(None);
703705
}
704706

705707
let (alloc_id, base_offset, orig_tag) = this.ptr_get_alloc_id(place.ptr)?;
@@ -804,7 +806,7 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
804806
}
805807
}
806808

807-
Ok(Some(alloc_id))
809+
Ok(Some(Provenance::Concrete { alloc_id, tag: new_tag }))
808810
}
809811

810812
/// Retags an individual pointer, returning the retagged version.
@@ -831,25 +833,10 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
831833
let new_tag = this.machine.borrow_tracker.as_mut().unwrap().get_mut().new_ptr();
832834

833835
// Reborrow.
834-
let alloc_id = this.sb_reborrow(&place, size, new_perm, new_tag, info)?;
836+
let new_prov = this.sb_reborrow(&place, size, new_perm, new_tag, info)?;
835837

836838
// Adjust pointer.
837-
let new_place = place.map_provenance(|p| {
838-
p.map(|prov| {
839-
match alloc_id {
840-
Some(alloc_id) => {
841-
// If `reborrow` could figure out the AllocId of this ptr, hard-code it into the new one.
842-
// Even if we started out with a wildcard, this newly retagged pointer is tied to that allocation.
843-
Provenance::Concrete { alloc_id, tag: new_tag }
844-
}
845-
None => {
846-
// Looks like this has to stay a wildcard pointer.
847-
assert!(matches!(prov, Provenance::Wildcard));
848-
Provenance::Wildcard
849-
}
850-
}
851-
})
852-
});
839+
let new_place = place.map_provenance(|_| new_prov);
853840

854841
// Return new pointer.
855842
Ok(ImmTy::from_immediate(new_place.to_ref(this), val.layout))

Diff for: src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ impl<'tcx> Tree {
218218
}
219219
}
220220

221-
#[derive(Debug, Clone, Copy, PartialEq)]
221+
#[derive(Debug, Clone, Copy)]
222222
pub(super) enum TransitionError {
223223
/// This access is not allowed because some parent tag has insufficient permissions.
224224
/// For example, if a tag is `Frozen` and encounters a child write this will

Diff for: src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs

+47-46
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ impl<'tcx> NewPermission {
117117
let ty_is_freeze = pointee.is_freeze(*cx.tcx, cx.param_env());
118118
let ty_is_unpin = pointee.is_unpin(*cx.tcx, cx.param_env());
119119
let initial_state = match mutability {
120-
Mutability::Mut if ty_is_unpin => Permission::new_unique_2phase(ty_is_freeze),
120+
Mutability::Mut if ty_is_unpin => Permission::new_reserved(ty_is_freeze),
121121
Mutability::Not if ty_is_freeze => Permission::new_frozen(),
122122
// Raw pointers never enter this function so they are not handled.
123123
// However raw pointers are not the only pointers that take the parent
@@ -146,7 +146,7 @@ impl<'tcx> NewPermission {
146146
let ty_is_freeze = ty.is_freeze(*cx.tcx, cx.param_env());
147147
Self {
148148
zero_size,
149-
initial_state: Permission::new_unique_2phase(ty_is_freeze),
149+
initial_state: Permission::new_reserved(ty_is_freeze),
150150
protector: (kind == RetagKind::FnEntry).then_some(ProtectorKind::WeakProtector),
151151
}
152152
})
@@ -161,22 +161,14 @@ impl<'mir: 'ecx, 'tcx: 'mir, 'ecx> EvalContextPrivExt<'mir, 'tcx, 'ecx>
161161
{
162162
}
163163
trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'mir, 'tcx> {
164-
/// Returns the `AllocId` the reborrow was done in, if there is some actual
165-
/// memory associated with this pointer. Returns `None` if there is no actual
166-
/// memory allocated. Also checks that the reborrow of size `ptr_size` is
167-
/// within bounds of the allocation.
168-
///
169-
/// Also returns the tag that the pointer should get, which is essentially
170-
/// `if new_perm.is_some() { new_tag } else { parent_tag }` along with
171-
/// some logging (always) and fake reads (if `new_perm` is
172-
/// `Some(NewPermission { perform_read_access: true }`).
164+
/// Returns the provenance that should be used henceforth.
173165
fn tb_reborrow(
174166
&mut self,
175167
place: &MPlaceTy<'tcx, Provenance>, // parent tag extracted from here
176168
ptr_size: Size,
177169
new_perm: NewPermission,
178170
new_tag: BorTag,
179-
) -> InterpResult<'tcx, Option<(AllocId, BorTag)>> {
171+
) -> InterpResult<'tcx, Option<Provenance>> {
180172
let this = self.eval_context_mut();
181173
// Ensure we bail out if the pointer goes out-of-bounds (see miri#1050).
182174
this.check_ptr_access_align(
@@ -222,13 +214,14 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
222214
place.layout.ty,
223215
);
224216
log_creation(this, None)?;
225-
return Ok(None);
217+
// Keep original provenance.
218+
return Ok(place.ptr.provenance);
226219
}
227220
};
228221
log_creation(this, Some((alloc_id, base_offset, parent_prov)))?;
229222

230223
let orig_tag = match parent_prov {
231-
ProvenanceExtra::Wildcard => return Ok(None), // TODO: handle wildcard pointers
224+
ProvenanceExtra::Wildcard => return Ok(place.ptr.provenance), // TODO: handle wildcard pointers
232225
ProvenanceExtra::Concrete(tag) => tag,
233226
};
234227

@@ -255,31 +248,54 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
255248
.insert(new_tag, protect);
256249
}
257250

251+
let alloc_kind = this.get_alloc_info(alloc_id).2;
252+
if !matches!(alloc_kind, AllocKind::LiveData) {
253+
assert_eq!(ptr_size, Size::ZERO); // we did the deref check above, size has to be 0 here
254+
// There's not actually any bytes here where accesses could even be tracked.
255+
// Just produce the new provenance, nothing else to do.
256+
return Ok(Some(Provenance::Concrete { alloc_id, tag: new_tag }));
257+
}
258+
258259
let span = this.machine.current_span();
259260
let alloc_extra = this.get_alloc_extra(alloc_id)?;
260261
let range = alloc_range(base_offset, ptr_size);
261262
let mut tree_borrows = alloc_extra.borrow_tracker_tb().borrow_mut();
262263

263264
// All reborrows incur a (possibly zero-sized) read access to the parent
264-
{
265-
let global = &this.machine.borrow_tracker.as_ref().unwrap();
266-
let span = this.machine.current_span();
267-
tree_borrows.perform_access(
268-
AccessKind::Read,
269-
orig_tag,
270-
range,
271-
global,
272-
span,
273-
diagnostics::AccessCause::Reborrow,
274-
)?;
265+
tree_borrows.perform_access(
266+
AccessKind::Read,
267+
orig_tag,
268+
range,
269+
this.machine.borrow_tracker.as_ref().unwrap(),
270+
this.machine.current_span(),
271+
diagnostics::AccessCause::Reborrow,
272+
)?;
273+
// Record the parent-child pair in the tree.
274+
tree_borrows.new_child(orig_tag, new_tag, new_perm.initial_state, range, span)?;
275+
drop(tree_borrows);
276+
277+
// Also inform the data race model (but only if any bytes are actually affected).
278+
if range.size.bytes() > 0 {
275279
if let Some(data_race) = alloc_extra.data_race.as_ref() {
276-
data_race.read(alloc_id, range, &this.machine)?;
280+
// We sometimes need to make it a write, since not all retags commute with reads!
281+
// FIXME: Is that truly the semantics we want? Some optimizations are likely to be
282+
// very unhappy without this. We'd tsill ge some UB just by picking a suitable
283+
// interleaving, but wether UB happens can depend on whether a write occurs in the
284+
// future...
285+
let is_write = new_perm.initial_state.is_active()
286+
|| (new_perm.initial_state.is_resrved() && new_perm.protector.is_some());
287+
if is_write {
288+
// Need to get mutable access to alloc_extra.
289+
// (Cannot always do this as we can do read-only reborrowing on read-only allocations.)
290+
let (alloc_extra, machine) = this.get_alloc_extra_mut(alloc_id)?;
291+
alloc_extra.data_race.as_mut().unwrap().write(alloc_id, range, machine)?;
292+
} else {
293+
data_race.read(alloc_id, range, &this.machine)?;
294+
}
277295
}
278296
}
279297

280-
// Record the parent-child pair in the tree.
281-
tree_borrows.new_child(orig_tag, new_tag, new_perm.initial_state, range, span)?;
282-
Ok(Some((alloc_id, new_tag)))
298+
Ok(Some(Provenance::Concrete { alloc_id, tag: new_tag }))
283299
}
284300

285301
/// Retags an individual pointer, returning the retagged version.
@@ -315,25 +331,10 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
315331
let new_tag = this.machine.borrow_tracker.as_mut().unwrap().get_mut().new_ptr();
316332

317333
// Compute the actual reborrow.
318-
let reborrowed = this.tb_reborrow(&place, reborrow_size, new_perm, new_tag)?;
334+
let new_prov = this.tb_reborrow(&place, reborrow_size, new_perm, new_tag)?;
319335

320336
// Adjust pointer.
321-
let new_place = place.map_provenance(|p| {
322-
p.map(|prov| {
323-
match reborrowed {
324-
Some((alloc_id, actual_tag)) => {
325-
// If `reborrow` could figure out the AllocId of this ptr, hard-code it into the new one.
326-
// Even if we started out with a wildcard, this newly retagged pointer is tied to that allocation.
327-
Provenance::Concrete { alloc_id, tag: actual_tag }
328-
}
329-
None => {
330-
// Looks like this has to stay a wildcard pointer.
331-
assert!(matches!(prov, Provenance::Wildcard));
332-
Provenance::Wildcard
333-
}
334-
}
335-
})
336-
});
337+
let new_place = place.map_provenance(|_| new_prov);
337338

338339
// Return new pointer.
339340
Ok(ImmTy::from_immediate(new_place.to_ref(this), val.layout))

Diff for: src/tools/miri/src/borrow_tracker/tree_borrows/perms.rs

+15-8
Original file line numberDiff line numberDiff line change
@@ -134,25 +134,32 @@ pub struct PermTransition {
134134

135135
impl Permission {
136136
/// Default initial permission of the root of a new tree.
137-
pub fn new_root() -> Self {
137+
pub fn new_active() -> Self {
138138
Self { inner: Active }
139139
}
140140

141141
/// Default initial permission of a reborrowed mutable reference.
142-
pub fn new_unique_2phase(ty_is_freeze: bool) -> Self {
142+
pub fn new_reserved(ty_is_freeze: bool) -> Self {
143143
Self { inner: Reserved { ty_is_freeze } }
144144
}
145145

146-
/// Default initial permission for return place.
147-
pub fn new_active() -> Self {
148-
Self { inner: Active }
149-
}
150-
151146
/// Default initial permission of a reborrowed shared reference
152147
pub fn new_frozen() -> Self {
153148
Self { inner: Frozen }
154149
}
155150

151+
pub fn is_active(self) -> bool {
152+
matches!(self.inner, Active)
153+
}
154+
155+
pub fn is_resrved(self) -> bool {
156+
matches!(self.inner, Reserved { .. })
157+
}
158+
159+
pub fn is_frozen(self) -> bool {
160+
matches!(self.inner, Frozen)
161+
}
162+
156163
/// Apply the transition to the inner PermissionPriv.
157164
pub fn perform_access(
158165
kind: AccessKind,
@@ -438,7 +445,7 @@ mod propagation_optimization_checks {
438445
}
439446

440447
#[test]
441-
fn foreign_read_is_noop_after_write() {
448+
fn foreign_read_is_noop_after_foreign_write() {
442449
use transition::*;
443450
let old_access = AccessKind::Write;
444451
let new_access = AccessKind::Read;

Diff for: src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs

+37-7
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ impl LocationState {
110110

111111
// Helper to optimize the tree traversal.
112112
// The optimization here consists of observing thanks to the tests
113-
// `foreign_read_is_noop_after_write` and `all_transitions_idempotent`,
113+
// `foreign_read_is_noop_after_foreign_write` and `all_transitions_idempotent`,
114114
// that there are actually just three possible sequences of events that can occur
115115
// in between two child accesses that produce different results.
116116
//
@@ -139,7 +139,7 @@ impl LocationState {
139139
let new_access_noop = match (self.latest_foreign_access, access_kind) {
140140
// Previously applied transition makes the new one a guaranteed
141141
// noop in the two following cases:
142-
// (1) justified by `foreign_read_is_noop_after_write`
142+
// (1) justified by `foreign_read_is_noop_after_foreign_write`
143143
(Some(AccessKind::Write), AccessKind::Read) => true,
144144
// (2) justified by `all_transitions_idempotent`
145145
(Some(old), new) if old == new => true,
@@ -376,7 +376,7 @@ where {
376376
impl Tree {
377377
/// Create a new tree, with only a root pointer.
378378
pub fn new(root_tag: BorTag, size: Size, span: Span) -> Self {
379-
let root_perm = Permission::new_root();
379+
let root_perm = Permission::new_active();
380380
let mut tag_mapping = UniKeyMap::default();
381381
let root_idx = tag_mapping.insert(root_tag);
382382
let nodes = {
@@ -670,7 +670,8 @@ impl AccessRelatedness {
670670
mod commutation_tests {
671671
use super::*;
672672
impl LocationState {
673-
pub fn all_without_access() -> impl Iterator<Item = Self> {
673+
pub fn all() -> impl Iterator<Item = Self> {
674+
// We keep `latest_foreign_access` at `None` as that's just a cache.
674675
Permission::all().flat_map(|permission| {
675676
[false, true].into_iter().map(move |initialized| {
676677
Self { permission, initialized, latest_foreign_access: None }
@@ -695,12 +696,12 @@ mod commutation_tests {
695696
// Any protector state works, but we can't move reads across function boundaries
696697
// so the two read accesses occur under the same protector.
697698
for &protected in &[true, false] {
698-
for loc in LocationState::all_without_access() {
699+
for loc in LocationState::all() {
699700
// Apply 1 then 2. Failure here means that there is UB in the source
700701
// and we skip the check in the target.
701702
let mut loc12 = loc;
702-
let Ok(_) = loc12.perform_access(kind, rel1, protected) else { continue; };
703-
let Ok(_) = loc12.perform_access(kind, rel2, protected) else { continue; };
703+
let Ok(_) = loc12.perform_access(kind, rel1, protected) else { continue };
704+
let Ok(_) = loc12.perform_access(kind, rel2, protected) else { continue };
704705

705706
// If 1 followed by 2 succeeded, then 2 followed by 1 must also succeed...
706707
let mut loc21 = loc;
@@ -718,4 +719,33 @@ mod commutation_tests {
718719
}
719720
}
720721
}
722+
723+
#[test]
724+
#[rustfmt::skip]
725+
// Ensure that of 2 accesses happen, one foreign and one a child, and we are protected, that we
726+
// get UB unless they are both reads.
727+
fn protected_enforces_noalias() {
728+
for rel1 in AccessRelatedness::all() {
729+
for rel2 in AccessRelatedness::all() {
730+
if rel1.is_foreign() == rel2.is_foreign() {
731+
// We want to check pairs of accesses where one is foreign and one is not.
732+
continue;
733+
}
734+
for kind1 in AccessKind::all() {
735+
for kind2 in AccessKind::all() {
736+
for mut state in LocationState::all() {
737+
let protected = true;
738+
let Ok(_) = state.perform_access(kind1, rel1, protected) else { continue };
739+
let Ok(_) = state.perform_access(kind2, rel2, protected) else { continue };
740+
// If these were both allowed, it must have been two reads.
741+
assert!(
742+
kind1 == AccessKind::Read && kind2 == AccessKind::Read,
743+
"failed to enforce noalias between two accesses that are not both reads"
744+
);
745+
}
746+
}
747+
}
748+
}
749+
}
750+
}
721751
}

Diff for: src/tools/miri/src/machine.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1219,7 +1219,8 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
12191219
// If we have a borrow tracker, we also have it set up protection so that all reads *and
12201220
// writes* during this call are insta-UB.
12211221
if ecx.machine.borrow_tracker.is_some() {
1222-
if let Either::Left(place) = place.as_mplace_or_local() {
1222+
// Have to do `to_op` first because a `Place::Local` doesn't imply the local doesn't have an address.
1223+
if let Either::Left(place) = ecx.place_to_op(place)?.as_mplace_or_imm() {
12231224
ecx.protect_place(&place)?;
12241225
} else {
12251226
// Locals that don't have their address taken are as protected as they can ever be.

0 commit comments

Comments
 (0)