From 69272a81c403352e032c767a0deaf40b7b0e104c Mon Sep 17 00:00:00 2001 From: Neven Villani Date: Thu, 31 Aug 2023 20:14:33 +0200 Subject: [PATCH] Issue of the current model: spurious reads are not possible This occurs because in some interleavings, inserting a spurious read turns a Reserved into Frozen. We show here an exhaustive test (including arbitrary unknown code in two different threads) that makes this issue observable. --- src/borrow_tracker/tree_borrows/exhaustive.rs | 111 +++ src/borrow_tracker/tree_borrows/mod.rs | 10 +- src/borrow_tracker/tree_borrows/perms.rs | 165 +++-- src/borrow_tracker/tree_borrows/tree.rs | 115 +-- src/borrow_tracker/tree_borrows/tree/tests.rs | 658 ++++++++++++++++++ .../reserved/cell-protected-write.stderr | 4 +- 6 files changed, 917 insertions(+), 146 deletions(-) create mode 100644 src/borrow_tracker/tree_borrows/exhaustive.rs create mode 100644 src/borrow_tracker/tree_borrows/tree/tests.rs diff --git a/src/borrow_tracker/tree_borrows/exhaustive.rs b/src/borrow_tracker/tree_borrows/exhaustive.rs new file mode 100644 index 0000000000..daf3590358 --- /dev/null +++ b/src/borrow_tracker/tree_borrows/exhaustive.rs @@ -0,0 +1,111 @@ +//! Exhaustive testing utilities. +//! (These are used in Tree Borrows `#[test]`s for thorough verification +//! of the behavior of the state machine of permissions, +//! but the contents of this file are extremely generic) +#![cfg(test)] + +pub trait Exhaustive: Sized { + fn exhaustive() -> Box>; +} + +macro_rules! precondition { + ($cond:expr) => { + if !$cond { + continue; + } + }; +} +pub(crate) use precondition; + +// Trivial impls of `Exhaustive` for the standard types with 0, 1 and 2 elements respectively. + +impl Exhaustive for ! { + fn exhaustive() -> Box> { + Box::new(std::iter::empty()) + } +} + +impl Exhaustive for () { + fn exhaustive() -> Box> { + Box::new(std::iter::once(())) + } +} + +impl Exhaustive for bool { + fn exhaustive() -> Box> { + Box::new(vec![true, false].into_iter()) + } +} + +// Some container impls for `Exhaustive`. + +impl Exhaustive for Option +where + T: Exhaustive + 'static, +{ + fn exhaustive() -> Box> { + Box::new(std::iter::once(None).chain(T::exhaustive().map(Some))) + } +} + +impl Exhaustive for (T1, T2) +where + T1: Exhaustive + Clone + 'static, + T2: Exhaustive + 'static, +{ + fn exhaustive() -> Box> { + Box::new(T1::exhaustive().flat_map(|t1| T2::exhaustive().map(move |t2| (t1.clone(), t2)))) + } +} + +impl Exhaustive for [T; 1] +where + T: Exhaustive + 'static, +{ + fn exhaustive() -> Box> { + Box::new(T::exhaustive().map(|t| [t])) + } +} + +impl Exhaustive for [T; 2] +where + T: Exhaustive + Clone + 'static, +{ + fn exhaustive() -> Box> { + Box::new(T::exhaustive().flat_map(|t1| T::exhaustive().map(move |t2| [t1.clone(), t2]))) + } +} + +impl Exhaustive for [T; 3] +where + T: Exhaustive + Clone + 'static, +{ + fn exhaustive() -> Box> { + Box::new( + <[T; 2]>::exhaustive() + .flat_map(|[t1, t2]| T::exhaustive().map(move |t3| [t1.clone(), t2.clone(), t3])), + ) + } +} + +impl Exhaustive for [T; 4] +where + T: Exhaustive + Clone + 'static, +{ + fn exhaustive() -> Box> { + Box::new(<[T; 2]>::exhaustive().flat_map(|[t1, t2]| { + <[T; 2]>::exhaustive().map(move |[t3, t4]| [t1.clone(), t2.clone(), t3, t4]) + })) + } +} + +impl Exhaustive for [T; 5] +where + T: Exhaustive + Clone + 'static, +{ + fn exhaustive() -> Box> { + Box::new(<[T; 2]>::exhaustive().flat_map(|[t1, t2]| { + <[T; 3]>::exhaustive().map(move |[t3, t4, t5]| [t1.clone(), t2.clone(), t3, t4, t5]) + })) + } +} diff --git a/src/borrow_tracker/tree_borrows/mod.rs b/src/borrow_tracker/tree_borrows/mod.rs index 8dd925a0ad..924e0de38c 100644 --- a/src/borrow_tracker/tree_borrows/mod.rs +++ b/src/borrow_tracker/tree_borrows/mod.rs @@ -15,6 +15,10 @@ pub mod diagnostics; mod perms; mod tree; mod unimap; + +#[cfg(test)] +mod exhaustive; + use perms::Permission; pub use tree::Tree; @@ -271,6 +275,10 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<' diagnostics::AccessCause::Reborrow, )?; // Record the parent-child pair in the tree. + // FIXME: We should eventually ensure that the following `assert` holds, because + // some "exhaustive" tests consider only the initial configurations that satisfy it. + // The culprit is `Permission::new_active` in `tb_protect_place`. + //assert!(new_perm.initial_state.is_initial()); tree_borrows.new_child(orig_tag, new_tag, new_perm.initial_state, range, span)?; drop(tree_borrows); @@ -283,7 +291,7 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<' // interleaving, but wether UB happens can depend on whether a write occurs in the // future... let is_write = new_perm.initial_state.is_active() - || (new_perm.initial_state.is_reserved() && new_perm.protector.is_some()); + || (new_perm.initial_state.is_reserved(None) && new_perm.protector.is_some()); if is_write { // Need to get mutable access to alloc_extra. // (Cannot always do this as we can do read-only reborrowing on read-only allocations.) diff --git a/src/borrow_tracker/tree_borrows/perms.rs b/src/borrow_tracker/tree_borrows/perms.rs index dab216bc5b..16bad13e28 100644 --- a/src/borrow_tracker/tree_borrows/perms.rs +++ b/src/borrow_tracker/tree_borrows/perms.rs @@ -6,7 +6,7 @@ use crate::borrow_tracker::tree_borrows::tree::AccessRelatedness; use crate::borrow_tracker::AccessKind; /// The activation states of a pointer. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum PermissionPriv { /// represents: a local reference that has not yet been written to; /// allows: child reads, foreign reads, foreign writes if type is freeze; @@ -48,6 +48,13 @@ impl PartialOrd for PermissionPriv { } } +impl PermissionPriv { + /// Check if `self` can be the initial state of a pointer. + fn is_initial(&self) -> bool { + matches!(self, Reserved { ty_is_freeze: _ } | Frozen) + } +} + /// This module controls how each permission individually reacts to an access. /// Although these functions take `protected` as an argument, this is NOT because /// we check protector violations here, but because some permissions behave differently @@ -66,7 +73,6 @@ mod transition { /// A non-child node was read-accessed: noop on non-protected Reserved, advance to Frozen otherwise. fn foreign_read(state: PermissionPriv, protected: bool) -> Option { - use Option::*; Some(match state { // Non-writeable states just ignore foreign reads. non_writeable @ (Frozen | Disabled) => non_writeable, @@ -134,7 +140,7 @@ mod transition { /// Public interface to the state machine that controls read-write permissions. /// This is the "private `enum`" pattern. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)] pub struct Permission { inner: PermissionPriv, } @@ -147,6 +153,11 @@ pub struct PermTransition { } impl Permission { + /// Check if `self` can be the initial state of a pointer. + pub fn is_initial(&self) -> bool { + self.inner.is_initial() + } + /// Default initial permission of the root of a new tree. pub fn new_active() -> Self { Self { inner: Active } @@ -166,14 +177,24 @@ impl Permission { matches!(self.inner, Active) } - pub fn is_reserved(self) -> bool { - matches!(self.inner, Reserved { .. }) + // Leave `interior_mut` as `None` if interior mutability + // is irrelevant. + pub fn is_reserved(self, interior_mut: Option) -> bool { + match (interior_mut, self.inner) { + (None, Reserved { .. }) => true, + (Some(b1), Reserved { ty_is_freeze: b2 }) => b1 == b2, + _ => false, + } } pub fn is_frozen(self) -> bool { matches!(self.inner, Frozen) } + pub fn is_disabled(self) -> bool { + matches!(self.inner, Disabled) + } + /// Apply the transition to the inner PermissionPriv. pub fn perform_access( kind: AccessKind, @@ -229,7 +250,8 @@ pub mod diagnostics { f, "{}", match self { - Reserved { .. } => "Reserved", + Reserved { ty_is_freeze: true } => "Reserved", + Reserved { ty_is_freeze: false } => "Reserved (interior mutable)", Active => "Active", Frozen => "Frozen", Disabled => "Disabled", @@ -397,43 +419,35 @@ pub mod diagnostics { #[cfg(test)] mod propagation_optimization_checks { pub use super::*; - - mod util { - pub use super::*; - impl PermissionPriv { - /// Enumerate all states - pub fn all() -> impl Iterator { - vec![ - Active, - Reserved { ty_is_freeze: true }, - Reserved { ty_is_freeze: false }, - Frozen, - Disabled, - ] - .into_iter() - } + use crate::borrow_tracker::tree_borrows::exhaustive::{precondition, Exhaustive}; + + impl Exhaustive for PermissionPriv { + fn exhaustive() -> Box> { + Box::new( + vec![Active, Frozen, Disabled] + .into_iter() + .chain(bool::exhaustive().map(|ty_is_freeze| Reserved { ty_is_freeze })), + ) } + } - impl Permission { - pub fn all() -> impl Iterator { - PermissionPriv::all().map(|inner| Self { inner }) - } + impl Exhaustive for Permission { + fn exhaustive() -> Box> { + Box::new(PermissionPriv::exhaustive().map(|inner| Self { inner })) } + } - impl AccessKind { - /// Enumerate all AccessKind. - pub fn all() -> impl Iterator { - use AccessKind::*; - [Read, Write].into_iter() - } + impl Exhaustive for AccessKind { + fn exhaustive() -> Box> { + use AccessKind::*; + Box::new(vec![Read, Write].into_iter()) } + } - impl AccessRelatedness { - /// Enumerate all relative positions - pub fn all() -> impl Iterator { - use AccessRelatedness::*; - [This, StrictChildAccess, AncestorAccess, DistantAccess].into_iter() - } + impl Exhaustive for AccessRelatedness { + fn exhaustive() -> Box> { + use AccessRelatedness::*; + Box::new(vec![This, StrictChildAccess, AncestorAccess, DistantAccess].into_iter()) } } @@ -442,16 +456,22 @@ mod propagation_optimization_checks { // Even if the protector has disappeared. fn all_transitions_idempotent() { use transition::*; - for old in PermissionPriv::all() { - for (old_protected, new_protected) in [(true, true), (true, false), (false, false)] { - for access in AccessKind::all() { - for rel_pos in AccessRelatedness::all() { - if let Some(new) = perform_access(access, rel_pos, old, old_protected) { - assert_eq!( - new, - perform_access(access, rel_pos, new, new_protected).unwrap() - ); - } + for old in PermissionPriv::exhaustive() { + for (old_protected, new_protected) in <(bool, bool)>::exhaustive() { + // Protector can't appear out of nowhere: either the permission was + // created with a protector (`old_protected = true`) and it then may + // or may not lose it (`new_protected = false`, resp. `new_protected = true`), + // or it didn't have one upon creation and never will + // (`old_protected = new_protected = false`). + // We thus eliminate from this test and all other tests + // the case where the tag is initially unprotected and later becomes protected. + precondition!(old_protected || !new_protected); + for (access, rel_pos) in <(AccessKind, AccessRelatedness)>::exhaustive() { + if let Some(new) = perform_access(access, rel_pos, old, old_protected) { + assert_eq!( + new, + perform_access(access, rel_pos, new, new_protected).unwrap() + ); } } } @@ -459,13 +479,16 @@ mod propagation_optimization_checks { } #[test] + #[rustfmt::skip] fn foreign_read_is_noop_after_foreign_write() { use transition::*; let old_access = AccessKind::Write; let new_access = AccessKind::Read; - for old in PermissionPriv::all() { - for (old_protected, new_protected) in [(true, true), (true, false), (false, false)] { - for rel_pos in AccessRelatedness::all().filter(|rel| rel.is_foreign()) { + for old in PermissionPriv::exhaustive() { + for [old_protected, new_protected] in <[bool; 2]>::exhaustive() { + precondition!(old_protected || !new_protected); + for rel_pos in AccessRelatedness::exhaustive() { + precondition!(rel_pos.is_foreign()); if let Some(new) = perform_access(old_access, rel_pos, old, old_protected) { assert_eq!( new, @@ -480,18 +503,44 @@ mod propagation_optimization_checks { #[test] // Check that all transitions are consistent with the order on PermissionPriv, // i.e. Reserved -> Active -> Frozen -> Disabled - fn access_transitions_progress_increasing() { - use transition::*; - for old in PermissionPriv::all() { - for protected in [true, false] { - for access in AccessKind::all() { - for rel_pos in AccessRelatedness::all() { - if let Some(new) = perform_access(access, rel_pos, old, protected) { - assert!(old <= new); + fn permissionpriv_partialord_is_reachability() { + let reach = { + let mut reach = rustc_data_structures::fx::FxHashSet::default(); + // One-step transitions + reflexivity + for start in PermissionPriv::exhaustive() { + reach.insert((start, start)); + for (access, rel) in <(AccessKind, AccessRelatedness)>::exhaustive() { + for prot in bool::exhaustive() { + if let Some(end) = transition::perform_access(access, rel, start, prot) { + reach.insert((start, end)); } } } } + // Transitive closure + let mut finished = false; + while !finished { + finished = true; + for [start, mid, end] in <[PermissionPriv; 3]>::exhaustive() { + if reach.contains(&(start, mid)) + && reach.contains(&(mid, end)) + && !reach.contains(&(start, end)) + { + finished = false; + reach.insert((start, end)); + } + } + } + reach + }; + // Check that it matches `<` + for [p1, p2] in <[PermissionPriv; 2]>::exhaustive() { + let le12 = p1 <= p2; + let reach12 = reach.contains(&(p1, p2)); + assert!( + le12 == reach12, + "`{p1} reach {p2}` ({reach12}) does not match `{p1} <= {p2}` ({le12})" + ); } } } diff --git a/src/borrow_tracker/tree_borrows/tree.rs b/src/borrow_tracker/tree_borrows/tree.rs index 5abf13229b..eb466f89a8 100644 --- a/src/borrow_tracker/tree_borrows/tree.rs +++ b/src/borrow_tracker/tree_borrows/tree.rs @@ -10,6 +10,8 @@ //! and the relative position of the access; //! - idempotency properties asserted in `perms.rs` (for optimizations) +use std::fmt; + use smallvec::SmallVec; use rustc_const_eval::interpret::InterpResult; @@ -26,8 +28,10 @@ use crate::borrow_tracker::tree_borrows::{ use crate::borrow_tracker::{AccessKind, GlobalState, ProtectorKind}; use crate::*; +mod tests; + /// Data for a single *location*. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(super) struct LocationState { /// A location is initialized when it is child-accessed for the first time (and the initial /// retag initializes the location for the range covered by the type), and it then stays @@ -65,10 +69,25 @@ impl LocationState { self } + /// Check if the location has been initialized, i.e. if it has + /// ever been accessed through a child pointer. pub fn is_initialized(&self) -> bool { self.initialized } + /// Check if the state can exist as the initial permission of a pointer. + /// + /// Do not confuse with `is_initialized`, the two are almost orthogonal + /// as apart from `Active` which is not initial and must be initialized, + /// any other permission can have an arbitrary combination of being + /// initial/initialized. + /// FIXME: when the corresponding `assert` in `tree_borrows/mod.rs` finally + /// passes and can be uncommented, remove this `#[allow(dead_code)]`. + #[cfg_attr(not(test), allow(dead_code))] + pub fn is_initial(&self) -> bool { + self.permission.is_initial() + } + pub fn permission(&self) -> Permission { self.permission } @@ -172,6 +191,16 @@ impl LocationState { } } +impl fmt::Display for LocationState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.permission)?; + if !self.initialized { + write!(f, "?")?; + } + Ok(()) + } +} + /// Tree structure with both parents and children since we want to be /// able to traverse the tree efficiently in both directions. #[derive(Clone, Debug)] @@ -665,87 +694,3 @@ impl AccessRelatedness { } } } - -#[cfg(test)] -mod commutation_tests { - use super::*; - impl LocationState { - pub fn all() -> impl Iterator { - // We keep `latest_foreign_access` at `None` as that's just a cache. - Permission::all().flat_map(|permission| { - [false, true].into_iter().map(move |initialized| { - Self { permission, initialized, latest_foreign_access: None } - }) - }) - } - } - - #[test] - #[rustfmt::skip] - // Exhaustive check that for any starting configuration loc, - // for any two read accesses r1 and r2, if `loc + r1 + r2` is not UB - // and results in `loc'`, then `loc + r2 + r1` is also not UB and results - // in the same final state `loc'`. - // This lets us justify arbitrary read-read reorderings. - fn all_read_accesses_commute() { - let kind = AccessKind::Read; - // Two of the four combinations of `AccessRelatedness` are trivial, - // but we might as well check them all. - for rel1 in AccessRelatedness::all() { - for rel2 in AccessRelatedness::all() { - // Any protector state works, but we can't move reads across function boundaries - // so the two read accesses occur under the same protector. - for &protected in &[true, false] { - for loc in LocationState::all() { - // Apply 1 then 2. Failure here means that there is UB in the source - // and we skip the check in the target. - let mut loc12 = loc; - let Ok(_) = loc12.perform_access(kind, rel1, protected) else { continue }; - let Ok(_) = loc12.perform_access(kind, rel2, protected) else { continue }; - - // If 1 followed by 2 succeeded, then 2 followed by 1 must also succeed... - let mut loc21 = loc; - loc21.perform_access(kind, rel2, protected).unwrap(); - loc21.perform_access(kind, rel1, protected).unwrap(); - - // ... and produce the same final result. - assert_eq!( - loc12, loc21, - "Read accesses {:?} followed by {:?} do not commute !", - rel1, rel2 - ); - } - } - } - } - } - - #[test] - #[rustfmt::skip] - // Ensure that of 2 accesses happen, one foreign and one a child, and we are protected, that we - // get UB unless they are both reads. - fn protected_enforces_noalias() { - for rel1 in AccessRelatedness::all() { - for rel2 in AccessRelatedness::all() { - if rel1.is_foreign() == rel2.is_foreign() { - // We want to check pairs of accesses where one is foreign and one is not. - continue; - } - for kind1 in AccessKind::all() { - for kind2 in AccessKind::all() { - for mut state in LocationState::all() { - let protected = true; - let Ok(_) = state.perform_access(kind1, rel1, protected) else { continue }; - let Ok(_) = state.perform_access(kind2, rel2, protected) else { continue }; - // If these were both allowed, it must have been two reads. - assert!( - kind1 == AccessKind::Read && kind2 == AccessKind::Read, - "failed to enforce noalias between two accesses that are not both reads" - ); - } - } - } - } - } - } -} diff --git a/src/borrow_tracker/tree_borrows/tree/tests.rs b/src/borrow_tracker/tree_borrows/tree/tests.rs new file mode 100644 index 0000000000..11c06d0b76 --- /dev/null +++ b/src/borrow_tracker/tree_borrows/tree/tests.rs @@ -0,0 +1,658 @@ +//! Tests for the tree +#![cfg(test)] + +use super::*; +use crate::borrow_tracker::tree_borrows::exhaustive::{precondition, Exhaustive}; +use std::fmt; + +impl Exhaustive for LocationState { + fn exhaustive() -> Box> { + // We keep `latest_foreign_access` at `None` as that's just a cache. + Box::new(<(Permission, bool)>::exhaustive().map(|(permission, initialized)| { + Self { permission, initialized, latest_foreign_access: None } + })) + } +} + +#[test] +#[rustfmt::skip] +// Exhaustive check that for any starting configuration loc, +// for any two read accesses r1 and r2, if `loc + r1 + r2` is not UB +// and results in `loc'`, then `loc + r2 + r1` is also not UB and results +// in the same final state `loc'`. +// This lets us justify arbitrary read-read reorderings. +fn all_read_accesses_commute() { + let kind = AccessKind::Read; + // Two of the four combinations of `AccessRelatedness` are trivial, + // but we might as well check them all. + for [rel1, rel2] in <[AccessRelatedness; 2]>::exhaustive() { + // Any protector state works, but we can't move reads across function boundaries + // so the two read accesses occur under the same protector. + for protected in bool::exhaustive() { + for loc in LocationState::exhaustive() { + // Apply 1 then 2. Failure here means that there is UB in the source + // and we skip the check in the target. + let mut loc12 = loc; + precondition!(loc12.perform_access(kind, rel1, protected).is_ok()); + precondition!(loc12.perform_access(kind, rel2, protected).is_ok()); + + // If 1 followed by 2 succeeded, then 2 followed by 1 must also succeed... + let mut loc21 = loc; + loc21.perform_access(kind, rel2, protected).unwrap(); + loc21.perform_access(kind, rel1, protected).unwrap(); + + // ... and produce the same final result. + assert_eq!( + loc12, loc21, + "Read accesses {:?} followed by {:?} do not commute !", + rel1, rel2 + ); + } + } + } +} + +#[test] +#[rustfmt::skip] +// Ensure that of 2 accesses happen, one foreign and one a child, and we are protected, that we +// get UB unless they are both reads. +fn protected_enforces_noalias() { + for [rel1, rel2] in <[AccessRelatedness; 2]>::exhaustive() { + // We want to check pairs of accesses where one is foreign and one is not. + precondition!(rel1.is_foreign() != rel2.is_foreign()); + for [kind1, kind2] in <[AccessKind; 2]>::exhaustive() { + for mut state in LocationState::exhaustive() { + let protected = true; + precondition!(state.perform_access(kind1, rel1, protected).is_ok()); + precondition!(state.perform_access(kind2, rel2, protected).is_ok()); + // If these were both allowed, it must have been two reads. + assert!( + kind1 == AccessKind::Read && kind2 == AccessKind::Read, + "failed to enforce noalias between two accesses that are not both reads" + ); + } + } + } +} + +/// We are going to exhaustively test the possibily of inserting +/// a spurious read in some code. +/// +/// We choose some pointer `x` through which we want a spurious read to be inserted. +/// `x` must thus be reborrowed, not have any children, and initially start protected. +/// +/// To check if inserting a spurious read is possible, we observe the behavior +/// of some pointer `y` different from `x` (possibly from a different thread, thus +/// the protectors on `x` and `y` are not necessarily well-nested). +/// It must be the case that no matter the context, the insertion of a spurious read +/// through `x` does not introduce UB in code that did not already have UB. +/// +/// Testing this will need some setup to simulate the evolution of the permissions +/// of `x` and `y` under arbitrary code. This arbitrary code of course includes +/// read and write accesses through `x` and `y`, but it must also consider +/// the less obvious: +/// - accesses through pointers that are *neither* `x` nor `y`, +/// - retags of `y` that change its relative position to `x`. +/// +/// +/// The general code pattern thus looks like +/// [thread 1] || [thread 2] +/// || y exists +/// retag x (protect) || +/// arbitrary code +/// read/write x/y/other +/// or retag y +/// or unprotect y +/// || +/// arbitrary code +/// read/write x/y/other +/// or retag y +/// or unprotect y +/// or unprotect x +/// +/// `x` must still be protected at the moment the spurious read is inserted +/// because spurious reads are impossible in general on unprotected tags. +mod spurious_read { + use super::*; + + /// Accessed pointer. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum PtrSelector { + X, + Y, + Other, + } + + /// Relative position of `x` and `y`. + /// `y` cannot be a child of `x` because `x` gets retagged as the first + /// thing in the pattern, so it cannot have children. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + enum RelPosXY { + MutuallyForeign, + /// X is a child of Y. + XChildY, + } + + impl Exhaustive for PtrSelector { + fn exhaustive() -> Box> { + use PtrSelector::*; + Box::new(vec![X, Y, Other].into_iter()) + } + } + + impl fmt::Display for PtrSelector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PtrSelector::X => write!(f, "x"), + PtrSelector::Y => write!(f, "y"), + PtrSelector::Other => write!(f, "z"), + } + } + } + + impl Exhaustive for RelPosXY { + fn exhaustive() -> Box> { + use RelPosXY::*; + Box::new(vec![MutuallyForeign, XChildY].into_iter()) + } + } + + impl fmt::Display for RelPosXY { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RelPosXY::MutuallyForeign => write!(f, "x and y are mutually foreign"), + RelPosXY::XChildY => write!(f, "x is a child of y"), + } + } + } + + impl PtrSelector { + /// Knowing the relative position of `x` to `y`, determine the relative + /// position of the accessed pointer defined by `self` relative to each `x` + /// and `y`. + /// + /// The output is not necessarily well-defined in general, but it + /// is unique when considered up to equivalence by `AccessRelatedness::is_foreign` + /// (e.g. having `RelPosXY::XChildY` and `PtrSelector::Other`, strictly + /// speaking it is impossible to determine if `Other` is a `DistantAccess` + /// or an `AncestorAccess` relative to `y`, but it doesn't really matter + /// because `DistantAccess.is_foreign() == AncestorAccess.is_foreign()`). + fn rel_pair(self, xy_rel: RelPosXY) -> (AccessRelatedness, AccessRelatedness) { + use AccessRelatedness::*; + match xy_rel { + RelPosXY::MutuallyForeign => + match self { + PtrSelector::X => (This, DistantAccess), + PtrSelector::Y => (DistantAccess, This), + PtrSelector::Other => (DistantAccess, DistantAccess), + }, + RelPosXY::XChildY => + match self { + PtrSelector::X => (This, StrictChildAccess), + PtrSelector::Y => (AncestorAccess, This), + PtrSelector::Other => (DistantAccess, DistantAccess), + }, + } + } + } + + /// Arbitrary access parametrized by the relative position of `x` and `y` + /// to each other. + #[derive(Debug, Clone, Copy)] + struct TestAccess { + ptr: PtrSelector, + kind: AccessKind, + } + + impl Exhaustive for TestAccess { + fn exhaustive() -> Box> { + Box::new( + <(PtrSelector, AccessKind)>::exhaustive() + .map(|(ptr, kind)| TestAccess { ptr, kind }), + ) + } + } + + impl fmt::Display for TestAccess { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let kind_text = match self.kind { + AccessKind::Read => "read", + AccessKind::Write => "write", + }; + write!(f, "{kind_text} {}", self.ptr) + } + } + + type AllowRet = (); + type NoRet = !; + #[derive(Clone)] + /// Events relevant to the evolution of 2 pointers are + /// - any access to the same location + /// - end of one of them being protected + /// - a retag that would change their relative position + /// The type `TestEvent` models these kinds of events. + /// + /// In order to prevent `x` or `y` from losing their protector, + /// choose a type `RetX` or `RetY` that is not inhabited. + /// e.g. + /// - `TestEvent` is any event including end of protector on either `x` or `y` + /// - `TestEvent` is any access + /// - `TestEvent` allows for `y` to lose its protector but not `x` + enum TestEvent { + Access(TestAccess), + RetX(RetX), + RetY(RetY), + /// The inner `LocStateProt` must be an initial state (as per the `is_initial` function) + RetagY(LocStateProt), + } + + impl Exhaustive for TestEvent + where + RetX: Exhaustive + 'static, + RetY: Exhaustive + 'static, + { + fn exhaustive() -> Box> { + Box::new( + ::exhaustive() + .map(|acc| Self::Access(acc)) + .chain(RetX::exhaustive().map(|retx| Self::RetX(retx))) + .chain(RetY::exhaustive().map(|rety| Self::RetY(rety))) + .chain( + LocStateProt::exhaustive() + .filter_map(|s| s.is_initial().then_some(Self::RetagY(s))), + ), + ) + } + } + + impl fmt::Display for TestEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TestEvent::Access(acc) => write!(f, "{acc}"), + // The fields of the `Ret` variants just serve to make them + // impossible to instanciate via the `RetX = NoRet` type; we can + // always ignore their value. + TestEvent::RetX(_) => write!(f, "ret x"), + TestEvent::RetY(_) => write!(f, "ret y"), + TestEvent::RetagY(newp) => write!(f, "retag y ({newp})"), + } + } + } + + #[derive(Clone, PartialEq, Eq, Hash)] + /// The state of a pointer on a location, including the protector. + /// It is represented here with the protector bound to the `LocationState` rather + /// than the `Map` as is normally the case, + /// but since all our exhaustive tests look at a single location + /// there's no risk of `prot` for different locations of the same tag getting + /// out of sync. + struct LocStateProt { + state: LocationState, + prot: bool, + } + + impl LocStateProt { + fn is_initial(&self) -> bool { + self.state.is_initial() + } + + fn perform_access(&self, kind: AccessKind, rel: AccessRelatedness) -> Result { + let mut state = self.state; + state.perform_access(kind, rel, self.prot).map_err(|_| ())?; + Ok(Self { state, prot: self.prot }) + } + + fn end_protector(&self) -> Self { + Self { prot: false, state: self.state } + } + } + + impl Exhaustive for LocStateProt { + fn exhaustive() -> Box> { + Box::new( + <(LocationState, bool)>::exhaustive().map(|(state, prot)| Self { state, prot }), + ) + } + } + + impl fmt::Display for LocStateProt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.state)?; + if self.prot { + write!(f, ", protect")?; + } + Ok(()) + } + } + + #[derive(Clone, PartialEq, Eq, Hash)] + /// The states of two pointers to the same location, + /// and their relationship to each other in the tree. + /// + /// Note that the two states interact: using one pointer can have + /// an impact on the other. + /// This makes `LocStateProtPair` more meaningful than a simple + /// `(LocStateProt, LocStateProt)` where the two states are not guaranteed + /// to be updated at the same time. + /// Some `LocStateProtPair` may be unreachable through normal means + /// such as `x: Active, y: Active` in the case of mutually foreign pointers. + struct LocStateProtPair { + xy_rel: RelPosXY, + x: LocStateProt, + y: LocStateProt, + } + + impl LocStateProtPair { + fn perform_test_access(self, acc: &TestAccess) -> Result { + let (xrel, yrel) = acc.ptr.rel_pair(self.xy_rel); + let x = self.x.perform_access(acc.kind, xrel)?; + let y = self.y.perform_access(acc.kind, yrel)?; + Ok(Self { x, y, ..self }) + } + + /// Perform a read on the given pointer if its state is `initialized`. + /// Must be called just after reborrowing a pointer. + fn read_if_initialized(self, ptr: PtrSelector) -> Result { + let initialized = match ptr { + PtrSelector::X => self.x.state.initialized, + PtrSelector::Y => self.y.state.initialized, + PtrSelector::Other => + panic!( + "the `initialized` status of `PtrSelector::Other` is unknown, do not pass it to `read_if_initialized`" + ), + }; + if initialized { + self.perform_test_access(&TestAccess { ptr, kind: AccessKind::Read }) + } else { + Ok(self) + } + } + + fn end_protector_x(self) -> Result { + let x = self.x.end_protector(); + Ok(Self { x, ..self }) + } + + fn end_protector_y(self) -> Result { + let y = self.y.end_protector(); + Ok(Self { y, ..self }) + } + + fn retag_y(self, new_y: LocStateProt) -> Result { + assert!(new_y.is_initial()); + // `xy_rel` changes to "mutually foreign" now: `y` can no longer be a parent of `x`. + Self { y: new_y, xy_rel: RelPosXY::MutuallyForeign, ..self } + .read_if_initialized(PtrSelector::Y) + } + + fn perform_test_event(self, evt: &TestEvent) -> Result { + match evt { + TestEvent::Access(acc) => self.perform_test_access(acc), + // The fields of the `Ret` variants just serve to make them + // impossible to instanciate via the `RetX = NoRet` type; we can + // always ignore their value. + TestEvent::RetX(_) => self.end_protector_x(), + TestEvent::RetY(_) => self.end_protector_y(), + TestEvent::RetagY(newp) => self.retag_y(newp.clone()), + } + } + } + + impl Exhaustive for LocStateProtPair { + fn exhaustive() -> Box> { + Box::new(<[LocStateProt; 2]>::exhaustive().flat_map(|[x, y]| { + RelPosXY::exhaustive() + .map(move |xy_rel| Self { x: x.clone(), y: y.clone(), xy_rel }) + })) + } + } + + impl fmt::Display for LocStateProtPair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "x:{}, y:{}", self.x, self.y) + } + } + + /// Arbitrary sequence of events, as experienced by two mutually foreign pointers + /// to the same location. + #[derive(Clone)] + struct OpaqueCode { + events: Vec>, + } + + impl fmt::Display for OpaqueCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for evt in &self.events { + write!(f, "{evt}; ")?; + } + Ok(()) + } + } + + impl LocStateProtPair { + /// List all sequences of operations that start at `self` and do not cause UB + /// There are no duplicates: all sequences returned lead to distinct final states + /// (though the sequence is not guaranteed to be the shortest possible sequence of events). + /// Yields the states it reaches, and the sequence of operations that got us there. + fn all_states_reachable_via_opaque_code( + self, + ) -> impl Iterator)> + where + RetX: Exhaustive + Clone + 'static, + RetY: Exhaustive + Clone + 'static, + { + // We compute the reachable set of `Self` from `self` by non-UB `OpaqueCode`. + // Configurations are `(reach: Self, code: OpaqueCode)` tuples + // for which `code` applied to `self` returns `Ok(reach)`. + + // Stack of all configurations left to handle. + let mut handle: Vec<(Self, OpaqueCode<_, _>)> = + vec![(self, OpaqueCode { events: Vec::new() })]; + // Code that can be applied to `self`, and final state. + let mut paths: Vec<(Self, OpaqueCode<_, _>)> = Default::default(); + // Already explored states reachable from `self` + let mut seen: FxHashSet = Default::default(); + // This is essentially just computing the transitive closure by `perform_test_event`, + // most of the work lies in remembering the path up to the current state. + while let Some((state, path)) = handle.pop() { + for evt in >::exhaustive() { + if let Ok(next) = state.clone().perform_test_event(&evt) { + if seen.insert(next.clone()) { + let mut evts = path.clone(); + evts.events.push(evt); + paths.push((next.clone(), evts.clone())); + handle.push((next, evts)); + } + } + } + } + paths.into_iter() + } + } + + impl LocStateProtPair { + #[rustfmt::skip] + /// Two states (by convention `self` is the source and `other` is the target) + /// are "distinguishable" if there exists a sequence of instructions + /// that causes UB in the target but not in the source. + /// This implementation simply explores the reachable space + /// by all sequences of `TestEvent`. + /// This function can be instanciated with `RetX` and `RetY` + /// among `NoRet` or `AllowRet` to resp. forbid/allow `x`/`y` to lose their + /// protector. + fn distinguishable(&self, other: &Self) -> bool + where + RetX: Exhaustive + 'static, + RetY: Exhaustive + 'static, + { + if self == other { return false; } + let mut states = vec![(self.clone(), other.clone())]; + let mut seen = FxHashSet::default(); + while let Some(state) = states.pop() { + if !seen.insert(state.clone()) { continue; }; + let (source, target) = state; + for evt in >::exhaustive() { + // Generate successor states through events (accesses and protector ends) + let Ok(new_source) = source.clone().perform_test_event(&evt) else { continue; }; + let Ok(new_target) = target.clone().perform_test_event(&evt) else { return true; }; + if new_source == new_target { continue; } + states.push((new_source, new_target)); + } + } + false + } + } + + #[test] + #[should_panic] + // This is why `Reserved -> Frozen` on foreign read for protected references + // prevents the insertion of spurious reads: the transition can cause UB in the target + // later down the line. + fn reserved_frozen_protected_distinguishable() { + let source = LocStateProtPair { + xy_rel: RelPosXY::MutuallyForeign, + x: LocStateProt { + state: LocationState::new(Permission::new_frozen()).with_access(), + prot: true, + }, + y: LocStateProt { + state: LocationState::new(Permission::new_reserved(false)), + prot: true, + }, + }; + let acc = TestAccess { ptr: PtrSelector::X, kind: AccessKind::Read }; + let target = source.clone().perform_test_access(&acc).unwrap(); + assert!(source.y.state.permission.is_reserved(None)); + assert!(target.y.state.permission.is_frozen()); + assert!(!source.distinguishable::<(), ()>(&target)) + } + + #[derive(Clone, Debug)] + struct Pattern { + /// The relative position of `x` and `y` at the beginning of the arbitrary + /// code (i.e., just after `x` got created). + /// Might change during the execution if said arbitrary code contains any `retag y`. + xy_rel: RelPosXY, + /// Permission that `x` will be created as + /// (always protected until a possible `ret x` in the second phase). + /// This one should be initial (as per `is_initial`). + x_retag_perm: LocationState, + /// Permission that `y` has at the beginning of the pattern. + /// Can be any state, not necessarily initial + /// (since `y` exists already before the pattern starts). + /// This state might be reset during the execution if the opaque code + /// contains any `retag y`, but only to an initial state this time. + y_current_perm: LocationState, + /// Whether `y` starts with a protector. + /// Might change if the opaque code contains any `ret y`. + y_protected: bool, + } + + impl Exhaustive for Pattern { + fn exhaustive() -> Box> { + let mut v = Vec::new(); + for xy_rel in RelPosXY::exhaustive() { + for (x_retag_perm, y_current_perm) in <(LocationState, LocationState)>::exhaustive() + { + // We can only do spurious reads for initialized locations anyway. + precondition!(x_retag_perm.initialized); + // And `x` just got retagged, so it must be initial. + precondition!(x_retag_perm.permission.is_initial()); + for y_protected in bool::exhaustive() { + v.push(Pattern { xy_rel, x_retag_perm, y_current_perm, y_protected }); + } + } + } + Box::new(v.into_iter()) + } + } + + impl fmt::Display for Pattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (x, y) = self.retag_permissions(); + write!(f, "{}; ", self.xy_rel)?; + write!(f, "y: ({}); ", y,)?; + write!(f, "retag x ({}); ", x)?; + + write!(f, "; ;")?; + Ok(()) + } + } + + impl Pattern { + /// Return the permission that `y` starts as, and the permission that we + /// will retag `x` with. + /// This does not yet include a possible read-on-reborrow through `x`. + fn retag_permissions(&self) -> (LocStateProt, LocStateProt) { + let x = LocStateProt { state: self.x_retag_perm, prot: true }; + let y = LocStateProt { state: self.y_current_perm, prot: self.y_protected }; + (x, y) + } + + /// State that the pattern deterministically produces immediately after + /// the retag of `x`. + fn initial_state(&self) -> Result { + let (x, y) = self.retag_permissions(); + let state = LocStateProtPair { xy_rel: self.xy_rel, x, y }; + state.read_if_initialized(PtrSelector::X) + } + } + + #[test] + #[should_panic] + /// For each of the patterns described above, execute it once + /// as-is, and once with a spurious read inserted. Report any UB + /// in the target but not in the source. + fn test_all_patterns() { + let mut ok = 0; + let mut err = 0; + for pat in Pattern::exhaustive() { + let Ok(initial_source) = pat.initial_state() else { + // Failed to retag `x` in the source (e.g. `y` was protected Active) + continue; + }; + // `x` must stay protected, but the function protecting `y` might return here + for (final_source, opaque) in + initial_source.all_states_reachable_via_opaque_code::() + { + // Both executions are identical up to here. + // Now we do nothing in the source and in the target we do a spurious read. + // Then we check if the resulting states are distinguishable. + let distinguishable = { + assert!(final_source.x.prot); + let spurious_read = TestAccess { ptr: PtrSelector::X, kind: AccessKind::Read }; + if let Ok(final_target) = + final_source.clone().perform_test_access(&spurious_read) + { + // Only after the spurious read has been executed can `x` lose its + // protector. + final_source + .distinguishable::(&final_target) + .then_some(format!("{}", final_target)) + } else { + Some(format!("UB")) + } + }; + if let Some(final_target) = distinguishable { + eprintln!( + "For pattern '{}', inserting a spurious read through x makes the final state '{}' instead of '{}' which is observable", + pat, final_target, final_source + ); + eprintln!(" (arbitrary code instanciated with '{}')", opaque); + err += 1; + // We found an instanciation of the opaque code that makes this Pattern + // fail, we don't really need to check the rest. + break; + } + ok += 1; + } + } + if err > 0 { + panic!( + "Test failed after {}/{} patterns had UB in the target but not the source", + err, + ok + err + ) + } + } +} diff --git a/tests/fail/tree_borrows/reserved/cell-protected-write.stderr b/tests/fail/tree_borrows/reserved/cell-protected-write.stderr index 769769d957..998ab956e1 100644 --- a/tests/fail/tree_borrows/reserved/cell-protected-write.stderr +++ b/tests/fail/tree_borrows/reserved/cell-protected-write.stderr @@ -16,14 +16,14 @@ LL | *y = 1; | = help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental = help: the accessed tag (y, callee:y, caller:y) is foreign to the protected tag (callee:x) (i.e., it is not a child) - = help: this foreign write access would cause the protected tag (callee:x) (currently Reserved) to become Disabled + = help: this foreign write access would cause the protected tag (callee:x) (currently Reserved (interior mutable)) to become Disabled = help: protected tags must never be Disabled help: the accessed tag was created here --> $DIR/cell-protected-write.rs:LL:CC | LL | let y = (&mut *n).get(); | ^^^^^^^^^ -help: the protected tag was created here, in the initial state Reserved +help: the protected tag was created here, in the initial state Reserved (interior mutable) --> $DIR/cell-protected-write.rs:LL:CC | LL | unsafe fn write_second(x: &mut UnsafeCell, y: *mut u8) {