Skip to content

Commit f479404

Browse files
committed
!Unpin retags must still be reads, to check dereferenceable
also fix ICE on deallocation error and avoid redundant find_granting on retag
1 parent 7d0db1e commit f479404

File tree

7 files changed

+151
-57
lines changed

7 files changed

+151
-57
lines changed

src/tools/miri/src/stacked_borrows/diagnostics.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,12 @@ impl<'history, 'ecx, 'mir, 'tcx> DiagnosticCx<'history, 'ecx, 'mir, 'tcx> {
353353

354354
/// Report a descriptive error when `new` could not be granted from `derived_from`.
355355
#[inline(never)] // This is only called on fatal code paths
356-
pub(super) fn grant_error(&self, perm: Permission, stack: &Stack) -> InterpError<'tcx> {
356+
pub(super) fn grant_error(&self, stack: &Stack) -> InterpError<'tcx> {
357357
let Operation::Retag(op) = &self.operation else {
358358
unreachable!("grant_error should only be called during a retag")
359359
};
360+
let perm =
361+
op.permission.expect("`start_grant` must be called before calling `grant_error`");
360362
let action = format!(
361363
"trying to retag from {:?} for {:?} permission at {:?}[{:#x}]",
362364
op.orig_tag,
@@ -374,8 +376,11 @@ impl<'history, 'ecx, 'mir, 'tcx> DiagnosticCx<'history, 'ecx, 'mir, 'tcx> {
374376
/// Report a descriptive error when `access` is not permitted based on `tag`.
375377
#[inline(never)] // This is only called on fatal code paths
376378
pub(super) fn access_error(&self, stack: &Stack) -> InterpError<'tcx> {
377-
let Operation::Access(op) = &self.operation else {
378-
unreachable!("access_error should only be called during an access")
379+
// Deallocation and retagging also do an access as part of their thing, so handle that here, too.
380+
let op = match &self.operation {
381+
Operation::Access(op) => op,
382+
Operation::Retag(_) => return self.grant_error(stack),
383+
Operation::Dealloc(_) => return self.dealloc_error(stack),
379384
};
380385
let action = format!(
381386
"attempting a {access} using {tag:?} at {alloc_id:?}[{offset:#x}]",
@@ -428,14 +433,16 @@ impl<'history, 'ecx, 'mir, 'tcx> DiagnosticCx<'history, 'ecx, 'mir, 'tcx> {
428433
}
429434

430435
#[inline(never)] // This is only called on fatal code paths
431-
pub fn dealloc_error(&self) -> InterpError<'tcx> {
436+
pub fn dealloc_error(&self, stack: &Stack) -> InterpError<'tcx> {
432437
let Operation::Dealloc(op) = &self.operation else {
433438
unreachable!("dealloc_error should only be called during a deallocation")
434439
};
435440
err_sb_ub(
436441
format!(
437-
"no item granting write access for deallocation to tag {:?} at {:?} found in borrow stack",
438-
op.tag, self.history.id,
442+
"attempting deallocation using {tag:?} at {alloc_id:?}{cause}",
443+
tag = op.tag,
444+
alloc_id = self.history.id,
445+
cause = error_cause(stack, op.tag),
439446
),
440447
None,
441448
op.tag.and_then(|tag| self.get_logs_relevant_to(tag, None)),

src/tools/miri/src/stacked_borrows/mod.rs

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ impl<'tcx> Stack {
392392

393393
// Step 1: Find granting item.
394394
let granting_idx =
395-
self.find_granting(access, tag, exposed_tags).map_err(|_| dcx.access_error(self))?;
395+
self.find_granting(access, tag, exposed_tags).map_err(|()| dcx.access_error(self))?;
396396

397397
// Step 2: Remove incompatible items above them. Make sure we do not remove protected
398398
// items. Behavior differs for reads and writes.
@@ -476,8 +476,7 @@ impl<'tcx> Stack {
476476
) -> InterpResult<'tcx> {
477477
// Step 1: Make a write access.
478478
// As part of this we do regular protector checking, i.e. even weakly protected items cause UB when popped.
479-
self.access(AccessKind::Write, tag, global, dcx, exposed_tags)
480-
.map_err(|_| dcx.dealloc_error())?;
479+
self.access(AccessKind::Write, tag, global, dcx, exposed_tags)?;
481480

482481
// Step 2: Pretend we remove the remaining items, checking if any are strongly protected.
483482
for idx in (0..self.len()).rev() {
@@ -489,39 +488,42 @@ impl<'tcx> Stack {
489488
}
490489

491490
/// Derive a new pointer from one with the given tag.
492-
/// `weak` controls whether this operation is weak or strong: weak granting does not act as
493-
/// an access, and they add the new item directly on top of the one it is derived
494-
/// from instead of all the way at the top of the stack.
495-
/// `range` refers the entire operation, and `offset` refers to the specific location in
496-
/// `range` that we are currently checking.
491+
///
492+
/// `access` indicates which kind of memory access this retag itself should correspond to.
497493
fn grant(
498494
&mut self,
499495
derived_from: ProvenanceExtra,
500496
new: Item,
497+
access: Option<AccessKind>,
501498
global: &GlobalStateInner,
502499
dcx: &mut DiagnosticCx<'_, '_, '_, 'tcx>,
503500
exposed_tags: &FxHashSet<SbTag>,
504501
) -> InterpResult<'tcx> {
505502
dcx.start_grant(new.perm());
506503

507-
// Figure out which access `perm` corresponds to.
508-
let access =
509-
if new.perm().grants(AccessKind::Write) { AccessKind::Write } else { AccessKind::Read };
510-
511-
// Now we figure out which item grants our parent (`derived_from`) this kind of access.
512-
// We use that to determine where to put the new item.
513-
let granting_idx = self
514-
.find_granting(access, derived_from, exposed_tags)
515-
.map_err(|_| dcx.grant_error(new.perm(), self))?;
516-
517504
// Compute where to put the new item.
518505
// Either way, we ensure that we insert the new item in a way such that between
519506
// `derived_from` and the new one, there are only items *compatible with* `derived_from`.
520-
let new_idx = if new.perm() == Permission::SharedReadWrite {
521-
assert!(
522-
access == AccessKind::Write,
523-
"this case only makes sense for stack-like accesses"
524-
);
507+
let new_idx = if let Some(access) = access {
508+
// Simple case: We are just a regular memory access, and then push our thing on top,
509+
// like a regular stack.
510+
// This ensures F2b for `Unique`, by removing offending `SharedReadOnly`.
511+
self.access(access, derived_from, global, dcx, exposed_tags)?;
512+
513+
// We insert "as far up as possible": We know only compatible items are remaining
514+
// on top of `derived_from`, and we want the new item at the top so that we
515+
// get the strongest possible guarantees.
516+
// This ensures U1 and F1.
517+
self.len()
518+
} else {
519+
// The tricky case: creating a new SRW permission without actually being an access.
520+
assert!(new.perm() == Permission::SharedReadWrite);
521+
522+
// First we figure out which item grants our parent (`derived_from`) this kind of access.
523+
// We use that to determine where to put the new item.
524+
let granting_idx = self
525+
.find_granting(AccessKind::Write, derived_from, exposed_tags)
526+
.map_err(|()| dcx.grant_error(self))?;
525527

526528
let (Some(granting_idx), ProvenanceExtra::Concrete(_)) = (granting_idx, derived_from) else {
527529
// The parent is a wildcard pointer or matched the unknown bottom.
@@ -538,17 +540,6 @@ impl<'tcx> Stack {
538540
// be popped to (i.e., we insert it above all the write-compatible items).
539541
// This ensures F2b by adding the new item below any potentially existing `SharedReadOnly`.
540542
self.find_first_write_incompatible(granting_idx)
541-
} else {
542-
// A "safe" reborrow for a pointer that actually expects some aliasing guarantees.
543-
// Here, creating a reference actually counts as an access.
544-
// This ensures F2b for `Unique`, by removing offending `SharedReadOnly`.
545-
self.access(access, derived_from, global, dcx, exposed_tags)?;
546-
547-
// We insert "as far up as possible": We know only compatible items are remaining
548-
// on top of `derived_from`, and we want the new item at the top so that we
549-
// get the strongest possible guarantees.
550-
// This ensures U1 and F1.
551-
self.len()
552543
};
553544

554545
// Put the new item there.
@@ -864,18 +855,22 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
864855
// Update the stacks.
865856
// Make sure that raw pointers and mutable shared references are reborrowed "weak":
866857
// There could be existing unique pointers reborrowed from them that should remain valid!
867-
let perm = match kind {
868-
RefKind::Unique { two_phase: false }
869-
if place.layout.ty.is_unpin(*this.tcx, this.param_env()) =>
870-
{
871-
// Only if the type is unpin do we actually enforce uniqueness
872-
Permission::Unique
858+
let (perm, access) = match kind {
859+
RefKind::Unique { two_phase } => {
860+
// Permission is Unique only if the type is `Unpin` and this is not twophase
861+
let perm = if !two_phase && place.layout.ty.is_unpin(*this.tcx, this.param_env()) {
862+
Permission::Unique
863+
} else {
864+
Permission::SharedReadWrite
865+
};
866+
// We do an access for all full borrows, even if `!Unpin`.
867+
let access = if !two_phase { Some(AccessKind::Write) } else { None };
868+
(perm, access)
873869
}
874-
RefKind::Unique { .. } => {
875-
// Two-phase references and !Unpin references are treated as SharedReadWrite
876-
Permission::SharedReadWrite
870+
RefKind::Raw { mutable: true } => {
871+
// Creating a raw ptr does not count as an access
872+
(Permission::SharedReadWrite, None)
877873
}
878-
RefKind::Raw { mutable: true } => Permission::SharedReadWrite,
879874
RefKind::Shared | RefKind::Raw { mutable: false } => {
880875
// Shared references and *const are a whole different kind of game, the
881876
// permission is not uniform across the entire range!
@@ -892,10 +887,13 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
892887
// Adjust range.
893888
range.start += base_offset;
894889
// We are only ever `SharedReadOnly` inside the frozen bits.
895-
let perm = if frozen {
896-
Permission::SharedReadOnly
890+
let (perm, access) = if frozen {
891+
(Permission::SharedReadOnly, Some(AccessKind::Read))
897892
} else {
898-
Permission::SharedReadWrite
893+
// Inside UnsafeCell, this does *not* count as an access, as there
894+
// might actually be mutable references further up the stack that
895+
// we have to keep alive.
896+
(Permission::SharedReadWrite, None)
899897
};
900898
let protected = if frozen {
901899
protect.is_some()
@@ -914,7 +912,7 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
914912
alloc_range(base_offset, size),
915913
);
916914
stacked_borrows.for_each(range, dcx, |stack, dcx, exposed_tags| {
917-
stack.grant(orig_tag, item, &global, dcx, exposed_tags)
915+
stack.grant(orig_tag, item, access, &global, dcx, exposed_tags)
918916
})
919917
})?;
920918
return Ok(Some(alloc_id));
@@ -941,7 +939,7 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
941939
alloc_range(base_offset, size),
942940
);
943941
stacked_borrows.for_each(range, dcx, |stack, dcx, exposed_tags| {
944-
stack.grant(orig_tag, item, &global, dcx, exposed_tags)
942+
stack.grant(orig_tag, item, access, &global, dcx, exposed_tags)
945943
})?;
946944

947945
Ok(Some(alloc_id))

src/tools/miri/src/stacked_borrows/stack.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,10 +367,10 @@ impl<'tcx> Stack {
367367

368368
/// Find all `Unique` elements in this borrow stack above `granting_idx`, pass a copy of them
369369
/// to the `visitor`, then set their `Permission` to `Disabled`.
370-
pub fn disable_uniques_starting_at<V: FnMut(Item) -> crate::InterpResult<'tcx>>(
370+
pub fn disable_uniques_starting_at(
371371
&mut self,
372372
disable_start: usize,
373-
mut visitor: V,
373+
mut visitor: impl FnMut(Item) -> crate::InterpResult<'tcx>,
374374
) -> crate::InterpResult<'tcx> {
375375
#[cfg(feature = "stack-cache")]
376376
let unique_range = self.unique_range.clone();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//@error-pattern: /deallocation .* tag does not exist in the borrow stack/
2+
use std::alloc::{alloc, dealloc, Layout};
3+
4+
fn main() {
5+
unsafe {
6+
let x = alloc(Layout::from_size_align_unchecked(1, 1));
7+
let ptr1 = (&mut *x) as *mut u8;
8+
let ptr2 = (&mut *ptr1) as *mut u8;
9+
// Invalidate ptr2 by writing to ptr1.
10+
ptr1.write(0);
11+
// Deallocate through ptr2.
12+
dealloc(ptr2, Layout::from_size_align_unchecked(1, 1));
13+
}
14+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
error: Undefined Behavior: attempting deallocation using <TAG> at ALLOC, but that tag does not exist in the borrow stack for this location
2+
--> RUSTLIB/alloc/src/alloc.rs:LL:CC
3+
|
4+
LL | unsafe { __rust_dealloc(ptr, layout.size(), layout.align()) }
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ attempting deallocation using <TAG> at ALLOC, but that tag does not exist in the borrow stack for this location
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 SharedReadWrite retag at offsets [0x0..0x1]
10+
--> $DIR/illegal_deALLOC.rs:LL:CC
11+
|
12+
LL | let ptr2 = (&mut *ptr1) as *mut u8;
13+
| ^^^^^^^^^^^^
14+
help: <TAG> was later invalidated at offsets [0x0..0x1] by a write access
15+
--> $DIR/illegal_deALLOC.rs:LL:CC
16+
|
17+
LL | ptr1.write(0);
18+
| ^^^^^^^^^^^^^
19+
= note: BACKTRACE:
20+
= note: inside `std::alloc::dealloc` at RUSTLIB/alloc/src/alloc.rs:LL:CC
21+
note: inside `main` at $DIR/illegal_deALLOC.rs:LL:CC
22+
--> $DIR/illegal_deALLOC.rs:LL:CC
23+
|
24+
LL | dealloc(ptr2, Layout::from_size_align_unchecked(1, 1));
25+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26+
27+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
28+
29+
error: aborting due to previous error
30+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//! Reborrowing a `&mut !Unpin` must still act like a (fake) read.
2+
use std::marker::PhantomPinned;
3+
4+
struct NotUnpin(i32, PhantomPinned);
5+
6+
fn main() {
7+
unsafe {
8+
let mut x = NotUnpin(0, PhantomPinned);
9+
// Mutable borrow of `Unpin` field (with lifetime laundering)
10+
let fieldref = &mut *(&mut x.0 as *mut i32);
11+
// Mutable reborrow of the entire `x`, which is `!Unpin` but should
12+
// still count as a read since we would add `dereferenceable`.
13+
let _xref = &mut x;
14+
// That read should have invalidated `fieldref`.
15+
*fieldref = 0; //~ ERROR: /write access .* tag does not exist in the borrow stack/
16+
}
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
error: Undefined Behavior: attempting a write access using <TAG> at ALLOC[0x0], but that tag does not exist in the borrow stack for this location
2+
--> $DIR/notunpin_dereferenceable_fakeread.rs:LL:CC
3+
|
4+
LL | *fieldref = 0;
5+
| ^^^^^^^^^^^^^
6+
| |
7+
| attempting a write access using <TAG> at ALLOC[0x0], but that tag does not exist in the borrow stack for this location
8+
| this error occurs as part of an access at ALLOC[0x0..0x4]
9+
|
10+
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
11+
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
12+
help: <TAG> was created by a Unique retag at offsets [0x0..0x4]
13+
--> $DIR/notunpin_dereferenceable_fakeread.rs:LL:CC
14+
|
15+
LL | let fieldref = &mut *(&mut x.0 as *mut i32);
16+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17+
help: <TAG> was later invalidated at offsets [0x0..0x4] by a SharedReadWrite retag
18+
--> $DIR/notunpin_dereferenceable_fakeread.rs:LL:CC
19+
|
20+
LL | let _xref = &mut x;
21+
| ^^^^^^
22+
= note: BACKTRACE:
23+
= note: inside `main` at $DIR/notunpin_dereferenceable_fakeread.rs:LL:CC
24+
25+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
26+
27+
error: aborting due to previous error
28+

0 commit comments

Comments
 (0)