| use std::cmp::{Ordering, PartialOrd}; |
| use std::fmt; |
| |
| use crate::borrow_tracker::tree_borrows::diagnostics::TransitionError; |
| 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)] |
| enum PermissionPriv { |
| /// represents: a local reference that has not yet been written to; |
| /// allows: child reads, foreign reads, foreign writes if type is freeze; |
| /// rejects: child writes (Active), foreign writes (Disabled, except if type is not freeze). |
| /// special case: behaves differently when protected to adhere more closely to noalias |
| Reserved { ty_is_freeze: bool }, |
| /// represents: a unique pointer; |
| /// allows: child reads, child writes; |
| /// rejects: foreign reads (Frozen), foreign writes (Disabled). |
| Active, |
| /// represents: a shared pointer; |
| /// allows: all read accesses; |
| /// rejects child writes (UB), foreign writes (Disabled). |
| Frozen, |
| /// represents: a dead pointer; |
| /// allows: all foreign accesses; |
| /// rejects: all child accesses (UB). |
| Disabled, |
| } |
| use PermissionPriv::*; |
| |
| impl PartialOrd for PermissionPriv { |
| /// PermissionPriv is ordered as follows: |
| /// - Reserved(_) < Active < Frozen < Disabled; |
| /// - different kinds of `Reserved` (with or without interior mutability) |
| /// are incomparable to each other. |
| fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
| use Ordering::*; |
| Some(match (self, other) { |
| (a, b) if a == b => Equal, |
| (Disabled, _) => Greater, |
| (_, Disabled) => Less, |
| (Frozen, _) => Greater, |
| (_, Frozen) => Less, |
| (Active, _) => Greater, |
| (_, Active) => Less, |
| (Reserved { .. }, Reserved { .. }) => return None, |
| }) |
| } |
| } |
| |
| /// 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 |
| /// when protected. |
| mod transition { |
| use super::*; |
| /// A child node was read-accessed: UB on Disabled, noop on the rest. |
| fn child_read(state: PermissionPriv, _protected: bool) -> Option<PermissionPriv> { |
| Some(match state { |
| Disabled => return None, |
| // The inner data `ty_is_freeze` of `Reserved` is always irrelevant for Read |
| // accesses, since the data is not being mutated. Hence the `{ .. }` |
| readable @ (Reserved { .. } | Active | Frozen) => readable, |
| }) |
| } |
| |
| /// A non-child node was read-accessed: noop on non-protected Reserved, advance to Frozen otherwise. |
| fn foreign_read(state: PermissionPriv, protected: bool) -> Option<PermissionPriv> { |
| use Option::*; |
| Some(match state { |
| // The inner data `ty_is_freeze` of `Reserved` is always irrelevant for Read |
| // accesses, since the data is not being mutated. Hence the `{ .. }` |
| res @ Reserved { .. } if !protected => res, |
| Reserved { .. } => Frozen, // protected reserved |
| Active => |
| if protected { |
| Disabled |
| } else { |
| Frozen |
| }, |
| non_writeable @ (Frozen | Disabled) => non_writeable, |
| }) |
| } |
| |
| /// A child node was write-accessed: `Reserved` must become `Active` to obtain |
| /// write permissions, `Frozen` and `Disabled` cannot obtain such permissions and produce UB. |
| fn child_write(state: PermissionPriv, _protected: bool) -> Option<PermissionPriv> { |
| Some(match state { |
| // A write always activates the 2-phase borrow, even with interior |
| // mutability |
| Reserved { .. } | Active => Active, |
| Frozen | Disabled => return None, |
| }) |
| } |
| |
| /// A non-child node was write-accessed: this makes everything `Disabled` except for |
| /// non-protected interior mutable `Reserved` which stay the same. |
| fn foreign_write(state: PermissionPriv, protected: bool) -> Option<PermissionPriv> { |
| Some(match state { |
| cell @ Reserved { ty_is_freeze: false } if !protected => cell, |
| _ => Disabled, |
| }) |
| } |
| |
| /// Dispatch handler depending on the kind of access and its position. |
| pub(super) fn perform_access( |
| kind: AccessKind, |
| rel_pos: AccessRelatedness, |
| child: PermissionPriv, |
| protected: bool, |
| ) -> Option<PermissionPriv> { |
| match (kind, rel_pos.is_foreign()) { |
| (AccessKind::Write, true) => foreign_write(child, protected), |
| (AccessKind::Read, true) => foreign_read(child, protected), |
| (AccessKind::Write, false) => child_write(child, protected), |
| (AccessKind::Read, false) => child_read(child, protected), |
| } |
| } |
| } |
| |
| /// Public interface to the state machine that controls read-write permissions. |
| /// This is the "private `enum`" pattern. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub struct Permission { |
| inner: PermissionPriv, |
| } |
| |
| /// Transition from one permission to the next. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub struct PermTransition { |
| from: PermissionPriv, |
| to: PermissionPriv, |
| } |
| |
| impl Permission { |
| /// Default initial permission of the root of a new tree. |
| pub fn new_active() -> Self { |
| Self { inner: Active } |
| } |
| |
| /// Default initial permission of a reborrowed mutable reference. |
| pub fn new_reserved(ty_is_freeze: bool) -> Self { |
| Self { inner: Reserved { ty_is_freeze } } |
| } |
| |
| /// Default initial permission of a reborrowed shared reference |
| pub fn new_frozen() -> Self { |
| Self { inner: Frozen } |
| } |
| |
| pub fn is_active(self) -> bool { |
| matches!(self.inner, Active) |
| } |
| |
| pub fn is_reserved(self) -> bool { |
| matches!(self.inner, Reserved { .. }) |
| } |
| |
| pub fn is_frozen(self) -> bool { |
| matches!(self.inner, Frozen) |
| } |
| |
| /// Apply the transition to the inner PermissionPriv. |
| pub fn perform_access( |
| kind: AccessKind, |
| rel_pos: AccessRelatedness, |
| old_perm: Self, |
| protected: bool, |
| ) -> Option<PermTransition> { |
| let old_state = old_perm.inner; |
| transition::perform_access(kind, rel_pos, old_state, protected) |
| .map(|new_state| PermTransition { from: old_state, to: new_state }) |
| } |
| } |
| |
| impl PermTransition { |
| /// All transitions created through normal means (using `perform_access`) |
| /// should be possible, but the same is not guaranteed by construction of |
| /// transitions inferred by diagnostics. This checks that a transition |
| /// reconstructed by diagnostics is indeed one that could happen. |
| fn is_possible(self) -> bool { |
| self.from <= self.to |
| } |
| |
| pub fn from(from: Permission, to: Permission) -> Option<Self> { |
| let t = Self { from: from.inner, to: to.inner }; |
| t.is_possible().then_some(t) |
| } |
| |
| pub fn is_noop(self) -> bool { |
| self.from == self.to |
| } |
| |
| /// Extract result of a transition (checks that the starting point matches). |
| pub fn applied(self, starting_point: Permission) -> Option<Permission> { |
| (starting_point.inner == self.from).then_some(Permission { inner: self.to }) |
| } |
| |
| /// Extract starting point of a transition |
| pub fn started(self) -> Permission { |
| Permission { inner: self.from } |
| } |
| |
| /// Determines if this transition would disable the permission. |
| pub fn produces_disabled(self) -> bool { |
| self.to == Disabled |
| } |
| } |
| |
| pub mod diagnostics { |
| use super::*; |
| impl fmt::Display for PermissionPriv { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!( |
| f, |
| "{}", |
| match self { |
| Reserved { .. } => "Reserved", |
| Active => "Active", |
| Frozen => "Frozen", |
| Disabled => "Disabled", |
| } |
| ) |
| } |
| } |
| |
| impl fmt::Display for PermTransition { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "from {} to {}", self.from, self.to) |
| } |
| } |
| |
| impl fmt::Display for Permission { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "{}", self.inner) |
| } |
| } |
| |
| impl Permission { |
| /// Abbreviated name of the permission (uniformly 3 letters for nice alignment). |
| pub fn short_name(self) -> &'static str { |
| // Make sure there are all of the same length as each other |
| // and also as `diagnostics::DisplayFmtPermission.uninit` otherwise |
| // alignment will be incorrect. |
| match self.inner { |
| Reserved { ty_is_freeze: true } => "Res", |
| Reserved { ty_is_freeze: false } => "Re*", |
| Active => "Act", |
| Frozen => "Frz", |
| Disabled => "Dis", |
| } |
| } |
| } |
| |
| impl PermTransition { |
| /// Readable explanation of the consequences of an event. |
| /// Fits in the sentence "This accessed caused {trans.summary()}". |
| /// |
| /// Important: for the purposes of this explanation, `Reserved` is considered |
| /// to have write permissions, because that's what the diagnostics care about |
| /// (otherwise `Reserved -> Frozen` would be considered a noop). |
| pub fn summary(&self) -> &'static str { |
| assert!(self.is_possible()); |
| match (self.from, self.to) { |
| (_, Active) => "the first write to a 2-phase borrowed mutable reference", |
| (_, Frozen) => "a loss of write permissions", |
| (Frozen, Disabled) => "a loss of read permissions", |
| (_, Disabled) => "a loss of read and write permissions", |
| (old, new) => |
| unreachable!("Transition from {old:?} to {new:?} should never be possible"), |
| } |
| } |
| |
| /// Determines whether `self` is a relevant transition for the error `err`. |
| /// `self` will be a transition that happened to a tag some time before |
| /// that tag caused the error. |
| /// |
| /// Irrelevant events: |
| /// - modifications of write permissions when the error is related to read permissions |
| /// (on failed reads and protected `Frozen -> Disabled`, ignore `Reserved -> Active`, |
| /// `Reserved -> Frozen`, and `Active -> Frozen`) |
| /// - all transitions for attempts to deallocate strongly protected tags |
| /// |
| /// # Panics |
| /// |
| /// This function assumes that its arguments apply to the same location |
| /// and that they were obtained during a normal execution. It will panic otherwise. |
| /// - all transitions involved in `self` and `err` should be increasing |
| /// (Reserved < Active < Frozen < Disabled); |
| /// - between `self` and `err` the permission should also be increasing, |
| /// so all permissions inside `err` should be greater than `self.1`; |
| /// - `Active` and `Reserved` cannot cause an error due to insufficient permissions, |
| /// so `err` cannot be a `ChildAccessForbidden(_)` of either of them; |
| /// - `err` should not be `ProtectedDisabled(Disabled)`, because the protected |
| /// tag should not have been `Disabled` in the first place (if this occurs it means |
| /// we have unprotected tags that become protected) |
| pub(in super::super) fn is_relevant(&self, err: TransitionError) -> bool { |
| // NOTE: `super::super` is the visibility of `TransitionError` |
| assert!(self.is_possible()); |
| if self.is_noop() { |
| return false; |
| } |
| match err { |
| TransitionError::ChildAccessForbidden(insufficient) => { |
| // Show where the permission was gained then lost, |
| // but ignore unrelated permissions. |
| // This eliminates transitions like `Active -> Frozen` |
| // when the error is a failed `Read`. |
| match (self.to, insufficient.inner) { |
| (Frozen, Frozen) => true, |
| (Active, Frozen) => true, |
| (Disabled, Disabled) => true, |
| // A pointer being `Disabled` is a strictly stronger source of |
| // errors than it being `Frozen`. If we try to access a `Disabled`, |
| // then where it became `Frozen` (or `Active`) is the least of our concerns for now. |
| (Active | Frozen, Disabled) => false, |
| |
| // `Active` and `Reserved` have all permissions, so a |
| // `ChildAccessForbidden(Reserved | Active)` can never exist. |
| (_, Active) | (_, Reserved { .. }) => |
| unreachable!("this permission cannot cause an error"), |
| // No transition has `Reserved` as its `.to` unless it's a noop. |
| (Reserved { .. }, _) => unreachable!("self is a noop transition"), |
| // All transitions produced in normal executions (using `apply_access`) |
| // change permissions in the order `Reserved -> Active -> Frozen -> Disabled`. |
| // We assume that the error was triggered on the same location that |
| // the transition `self` applies to, so permissions found must be increasing |
| // in the order `self.from < self.to <= insufficient.inner` |
| (Disabled, Frozen) => |
| unreachable!("permissions between self and err must be increasing"), |
| } |
| } |
| TransitionError::ProtectedDisabled(before_disabled) => { |
| // Show how we got to the starting point of the forbidden transition, |
| // but ignore what came before. |
| // This eliminates transitions like `Reserved -> Active` |
| // when the error is a `Frozen -> Disabled`. |
| match (self.to, before_disabled.inner) { |
| // We absolutely want to know where it was activated. |
| (Active, Active) => true, |
| // And knowing where it became Frozen is also important. |
| (Frozen, Frozen) => true, |
| // If the error is a transition `Frozen -> Disabled`, then we don't really |
| // care whether before that was `Reserved -> Active -> Frozen` or |
| // `Reserved -> Frozen` or even `Frozen` directly. |
| // The error will only show either |
| // - created as Frozen, then Frozen -> Disabled is forbidden |
| // - created as Reserved, later became Frozen, then Frozen -> Disabled is forbidden |
| // In both cases the `Reserved -> Active` part is inexistant or irrelevant. |
| (Active, Frozen) => false, |
| |
| (_, Disabled) => |
| unreachable!( |
| "permission that results in Disabled should not itself be Disabled in the first place" |
| ), |
| // No transition has `Reserved` as its `.to` unless it's a noop. |
| (Reserved { .. }, _) => unreachable!("self is a noop transition"), |
| |
| // Permissions only evolve in the order `Reserved -> Active -> Frozen -> Disabled`, |
| // so permissions found must be increasing in the order |
| // `self.from < self.to <= forbidden.from < forbidden.to`. |
| (Disabled, Reserved { .. } | Active | Frozen) |
| | (Frozen, Reserved { .. } | Active) |
| | (Active, Reserved { .. }) => |
| unreachable!("permissions between self and err must be increasing"), |
| } |
| } |
| // We don't care because protectors evolve independently from |
| // permissions. |
| TransitionError::ProtectedDealloc => false, |
| } |
| } |
| |
| /// Endpoint of a transition. |
| /// Meant only for diagnostics, use `applied` in non-diagnostics |
| /// code, which also checks that the starting point matches the current state. |
| pub fn endpoint(&self) -> Permission { |
| Permission { inner: self.to } |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod propagation_optimization_checks { |
| pub use super::*; |
| |
| mod util { |
| pub use super::*; |
| impl PermissionPriv { |
| /// Enumerate all states |
| pub fn all() -> impl Iterator<Item = Self> { |
| vec![ |
| Active, |
| Reserved { ty_is_freeze: true }, |
| Reserved { ty_is_freeze: false }, |
| Frozen, |
| Disabled, |
| ] |
| .into_iter() |
| } |
| } |
| |
| impl Permission { |
| pub fn all() -> impl Iterator<Item = Self> { |
| PermissionPriv::all().map(|inner| Self { inner }) |
| } |
| } |
| |
| impl AccessKind { |
| /// Enumerate all AccessKind. |
| pub fn all() -> impl Iterator<Item = Self> { |
| use AccessKind::*; |
| [Read, Write].into_iter() |
| } |
| } |
| |
| impl AccessRelatedness { |
| /// Enumerate all relative positions |
| pub fn all() -> impl Iterator<Item = Self> { |
| use AccessRelatedness::*; |
| [This, StrictChildAccess, AncestorAccess, DistantAccess].into_iter() |
| } |
| } |
| } |
| |
| #[test] |
| // For any kind of access, if we do it twice the second should be a no-op. |
| // 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() |
| ); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| #[test] |
| 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()) { |
| if let Some(new) = perform_access(old_access, rel_pos, old, old_protected) { |
| assert_eq!( |
| new, |
| perform_access(new_access, rel_pos, new, new_protected).unwrap() |
| ); |
| } |
| } |
| } |
| } |
| } |
| |
| #[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); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |