Auto merge of #2287 - RalfJung:field-retagging, r=RalfJung

stacked borrows: add option for recursive field retagging
This commit is contained in:
bors 2022-06-29 23:04:07 +00:00
commit 5974e7d4a9
11 changed files with 219 additions and 84 deletions

View File

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

View File

@ -1 +1 @@
493c960a3e6cdd2e2fbe8b6ea130fadea05f1ab0
ddcbba036aee08f0709f98a92a342a278eae5c05

View File

@ -383,6 +383,8 @@ fn main() {
miri_config.provenance_mode = ProvenanceMode::Permissive;
} else if arg == "-Zmiri-mute-stdout-stderr" {
miri_config.mute_stdout_stderr = true;
} else if arg == "-Zmiri-retag-fields" {
miri_config.retag_fields = true;
} else if arg == "-Zmiri-track-raw-pointers" {
eprintln!(
"WARNING: `-Zmiri-track-raw-pointers` has no effect; it is enabled by default"

View File

@ -124,6 +124,8 @@ pub struct MiriConfig {
pub preemption_rate: f64,
/// Report the current instruction being executed every N basic blocks.
pub report_progress: Option<u32>,
/// Whether Stacked Borrows retagging should recurse into fields of datatypes.
pub retag_fields: bool,
}
impl Default for MiriConfig {
@ -154,6 +156,7 @@ fn default() -> MiriConfig {
mute_stdout_stderr: false,
preemption_rate: 0.01, // 1%
report_progress: None,
retag_fields: false,
}
}
}

View File

@ -354,6 +354,7 @@ pub(crate) fn new(config: &MiriConfig, layout_cx: LayoutCx<'tcx, TyCtxt<'tcx>>)
Some(RefCell::new(stacked_borrows::GlobalStateInner::new(
config.tracked_pointer_tags.clone(),
config.tracked_call_ids.clone(),
config.retag_fields,
)))
} else {
None

View File

@ -156,6 +156,8 @@ pub struct GlobalStateInner {
tracked_pointer_tags: HashSet<SbTag>,
/// The call ids to trace
tracked_call_ids: HashSet<CallId>,
/// Whether to recurse into datatypes when searching for pointers to retag.
retag_fields: bool,
}
/// We need interior mutable access to the global state.
@ -204,7 +206,11 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// Utilities for initialization and ID generation
impl GlobalStateInner {
pub fn new(tracked_pointer_tags: HashSet<SbTag>, tracked_call_ids: HashSet<CallId>) -> Self {
pub fn new(
tracked_pointer_tags: HashSet<SbTag>,
tracked_call_ids: HashSet<CallId>,
retag_fields: bool,
) -> Self {
GlobalStateInner {
next_ptr_tag: SbTag(NonZeroU64::new(1).unwrap()),
base_ptr_tags: FxHashMap::default(),
@ -212,6 +218,7 @@ pub fn new(tracked_pointer_tags: HashSet<SbTag>, tracked_call_ids: HashSet<CallI
active_calls: FxHashSet::default(),
tracked_pointer_tags,
tracked_call_ids,
retag_fields,
}
}
@ -1035,17 +1042,69 @@ fn qualify(ty: ty::Ty<'_>, kind: RetagKind) -> Option<(RefKind, bool)> {
}
}
// We only reborrow "bare" references/boxes.
// Not traversing into fields helps with <https://github.com/rust-lang/unsafe-code-guidelines/issues/125>,
// but might also cost us optimization and analyses. We will have to experiment more with this.
// We need a visitor to visit all references. However, that requires
// a `MPlaceTy` (or `OpTy), so we have a fast path for reference types that
// avoids allocating.
if let Some((mutbl, protector)) = qualify(place.layout.ty, kind) {
// Fast path.
let val = this.read_immediate(&this.place_to_op(place)?)?;
let val = this.retag_reference(&val, mutbl, protector)?;
this.write_immediate(*val, place)?;
return Ok(());
}
Ok(())
// If we don't want to recurse, we are already done.
if !this.machine.stacked_borrows.as_mut().unwrap().get_mut().retag_fields {
return Ok(());
}
// Skip some types that have no further structure we might care about.
if matches!(
place.layout.ty.kind(),
ty::RawPtr(..)
| ty::Ref(..)
| ty::Int(..)
| ty::Uint(..)
| ty::Float(..)
| ty::Bool
| ty::Char
) {
return Ok(());
}
// Now go visit this thing.
let place = this.force_allocation(place)?;
let mut visitor = RetagVisitor { ecx: this, kind };
return visitor.visit_value(&place);
// The actual visitor.
struct RetagVisitor<'ecx, 'mir, 'tcx> {
ecx: &'ecx mut MiriEvalContext<'mir, 'tcx>,
kind: RetagKind,
}
impl<'ecx, 'mir, 'tcx> MutValueVisitor<'mir, 'tcx, Evaluator<'mir, 'tcx>>
for RetagVisitor<'ecx, 'mir, 'tcx>
{
type V = MPlaceTy<'tcx, Tag>;
#[inline(always)]
fn ecx(&mut self) -> &mut MiriEvalContext<'mir, 'tcx> {
&mut self.ecx
}
fn visit_value(&mut self, place: &MPlaceTy<'tcx, Tag>) -> InterpResult<'tcx> {
if let Some((mutbl, protector)) = qualify(place.layout.ty, self.kind) {
let val = self.ecx.read_immediate(&place.into())?;
let val = self.ecx.retag_reference(&val, mutbl, protector)?;
self.ecx.write_immediate(*val, &(*place).into())?;
} else {
// Maybe we need to go deeper.
self.walk_value(place)?;
}
Ok(())
}
}
}
/// After a stack frame got pushed, retag the return place so that we are sure

View File

@ -0,0 +1,19 @@
// compile-flags: -Zmiri-retag-fields
// error-pattern: incompatible item is protected
struct Newtype<'a>(&'a mut i32);
fn dealloc_while_running(_n: Newtype<'_>, dealloc: impl FnOnce()) {
dealloc();
}
// Make sure that we protect references inside structs.
fn main() {
let ptr = Box::into_raw(Box::new(0i32));
#[rustfmt::skip] // I like my newlines
unsafe {
dealloc_while_running(
Newtype(&mut *ptr),
|| drop(Box::from_raw(ptr)),
)
};
}

View File

@ -0,0 +1,50 @@
error: Undefined Behavior: not granting access to tag <TAG> because incompatible item is protected: [Unique for <TAG> (call ID)]
--> RUSTLIB/alloc/src/boxed.rs:LL:CC
|
LL | Box(unsafe { Unique::new_unchecked(raw) }, alloc)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not granting access to tag <TAG> because incompatible item is protected: [Unique for <TAG> (call ID)]
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <TAG> was created by a retag at offsets [0x0..0x4]
--> $DIR/newtype_retagging.rs:LL:CC
|
LL | let ptr = Box::into_raw(Box::new(0i32));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: <TAG> was protected due to <TAG> which was created here
--> $DIR/newtype_retagging.rs:LL:CC
|
LL | Newtype(&mut *ptr),
| ^^^^^^^^^^^^^^^^^^
help: this protector is live for this call
--> $DIR/newtype_retagging.rs:LL:CC
|
LL | / fn dealloc_while_running(_n: Newtype<'_>, dealloc: impl FnOnce()) {
LL | | dealloc();
LL | | }
| |_^
= note: inside `std::boxed::Box::<i32>::from_raw_in` at RUSTLIB/alloc/src/boxed.rs:LL:CC
= note: inside `std::boxed::Box::<i32>::from_raw` at RUSTLIB/alloc/src/boxed.rs:LL:CC
note: inside closure at $DIR/newtype_retagging.rs:LL:CC
--> $DIR/newtype_retagging.rs:LL:CC
|
LL | || drop(Box::from_raw(ptr)),
| ^^^^^^^^^^^^^^^^^^
note: inside `dealloc_while_running::<[closure@$DIR/newtype_retagging.rs:LL:CC]>` at $DIR/newtype_retagging.rs:LL:CC
--> $DIR/newtype_retagging.rs:LL:CC
|
LL | dealloc();
| ^^^^^^^^^
note: inside `main` at $DIR/newtype_retagging.rs:LL:CC
--> $DIR/newtype_retagging.rs:LL:CC
|
LL | / dealloc_while_running(
LL | | Newtype(&mut *ptr),
LL | | || drop(Box::from_raw(ptr)),
LL | | )
| |_________^
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to previous error

View File

@ -1,4 +1,5 @@
use std::cell::{Cell, RefCell, UnsafeCell};
// compile-flags: -Zmiri-retag-fields
use std::cell::{Cell, Ref, RefCell, RefMut, UnsafeCell};
use std::mem::{self, MaybeUninit};
fn main() {
@ -8,6 +9,10 @@ fn main() {
unsafe_cell_2phase();
unsafe_cell_deallocate();
unsafe_cell_invalidate();
refcell_basic();
ref_protector();
ref_mut_protector();
rust_issue_68303();
}
fn aliasing_mut_and_shr() {
@ -99,3 +104,72 @@ fn f(_x: &UnsafeCell<i32>, y: *mut i32) {
// So using raw1 invalidates raw2.
f(unsafe { mem::transmute(raw2) }, raw1);
}
fn refcell_basic() {
let c = RefCell::new(42);
{
let s1 = c.borrow();
let _x: i32 = *s1;
let s2 = c.borrow();
let _x: i32 = *s1;
let _y: i32 = *s2;
let _x: i32 = *s1;
let _y: i32 = *s2;
}
{
let mut m = c.borrow_mut();
let _z: i32 = *m;
{
let s: &i32 = &*m;
let _x = *s;
}
*m = 23;
let _z: i32 = *m;
}
{
let s1 = c.borrow();
let _x: i32 = *s1;
let s2 = c.borrow();
let _x: i32 = *s1;
let _y: i32 = *s2;
let _x: i32 = *s1;
let _y: i32 = *s2;
}
}
// Adding a Stacked Borrows protector for `Ref` would break this
fn ref_protector() {
fn break_it(rc: &RefCell<i32>, r: Ref<'_, i32>) {
// `r` has a shared reference, it is passed in as argument and hence
// a protector is added that marks this memory as read-only for the entire
// duration of this function.
drop(r);
// *oops* here we can mutate that memory.
*rc.borrow_mut() = 2;
}
let rc = RefCell::new(0);
break_it(&rc, rc.borrow())
}
fn ref_mut_protector() {
fn break_it(rc: &RefCell<i32>, r: RefMut<'_, i32>) {
// `r` has a shared reference, it is passed in as argument and hence
// a protector is added that marks this memory as inaccessible for the entire
// duration of this function
drop(r);
// *oops* here we can mutate that memory.
*rc.borrow_mut() = 2;
}
let rc = RefCell::new(0);
break_it(&rc, rc.borrow_mut())
}
/// Make sure we do not have bad enum layout optimizations.
fn rust_issue_68303() {
let optional = Some(RefCell::new(false));
let mut handle = optional.as_ref().unwrap().borrow_mut();
assert!(optional.is_some());
*handle = true;
}

View File

@ -1,77 +0,0 @@
use std::cell::{Ref, RefCell, RefMut};
fn main() {
basic();
ref_protector();
ref_mut_protector();
rust_issue_68303();
}
fn basic() {
let c = RefCell::new(42);
{
let s1 = c.borrow();
let _x: i32 = *s1;
let s2 = c.borrow();
let _x: i32 = *s1;
let _y: i32 = *s2;
let _x: i32 = *s1;
let _y: i32 = *s2;
}
{
let mut m = c.borrow_mut();
let _z: i32 = *m;
{
let s: &i32 = &*m;
let _x = *s;
}
*m = 23;
let _z: i32 = *m;
}
{
let s1 = c.borrow();
let _x: i32 = *s1;
let s2 = c.borrow();
let _x: i32 = *s1;
let _y: i32 = *s2;
let _x: i32 = *s1;
let _y: i32 = *s2;
}
}
// Adding a Stacked Borrows protector for `Ref` would break this
fn ref_protector() {
fn break_it(rc: &RefCell<i32>, r: Ref<'_, i32>) {
// `r` has a shared reference, it is passed in as argument and hence
// a protector is added that marks this memory as read-only for the entire
// duration of this function.
drop(r);
// *oops* here we can mutate that memory.
*rc.borrow_mut() = 2;
}
let rc = RefCell::new(0);
break_it(&rc, rc.borrow())
}
fn ref_mut_protector() {
fn break_it(rc: &RefCell<i32>, r: RefMut<'_, i32>) {
// `r` has a shared reference, it is passed in as argument and hence
// a protector is added that marks this memory as inaccessible for the entire
// duration of this function
drop(r);
// *oops* here we can mutate that memory.
*rc.borrow_mut() = 2;
}
let rc = RefCell::new(0);
break_it(&rc, rc.borrow_mut())
}
/// Make sure we do not have bad enum layout optimizations.
fn rust_issue_68303() {
let optional = Some(RefCell::new(false));
let mut handle = optional.as_ref().unwrap().borrow_mut();
assert!(optional.is_some());
*handle = true;
}

View File

@ -1,3 +1,4 @@
// compile-flags: -Zmiri-retag-fields
use std::ptr;
// Test various stacked-borrows-related things.