blob: 2c6be522837cb3572adb806b51dcb9ea608a9ce9 [file] [log] [blame] [edit]
//! In this file we handle the "Tree" part of Tree Borrows, i.e. all tree
//! traversal functions, optimizations to trim branches, and keeping track of
//! the relative position of the access to each node being updated. This of course
//! also includes the definition of the tree structure.
//!
//! Functions here manipulate permissions but are oblivious to them: as
//! the internals of `Permission` are private, the update process is a black
//! box. All we need to know here are
//! - the fact that updates depend only on the old state, the status of protectors,
//! and the relative position of the access;
//! - idempotency properties asserted in `perms.rs` (for optimizations)
use std::ops::Range;
use std::{cmp, fmt, mem};
use rustc_abi::Size;
use rustc_data_structures::fx::FxHashSet;
use rustc_span::Span;
use smallvec::SmallVec;
use super::diagnostics::{
AccessCause, DiagnosticInfo, NodeDebugInfo, TbError, TransitionError,
no_valid_exposed_references_error,
};
use super::foreign_access_skipping::IdempotentForeignAccess;
use super::perms::{PermTransition, Permission};
use super::tree_visitor::{ChildrenVisitMode, ContinueTraversal, NodeAppArgs, TreeVisitor};
use super::unimap::{UniIndex, UniKeyMap, UniValMap};
use super::wildcard::WildcardState;
use crate::borrow_tracker::{AccessKind, GlobalState, ProtectorKind};
use crate::*;
mod tests;
/// Data for a reference at single *location*.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(super) struct LocationState {
/// A location is "accessed" 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
/// accessed forever.
/// For accessed locations, "permission" is the current permission. However, for
/// non-accessed locations, we still need to track the "future initial permission": this will
/// start out to be `default_initial_perm`, but foreign accesses need to be taken into account.
/// Crucially however, while transitions to `Disabled` would usually be UB if this location is
/// protected, that is *not* the case for non-accessed locations. Instead we just have a latent
/// "future initial permission" of `Disabled`, causing UB only if an access is ever actually
/// performed.
/// Note that the tree root is also always accessed, as if the allocation was a write access.
accessed: bool,
/// This pointer's current permission / future initial permission.
permission: Permission,
/// See `foreign_access_skipping.rs`.
/// Stores an idempotent foreign access for this location and its children.
/// For correctness, this must not be too strong, and the recorded idempotent foreign access
/// of all children must be at least as strong as this. For performance, it should be as strong as possible.
idempotent_foreign_access: IdempotentForeignAccess,
}
impl LocationState {
/// Constructs a new initial state. It has neither been accessed, nor been subjected
/// to any foreign access yet.
/// The permission is not allowed to be `Unique`.
/// `sifa` is the (strongest) idempotent foreign access, see `foreign_access_skipping.rs`
pub fn new_non_accessed(permission: Permission, sifa: IdempotentForeignAccess) -> Self {
assert!(permission.is_initial() || permission.is_disabled());
Self { permission, accessed: false, idempotent_foreign_access: sifa }
}
/// Constructs a new initial state. It has not yet been subjected
/// to any foreign access. However, it is already marked as having been accessed.
/// `sifa` is the (strongest) idempotent foreign access, see `foreign_access_skipping.rs`
pub fn new_accessed(permission: Permission, sifa: IdempotentForeignAccess) -> Self {
Self { permission, accessed: true, idempotent_foreign_access: sifa }
}
/// Check if the location has been accessed, i.e. if it has
/// ever been accessed through a child pointer.
pub fn accessed(&self) -> bool {
self.accessed
}
pub fn permission(&self) -> Permission {
self.permission
}
/// Performs an access on this index and updates node,
/// perm and wildcard_state to reflect the transition.
fn perform_transition(
&mut self,
idx: UniIndex,
nodes: &mut UniValMap<Node>,
wildcard_accesses: &mut UniValMap<WildcardState>,
access_kind: AccessKind,
relatedness: AccessRelatedness,
protected: bool,
diagnostics: &DiagnosticInfo,
) -> Result<(), TransitionError> {
// Call this function now (i.e. only if we know `relatedness`), which
// ensures it is only called when `skip_if_known_noop` returns
// `Recurse`, due to the contract of `traverse_this_parents_children_other`.
self.record_new_access(access_kind, relatedness);
let transition = self.perform_access(access_kind, relatedness, protected)?;
if !transition.is_noop() {
let node = nodes.get_mut(idx).unwrap();
// Record the event as part of the history.
node.debug_info
.history
.push(diagnostics.create_event(transition, relatedness.is_foreign()));
// We need to update the wildcard state, if the permission
// of an exposed pointer changes.
if node.is_exposed {
let access_type = self.permission.strongest_allowed_local_access(protected);
WildcardState::update_exposure(idx, access_type, nodes, wildcard_accesses);
}
}
Ok(())
}
/// Apply the effect of an access to one location, including
/// - applying `Permission::perform_access` to the inner `Permission`,
/// - emitting protector UB if the location is accessed,
/// - updating the accessed status (child accesses produce accessed locations).
fn perform_access(
&mut self,
access_kind: AccessKind,
rel_pos: AccessRelatedness,
protected: bool,
) -> Result<PermTransition, TransitionError> {
let old_perm = self.permission;
let transition = Permission::perform_access(access_kind, rel_pos, old_perm, protected)
.ok_or(TransitionError::ChildAccessForbidden(old_perm))?;
self.accessed |= !rel_pos.is_foreign();
self.permission = transition.applied(old_perm).unwrap();
// Why do only accessed locations cause protector errors?
// Consider two mutable references `x`, `y` into disjoint parts of
// the same allocation. A priori, these may actually both be used to
// access the entire allocation, as long as only reads occur. However,
// a write to `y` needs to somehow record that `x` can no longer be used
// on that location at all. For these non-accessed locations (i.e., locations
// that haven't been accessed with `x` yet), we track the "future initial state":
// it defaults to whatever the initial state of the tag is,
// but the access to `y` moves that "future initial state" of `x` to `Disabled`.
// However, usually a `Reserved -> Disabled` transition would be UB due to the protector!
// So clearly protectors shouldn't fire for such "future initial state" transitions.
//
// See the test `two_mut_protected_same_alloc` in `tests/pass/tree_borrows/tree-borrows.rs`
// for an example of safe code that would be UB if we forgot to check `self.accessed`.
if protected && self.accessed && transition.produces_disabled() {
return Err(TransitionError::ProtectedDisabled(old_perm));
}
Ok(transition)
}
/// Like `perform_access`, but ignores the concrete error cause and also uses state-passing
/// rather than a mutable reference. As such, it returns `Some(x)` if the transition succeeded,
/// or `None` if there was an error.
#[cfg(test)]
fn perform_access_no_fluff(
mut self,
access_kind: AccessKind,
rel_pos: AccessRelatedness,
protected: bool,
) -> Option<Self> {
match self.perform_access(access_kind, rel_pos, protected) {
Ok(_) => Some(self),
Err(_) => None,
}
}
/// Tree traversal optimizations. See `foreign_access_skipping.rs`.
/// This checks if such a foreign access can be skipped.
fn skip_if_known_noop(
&self,
access_kind: AccessKind,
rel_pos: AccessRelatedness,
) -> ContinueTraversal {
if rel_pos.is_foreign() {
let happening_now = IdempotentForeignAccess::from_foreign(access_kind);
let mut new_access_noop =
self.idempotent_foreign_access.can_skip_foreign_access(happening_now);
if self.permission.is_disabled() {
// A foreign access to a `Disabled` tag will have almost no observable effect.
// It's a theorem that `Disabled` node have no protected accessed children,
// and so this foreign access will never trigger any protector.
// (Intuition: You're either protected accessed, and thus can't become Disabled
// or you're already Disabled protected, but not accessed, and then can't
// become accessed since that requires a child access, which Disabled blocks.)
// Further, the children will never be able to read or write again, since they
// have a `Disabled` parent. So this only affects diagnostics, such that the
// blocking write will still be identified directly, just at a different tag.
new_access_noop = true;
}
if self.permission.is_frozen() && access_kind == AccessKind::Read {
// A foreign read to a `Frozen` tag will have almost no observable effect.
// It's a theorem that `Frozen` nodes have no `Unique` children, so all children
// already survive foreign reads. Foreign reads in general have almost no
// effect, the only further thing they could do is make protected `Reserved`
// nodes become conflicted, i.e. make them reject child writes for the further
// duration of their protector. But such a child write is already rejected
// because this node is frozen. So this only affects diagnostics, but the
// blocking read will still be identified directly, just at a different tag.
new_access_noop = true;
}
if new_access_noop {
// Abort traversal if the new access is indeed guaranteed
// to be noop.
// No need to update `self.idempotent_foreign_access`,
// the type of the current streak among nonempty read-only
// or nonempty with at least one write has not changed.
ContinueTraversal::SkipSelfAndChildren
} else {
// Otherwise propagate this time, and also record the
// access that just occurred so that we can skip the propagation
// next time.
ContinueTraversal::Recurse
}
} else {
// A child access occurred, this breaks the streak of foreign
// accesses in a row and the sequence since the previous child access
// is now empty.
ContinueTraversal::Recurse
}
}
/// Records a new access, so that future access can potentially be skipped
/// by `skip_if_known_noop`. This must be called on child accesses, and otherwise
/// shoud be called on foreign accesses for increased performance. It should not be called
/// when `skip_if_known_noop` indicated skipping, since it then is a no-op.
/// See `foreign_access_skipping.rs`
fn record_new_access(&mut self, access_kind: AccessKind, rel_pos: AccessRelatedness) {
debug_assert!(matches!(
self.skip_if_known_noop(access_kind, rel_pos),
ContinueTraversal::Recurse
));
self.idempotent_foreign_access
.record_new(IdempotentForeignAccess::from_acc_and_rel(access_kind, rel_pos));
}
}
impl fmt::Display for LocationState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.permission)?;
if !self.accessed {
write!(f, "?")?;
}
Ok(())
}
}
/// The state of the full tree for a particular location: for all nodes, the local permissions
/// of that node, and the tracking for wildcard accesses.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LocationTree {
/// Maps a tag to a perm, with possible lazy initialization.
///
/// NOTE: not all tags registered in `Tree::nodes` are necessarily in all
/// ranges of `perms`, because `perms` is in part lazily initialized.
/// Just because `nodes.get(key)` is `Some(_)` does not mean you can safely
/// `unwrap` any `perm.get(key)`.
///
/// We do uphold the fact that `keys(perms)` is a subset of `keys(nodes)`
pub perms: UniValMap<LocationState>,
/// Maps a tag and a location to its wildcard access tracking information,
/// with possible lazy initialization.
///
/// If this allocation doesn't have any exposed nodes, then this map doesn't get
/// initialized. This way we only need to allocate the map if we need it.
///
/// NOTE: same guarantees on entry initialization as for `perms`.
pub wildcard_accesses: UniValMap<WildcardState>,
}
/// Tree structure with both parents and children since we want to be
/// able to traverse the tree efficiently in both directions.
#[derive(Clone, Debug)]
pub struct Tree {
/// Mapping from tags to keys. The key obtained can then be used in
/// any of the `UniValMap` relative to this allocation, i.e.
/// `nodes`, `LocationTree::perms` and `LocationTree::wildcard_accesses`
/// of the same `Tree`.
/// The parent-child relationship in `Node` is encoded in terms of these same
/// keys, so traversing the entire tree needs exactly one access to
/// `tag_mapping`.
pub(super) tag_mapping: UniKeyMap<BorTag>,
/// All nodes of this tree.
pub(super) nodes: UniValMap<Node>,
/// Associates with each location its state and wildcard access tracking.
pub(super) locations: DedupRangeMap<LocationTree>,
/// Contains both the root of the main tree as well as the roots of the wildcard subtrees.
///
/// If we reborrow a reference which has wildcard provenance, then we do not know where in
/// the tree to attach them. Instead we create a new additional tree for this allocation
/// with this new reference as a root. We call this additional tree a wildcard subtree.
///
/// The actual structure should be a single tree but with wildcard provenance we approximate
/// this with this ordered set of trees. Each wildcard subtree is the direct child of *some* exposed
/// tag (that is smaller than the root), but we do not know which. This also means that it can only be the
/// child of a tree that comes before it in the vec ensuring we don't have any cycles in our
/// approximated tree.
///
/// Sorted according to `BorTag` from low to high. This also means the main root is `root[0]`.
///
/// Has array size 2 because that still ensures the minimum size for SmallVec.
pub(super) roots: SmallVec<[UniIndex; 2]>,
}
/// A node in the borrow tree. Each node is uniquely identified by a tag via
/// the `nodes` map of `Tree`.
#[derive(Clone, Debug)]
pub(super) struct Node {
/// The tag of this node.
pub tag: BorTag,
/// All tags except the root have a parent tag.
pub parent: Option<UniIndex>,
/// If the pointer was reborrowed, it has children.
// FIXME: bench to compare this to FxHashSet and to other SmallVec sizes
pub children: SmallVec<[UniIndex; 4]>,
/// Either `Reserved`, `Frozen`, or `Disabled`, it is the permission this tag will
/// lazily be initialized to on the first access.
/// It is only ever `Disabled` for a tree root, since the root is initialized to `Unique` by
/// its own separate mechanism.
default_initial_perm: Permission,
/// The default initial (strongest) idempotent foreign access.
/// This participates in the invariant for `LocationState::idempotent_foreign_access`
/// in cases where there is no location state yet. See `foreign_access_skipping.rs`,
/// and `LocationState::idempotent_foreign_access` for more information
default_initial_idempotent_foreign_access: IdempotentForeignAccess,
/// Whether a wildcard access could happen through this node.
pub is_exposed: bool,
/// Some extra information useful only for debugging purposes.
pub debug_info: NodeDebugInfo,
}
impl Tree {
/// Create a new tree, with only a root pointer.
pub fn new(root_tag: BorTag, size: Size, span: Span) -> Self {
// The root has `Disabled` as the default permission,
// so that any access out of bounds is invalid.
let root_default_perm = Permission::new_disabled();
let mut tag_mapping = UniKeyMap::default();
let root_idx = tag_mapping.insert(root_tag);
let nodes = {
let mut nodes = UniValMap::<Node>::default();
let mut debug_info = NodeDebugInfo::new(root_tag, root_default_perm, span);
// name the root so that all allocations contain one named pointer
debug_info.add_name("root of the allocation");
nodes.insert(
root_idx,
Node {
tag: root_tag,
parent: None,
children: SmallVec::default(),
default_initial_perm: root_default_perm,
// The root may never be skipped, all accesses will be local.
default_initial_idempotent_foreign_access: IdempotentForeignAccess::None,
is_exposed: false,
debug_info,
},
);
nodes
};
let locations = {
let mut perms = UniValMap::default();
// We manually set it to `Unique` on all in-bounds positions.
// We also ensure that it is accessed, so that no `Unique` but
// not yet accessed nodes exist. Essentially, we pretend there
// was a write that initialized these to `Unique`.
perms.insert(
root_idx,
LocationState::new_accessed(
Permission::new_unique(),
IdempotentForeignAccess::None,
),
);
let wildcard_accesses = UniValMap::default();
DedupRangeMap::new(size, LocationTree { perms, wildcard_accesses })
};
Self { roots: SmallVec::from_slice(&[root_idx]), nodes, locations, tag_mapping }
}
}
impl<'tcx> Tree {
/// Insert a new tag in the tree.
///
/// `inside_perm` defines the initial permissions for a block of memory starting at
/// `base_offset`. These may nor may not be already marked as "accessed".
/// `outside_perm` defines the initial permission for the rest of the allocation.
/// These are definitely not "accessed".
pub(super) fn new_child(
&mut self,
base_offset: Size,
parent_prov: ProvenanceExtra,
new_tag: BorTag,
inside_perms: DedupRangeMap<LocationState>,
outside_perm: Permission,
protected: bool,
span: Span,
) -> InterpResult<'tcx> {
let idx = self.tag_mapping.insert(new_tag);
let parent_idx = match parent_prov {
ProvenanceExtra::Concrete(parent_tag) =>
Some(self.tag_mapping.get(&parent_tag).unwrap()),
ProvenanceExtra::Wildcard => None,
};
assert!(outside_perm.is_initial());
let default_strongest_idempotent =
outside_perm.strongest_idempotent_foreign_access(protected);
// Create the node
self.nodes.insert(
idx,
Node {
tag: new_tag,
parent: parent_idx,
children: SmallVec::default(),
default_initial_perm: outside_perm,
default_initial_idempotent_foreign_access: default_strongest_idempotent,
is_exposed: false,
debug_info: NodeDebugInfo::new(new_tag, outside_perm, span),
},
);
if let Some(parent_idx) = parent_idx {
let parent_node = self.nodes.get_mut(parent_idx).unwrap();
// Register new_tag as a child of parent_tag
parent_node.children.push(idx);
} else {
// If the parent had wildcard provenance, then register the idx
// as a new wildcard root.
// This preserves the orderedness of `roots` because a newly created
// tag is greater than all previous tags.
self.roots.push(idx);
}
// We need to know the weakest SIFA for `update_idempotent_foreign_access_after_retag`.
let mut min_sifa = default_strongest_idempotent;
for (Range { start, end }, &perm) in
inside_perms.iter(Size::from_bytes(0), inside_perms.size())
{
assert!(perm.permission.is_initial());
assert_eq!(
perm.idempotent_foreign_access,
perm.permission.strongest_idempotent_foreign_access(protected)
);
min_sifa = cmp::min(min_sifa, perm.idempotent_foreign_access);
for (_range, loc) in self
.locations
.iter_mut(Size::from_bytes(start) + base_offset, Size::from_bytes(end - start))
{
loc.perms.insert(idx, perm);
}
}
// We need to ensure the consistency of the wildcard access tracking data structure.
// For this, we insert the correct entry for this tag based on its parent, if it exists.
// If we are inserting a new wildcard root (with Wildcard as parent_prov) then we insert
// the special wildcard root initial state instead.
for (_range, loc) in self.locations.iter_mut_all() {
if let Some(parent_idx) = parent_idx {
if let Some(parent_access) = loc.wildcard_accesses.get(parent_idx) {
loc.wildcard_accesses.insert(idx, parent_access.for_new_child());
}
} else {
loc.wildcard_accesses.insert(idx, WildcardState::for_wildcard_root());
}
}
// If the parent is a wildcard pointer, then it doesn't track SIFA and doesn't need to be updated.
if let Some(parent_idx) = parent_idx {
// Inserting the new perms might have broken the SIFA invariant (see
// `foreign_access_skipping.rs`) if the SIFA we inserted is weaker than that of some parent.
// We now weaken the recorded SIFA for our parents, until the invariant is restored. We
// could weaken them all to `None`, but it is more efficient to compute the SIFA for the new
// permission statically, and use that. For this we need the *minimum* SIFA (`None` needs
// more fixup than `Write`).
self.update_idempotent_foreign_access_after_retag(parent_idx, min_sifa);
}
interp_ok(())
}
/// Restores the SIFA "children are stronger"/"parents are weaker" invariant after a retag:
/// reduce the SIFA of `current` and its parents to be no stronger than `strongest_allowed`.
/// See `foreign_access_skipping.rs` and [`Tree::new_child`].
fn update_idempotent_foreign_access_after_retag(
&mut self,
mut current: UniIndex,
strongest_allowed: IdempotentForeignAccess,
) {
if strongest_allowed == IdempotentForeignAccess::Write {
// Nothing is stronger than `Write`.
return;
}
// We walk the tree upwards, until the invariant is restored
loop {
let current_node = self.nodes.get_mut(current).unwrap();
// Call `ensure_no_stronger_than` on all SIFAs for this node: the per-location SIFA, as well
// as the default SIFA for not-yet-initialized locations.
// Record whether we did any change; if not, the invariant is restored and we can stop the traversal.
let mut any_change = false;
for (_range, loc) in self.locations.iter_mut_all() {
// Check if this node has a state for this location (or range of locations).
if let Some(perm) = loc.perms.get_mut(current) {
// Update the per-location SIFA, recording if it changed.
any_change |=
perm.idempotent_foreign_access.ensure_no_stronger_than(strongest_allowed);
}
}
// Now update `default_initial_idempotent_foreign_access`, which stores the default SIFA for not-yet-initialized locations.
any_change |= current_node
.default_initial_idempotent_foreign_access
.ensure_no_stronger_than(strongest_allowed);
if any_change {
let Some(next) = self.nodes.get(current).unwrap().parent else {
// We have arrived at the root.
break;
};
current = next;
continue;
} else {
break;
}
}
}
/// Deallocation requires
/// - a pointer that permits write accesses
/// - the absence of Strong Protectors anywhere in the allocation
pub fn dealloc(
&mut self,
prov: ProvenanceExtra,
access_range: AllocRange,
global: &GlobalState,
alloc_id: AllocId, // diagnostics
span: Span, // diagnostics
) -> InterpResult<'tcx> {
self.perform_access(
prov,
access_range,
AccessKind::Write,
AccessCause::Dealloc,
global,
alloc_id,
span,
)?;
let start_idx = match prov {
ProvenanceExtra::Concrete(tag) => Some(self.tag_mapping.get(&tag).unwrap()),
ProvenanceExtra::Wildcard => None,
};
// Check if this breaks any strong protector.
// (Weak protectors are already handled by `perform_access`.)
for (loc_range, loc) in self.locations.iter_mut(access_range.start, access_range.size) {
let diagnostics = DiagnosticInfo {
alloc_id,
span,
transition_range: loc_range,
access_range: Some(access_range),
access_cause: AccessCause::Dealloc,
};
// Checks the tree containing `idx` for strong protector violations.
// It does this in traversal order.
let mut check_tree = |idx| {
TreeVisitor { nodes: &mut self.nodes, data: loc }
.traverse_this_parents_children_other(
idx,
// Visit all children, skipping none.
|_| ContinueTraversal::Recurse,
|args: NodeAppArgs<'_, _>| {
let node = args.nodes.get(args.idx).unwrap();
let perm = args
.data
.perms
.get(args.idx)
.copied()
.unwrap_or_else(|| node.default_location_state());
if global.borrow().protected_tags.get(&node.tag)
== Some(&ProtectorKind::StrongProtector)
// Don't check for protector if it is a Cell (see `unsafe_cell_deallocate` in `interior_mutability.rs`).
// Related to https://github.com/rust-lang/rust/issues/55005.
&& !perm.permission.is_cell()
// Only trigger UB if the accessed bit is set, i.e. if the protector is actually protecting this offset. See #4579.
&& perm.accessed
{
Err(TbError {
error_kind: TransitionError::ProtectedDealloc,
access_info: &diagnostics,
conflicting_node_info: &node.debug_info,
accessed_node_info: start_idx
.map(|idx| &args.nodes.get(idx).unwrap().debug_info),
}
.build())
} else {
Ok(())
}
},
)
};
// If we have a start index we first check its subtree in traversal order.
// This results in us showing the error of the closest node instead of an
// arbitrary one.
let accessed_root = start_idx.map(&mut check_tree).transpose()?;
// Afterwards we check all other trees.
// We iterate over the list in reverse order to ensure that we do not visit
// a parent before its child.
for &root in self.roots.iter().rev() {
if Some(root) == accessed_root {
continue;
}
check_tree(root)?;
}
}
interp_ok(())
}
/// Map the per-node and per-location `LocationState::perform_access`
/// to each location of the first component of `access_range_and_kind`,
/// on every tag of the allocation.
///
/// `LocationState::perform_access` will take care of raising transition
/// errors and updating the `accessed` status of each location,
/// this traversal adds to that:
/// - inserting into the map locations that do not exist yet,
/// - trimming the traversal,
/// - recording the history.
pub fn perform_access(
&mut self,
prov: ProvenanceExtra,
access_range: AllocRange,
access_kind: AccessKind,
access_cause: AccessCause, // diagnostics
global: &GlobalState,
alloc_id: AllocId, // diagnostics
span: Span, // diagnostics
) -> InterpResult<'tcx> {
#[cfg(feature = "expensive-consistency-checks")]
if self.roots.len() > 1 || matches!(prov, ProvenanceExtra::Wildcard) {
self.verify_wildcard_consistency(global);
}
let source_idx = match prov {
ProvenanceExtra::Concrete(tag) => Some(self.tag_mapping.get(&tag).unwrap()),
ProvenanceExtra::Wildcard => None,
};
// We iterate over affected locations and traverse the tree for each of them.
for (loc_range, loc) in self.locations.iter_mut(access_range.start, access_range.size) {
let diagnostics = DiagnosticInfo {
access_cause,
access_range: Some(access_range),
alloc_id,
span,
transition_range: loc_range,
};
loc.perform_access(
self.roots.iter().copied(),
&mut self.nodes,
source_idx,
access_kind,
global,
ChildrenVisitMode::VisitChildrenOfAccessed,
&diagnostics,
/* min_exposed_child */ None, // only matters for protector end access,
)?;
}
interp_ok(())
}
/// This is the special access that is applied on protector release:
/// - the access will be applied only to accessed locations of the allocation,
/// - it will not be visible to children,
/// - it will be recorded as a `FnExit` diagnostic access
/// - and it will be a read except if the location is `Unique`, i.e. has been written to,
/// in which case it will be a write.
/// - otherwise identical to `Tree::perform_access`
pub fn perform_protector_end_access(
&mut self,
tag: BorTag,
global: &GlobalState,
alloc_id: AllocId, // diagnostics
span: Span, // diagnostics
) -> InterpResult<'tcx> {
#[cfg(feature = "expensive-consistency-checks")]
if self.roots.len() > 1 {
self.verify_wildcard_consistency(global);
}
let source_idx = self.tag_mapping.get(&tag).unwrap();
let min_exposed_child = if self.roots.len() > 1 {
LocationTree::get_min_exposed_child(source_idx, &self.nodes)
} else {
// There's no point in computing this when there is just one tree.
None
};
// This is a special access through the entire allocation.
// It actually only affects `accessed` locations, so we need
// to filter on those before initiating the traversal.
//
// In addition this implicit access should not be visible to children,
// thus the use of `traverse_nonchildren`.
// See the test case `returned_mut_is_usable` from
// `tests/pass/tree_borrows/tree-borrows.rs` for an example of
// why this is important.
for (loc_range, loc) in self.locations.iter_mut_all() {
// Only visit accessed permissions
if let Some(p) = loc.perms.get(source_idx)
&& let Some(access_kind) = p.permission.protector_end_access()
&& p.accessed
{
let diagnostics = DiagnosticInfo {
access_cause: AccessCause::FnExit(access_kind),
access_range: None,
alloc_id,
span,
transition_range: loc_range,
};
loc.perform_access(
self.roots.iter().copied(),
&mut self.nodes,
Some(source_idx),
access_kind,
global,
ChildrenVisitMode::SkipChildrenOfAccessed,
&diagnostics,
min_exposed_child,
)?;
}
}
interp_ok(())
}
}
/// Integration with the BorTag garbage collector
impl Tree {
pub fn remove_unreachable_tags(&mut self, live_tags: &FxHashSet<BorTag>) {
for i in 0..(self.roots.len()) {
self.remove_useless_children(self.roots[i], live_tags);
}
// Right after the GC runs is a good moment to check if we can
// merge some adjacent ranges that were made equal by the removal of some
// tags (this does not necessarily mean that they have identical internal representations,
// see the `PartialEq` impl for `UniValMap`)
self.locations.merge_adjacent_thorough();
}
/// Checks if a node is useless and should be GC'ed.
/// A node is useless if it has no children and also the tag is no longer live.
fn is_useless(&self, idx: UniIndex, live: &FxHashSet<BorTag>) -> bool {
let node = self.nodes.get(idx).unwrap();
node.children.is_empty() && !live.contains(&node.tag)
}
/// Checks whether a node can be replaced by its only child.
/// If so, returns the index of said only child.
/// If not, returns none.
fn can_be_replaced_by_single_child(
&self,
idx: UniIndex,
live: &FxHashSet<BorTag>,
) -> Option<UniIndex> {
let node = self.nodes.get(idx).unwrap();
let [child_idx] = node.children[..] else { return None };
// We never want to replace the root node, as it is also kept in `root_ptr_tags`.
if live.contains(&node.tag) || node.parent.is_none() {
return None;
}
// Since protected nodes are never GC'd (see `borrow_tracker::FrameExtra::visit_provenance`),
// we know that `node` is not protected because otherwise `live` would
// have contained `node.tag`.
let child = self.nodes.get(child_idx).unwrap();
// Check that for that one child, `can_be_replaced_by_child` holds for the permission
// on all locations.
for (_range, loc) in self.locations.iter_all() {
let parent_perm = loc
.perms
.get(idx)
.map(|x| x.permission)
.unwrap_or_else(|| node.default_initial_perm);
let child_perm = loc
.perms
.get(child_idx)
.map(|x| x.permission)
.unwrap_or_else(|| child.default_initial_perm);
if !parent_perm.can_be_replaced_by_child(child_perm) {
return None;
}
}
Some(child_idx)
}
/// Properly removes a node.
/// The node to be removed should not otherwise be usable. It also
/// should have no children, but this is not checked, so that nodes
/// whose children were rotated somewhere else can be deleted without
/// having to first modify them to clear that array.
fn remove_useless_node(&mut self, this: UniIndex) {
// Due to the API of UniMap we must make sure to call
// `UniValMap::remove` for the key of this node on *all* maps that used it
// (which are `self.nodes` and every range of `self.rperms`)
// before we can safely apply `UniKeyMap::remove` to truly remove
// this tag from the `tag_mapping`.
let node = self.nodes.remove(this).unwrap();
for (_range, loc) in self.locations.iter_mut_all() {
loc.perms.remove(this);
loc.wildcard_accesses.remove(this);
}
self.tag_mapping.remove(&node.tag);
}
/// Traverses the entire tree looking for useless tags.
/// Removes from the tree all useless child nodes of root.
/// It will not delete the root itself.
///
/// NOTE: This leaves in the middle of the tree tags that are unreachable but have
/// reachable children. There is a potential for compacting the tree by reassigning
/// children of dead tags to the nearest live parent, but it must be done with care
/// not to remove UB.
///
/// Example: Consider the tree `root - parent - child`, with `parent: Frozen` and
/// `child: Reserved`. This tree can exist. If we blindly delete `parent` and reassign
/// `child` to be a direct child of `root` then Writes to `child` are now permitted
/// whereas they were not when `parent` was still there.
fn remove_useless_children(&mut self, root: UniIndex, live: &FxHashSet<BorTag>) {
// To avoid stack overflows, we roll our own stack.
// Each element in the stack consists of the current tag, and the number of the
// next child to be processed.
// The other functions are written using the `TreeVisitorStack`, but that does not work here
// since we need to 1) do a post-traversal and 2) remove nodes from the tree.
// Since we do a post-traversal (by deleting nodes only after handling all children),
// we also need to be a bit smarter than "pop node, push all children."
let mut stack = vec![(root, 0)];
while let Some((tag, nth_child)) = stack.last_mut() {
let node = self.nodes.get(*tag).unwrap();
if *nth_child < node.children.len() {
// Visit the child by pushing it to the stack.
// Also increase `nth_child` so that when we come back to the `tag` node, we
// look at the next child.
let next_child = node.children[*nth_child];
*nth_child += 1;
stack.push((next_child, 0));
continue;
} else {
// We have processed all children of `node`, so now it is time to process `node` itself.
// First, get the current children of `node`. To appease the borrow checker,
// we have to temporarily move the list out of the node, and then put the
// list of remaining children back in.
let mut children_of_node =
mem::take(&mut self.nodes.get_mut(*tag).unwrap().children);
// Remove all useless children.
children_of_node.retain_mut(|idx| {
if self.is_useless(*idx, live) {
// Delete `idx` node everywhere else.
self.remove_useless_node(*idx);
// And delete it from children_of_node.
false
} else {
if let Some(nextchild) = self.can_be_replaced_by_single_child(*idx, live) {
// `nextchild` is our grandchild, and will become our direct child.
// Delete the in-between node, `idx`.
self.remove_useless_node(*idx);
// Set the new child's parent.
self.nodes.get_mut(nextchild).unwrap().parent = Some(*tag);
// Save the new child in children_of_node.
*idx = nextchild;
}
// retain it
true
}
});
// Put back the now-filtered vector.
self.nodes.get_mut(*tag).unwrap().children = children_of_node;
// We are done, the parent can continue.
stack.pop();
continue;
}
}
}
}
impl<'tcx> LocationTree {
/// Returns the smallest exposed tag, if any, that is a transitive child of `root`.
fn get_min_exposed_child(root: UniIndex, nodes: &UniValMap<Node>) -> Option<BorTag> {
// We cannot use the wildcard datastructure to improve this lookup. This is because
// the datastructure only tracks enabled nodes and we need to also consider disabled ones.
let mut stack = vec![root];
let mut min_tag = None;
while let Some(idx) = stack.pop() {
let node = nodes.get(idx).unwrap();
if min_tag.is_some_and(|min| min < node.tag) {
// The minimum we found before is bigger than this tag, and therefore
// also bigger than all its children, so we can skip this subtree.
continue;
}
stack.extend_from_slice(node.children.as_slice());
if node.is_exposed {
min_tag = match min_tag {
Some(prev) if prev < node.tag => Some(prev),
_ => Some(node.tag),
};
}
}
min_tag
}
/// Performs an access on this location.
/// * `access_source`: The index, if any, where the access came from.
/// * `visit_children`: Whether to skip updating the children of `access_source`.
/// * `min_exposed_child`: The tag of the smallest exposed (transitive) child of the accessed node.
/// This is only used with `visit_children == SkipChildrenOfAccessed`, where we need to skip children
/// of the accessed node.
fn perform_access(
&mut self,
roots: impl Iterator<Item = UniIndex>,
nodes: &mut UniValMap<Node>,
access_source: Option<UniIndex>,
access_kind: AccessKind,
global: &GlobalState,
visit_children: ChildrenVisitMode,
diagnostics: &DiagnosticInfo,
min_exposed_child: Option<BorTag>,
) -> InterpResult<'tcx> {
let accessed_root = if let Some(idx) = access_source {
Some(self.perform_normal_access(
idx,
nodes,
access_kind,
global,
visit_children,
diagnostics,
)?)
} else {
// `SkipChildrenOfAccessed` only gets set on protector release, which only
// occurs on a known node.
assert!(matches!(visit_children, ChildrenVisitMode::VisitChildrenOfAccessed));
None
};
let accessed_root_tag = accessed_root.map(|idx| nodes.get(idx).unwrap().tag);
for root in roots {
let tag = nodes.get(root).unwrap().tag;
// On a protector release access we have to skip the children of the accessed tag.
// However, if the tag has exposed children then some of the wildcard subtrees could
// also be children of the accessed node and would also need to be skipped. We can
// narrow down which wildcard trees might be children by comparing their root tag to the
// minimum exposed child of the accessed node. As the parent tag is always smaller
// than the child tag this means we only need to skip subtrees with a root tag larger
// than `min_exposed_child`. Once we find such a root, we can leave the loop because roots
// are sorted by tag.
if matches!(visit_children, ChildrenVisitMode::SkipChildrenOfAccessed)
&& let Some(min_exposed_child) = min_exposed_child
&& tag > min_exposed_child
{
break;
}
// We don't perform a wildcard access on the tree we already performed a
// normal access on.
if Some(root) == accessed_root {
continue;
}
// The choice of `max_local_tag` requires some thought.
// This can only be a local access for nodes that are a parent of the accessed node
// and are therefore smaller, so the accessed node itself is a valid choice for `max_local_tag`.
// However, using `accessed_root` is better since that will be smaller. It is still a valid choice
// because for nodes *in other trees*, if they are a parent of the accessed node then they
// are a parent of `accessed_root`.
//
// As a consequence of this, since the root of the main tree is the smallest tag in the entire
// allocation, if the access occurred in the main tree then other subtrees will only see foreign accesses.
self.perform_wildcard_access(
root,
access_source,
/*max_local_tag*/ accessed_root_tag,
nodes,
access_kind,
global,
diagnostics,
)?;
}
interp_ok(())
}
/// Performs a normal access on the tree containing `access_source`.
///
/// Returns the root index of this tree.
/// * `access_source`: The index of the tag being accessed.
/// * `visit_children`: Whether to skip the children of `access_source`
/// during the access. Used for protector end access.
fn perform_normal_access(
&mut self,
access_source: UniIndex,
nodes: &mut UniValMap<Node>,
access_kind: AccessKind,
global: &GlobalState,
visit_children: ChildrenVisitMode,
diagnostics: &DiagnosticInfo,
) -> InterpResult<'tcx, UniIndex> {
// Performs the per-node work:
// - insert the permission if it does not exist
// - perform the access
// - record the transition
// to which some optimizations are added:
// - skip the traversal of the children in some cases
// - do not record noop transitions
//
// `loc_range` is only for diagnostics (it is the range of
// the `RangeMap` on which we are currently working).
let node_skipper = |args: &NodeAppArgs<'_, LocationTree>| -> ContinueTraversal {
let node = args.nodes.get(args.idx).unwrap();
let perm = args.data.perms.get(args.idx);
let old_state = perm.copied().unwrap_or_else(|| node.default_location_state());
old_state.skip_if_known_noop(access_kind, args.rel_pos)
};
let node_app = |args: NodeAppArgs<'_, LocationTree>| {
let node = args.nodes.get_mut(args.idx).unwrap();
let mut perm = args.data.perms.entry(args.idx);
let state = perm.or_insert(node.default_location_state());
let protected = global.borrow().protected_tags.contains_key(&node.tag);
state
.perform_transition(
args.idx,
args.nodes,
&mut args.data.wildcard_accesses,
access_kind,
args.rel_pos,
protected,
diagnostics,
)
.map_err(|error_kind| {
TbError {
error_kind,
access_info: diagnostics,
conflicting_node_info: &args.nodes.get(args.idx).unwrap().debug_info,
accessed_node_info: Some(
&args.nodes.get(access_source).unwrap().debug_info,
),
}
.build()
})
};
let visitor = TreeVisitor { nodes, data: self };
match visit_children {
ChildrenVisitMode::VisitChildrenOfAccessed =>
visitor.traverse_this_parents_children_other(access_source, node_skipper, node_app),
ChildrenVisitMode::SkipChildrenOfAccessed =>
visitor.traverse_nonchildren(access_source, node_skipper, node_app),
}
.into()
}
/// Performs a wildcard access on the tree with root `root`. Takes the `access_relatedness`
/// for each node from the `WildcardState` datastructure.
/// * `root`: Root of the tree being accessed.
/// * `access_source`: the index of the accessed tag, if any.
/// This is only used for printing the correct tag on errors.
/// * `max_local_tag`: The access can only be local for nodes whose tag is
/// at most `max_local_tag`.
fn perform_wildcard_access(
&mut self,
root: UniIndex,
access_source: Option<UniIndex>,
max_local_tag: Option<BorTag>,
nodes: &mut UniValMap<Node>,
access_kind: AccessKind,
global: &GlobalState,
diagnostics: &DiagnosticInfo,
) -> InterpResult<'tcx> {
let get_relatedness = |idx: UniIndex, node: &Node, loc: &LocationTree| {
let wildcard_state = loc.wildcard_accesses.get(idx).cloned().unwrap_or_default();
// If the tag is larger than `max_local_tag` then the access can only be foreign.
let only_foreign = max_local_tag.is_some_and(|max_local_tag| max_local_tag < node.tag);
wildcard_state.access_relatedness(access_kind, only_foreign)
};
// Whether there is an exposed node in this tree that allows this access.
let mut has_valid_exposed = false;
// This does a traversal across the tree updating children before their parents. The
// difference to `perform_normal_access` is that we take the access relatedness from
// the wildcard tracking state of the node instead of from the visitor itself.
//
// Unlike for a normal access, the iteration order is important for improving the
// accuracy of wildcard accesses if `max_local_tag` is `Some`: processing the effects of this
// access further down the tree can cause exposed nodes to lose permissions, thus updating
// the wildcard data structure, which will be taken into account when processing the parent
// nodes. Also see the test `cross_tree_update_older_invalid_exposed2.rs`
// (Doing accesses in the opposite order cannot help with precision but the reasons are complicated;
// see <https://github.com/rust-lang/miri/pull/4707#discussion_r2581661123>.)
//
// Note, however, that this is an approximation: there can be situations where a node is
// marked as having an exposed foreign node, but actually that foreign node cannot be
// the source of the access due to `max_local_tag`. The wildcard tracking cannot know
// about `max_local_tag` so we will incorrectly assume that this might be a foreign access.
TreeVisitor { data: self, nodes }.traverse_children_this(
root,
|args| -> ContinueTraversal {
let node = args.nodes.get(args.idx).unwrap();
let perm = args.data.perms.get(args.idx);
let old_state = perm.copied().unwrap_or_else(|| node.default_location_state());
// If we know where, relative to this node, the wildcard access occurs,
// then check if we can skip the entire subtree.
if let Some(relatedness) = get_relatedness(args.idx, node, args.data)
&& let Some(relatedness) = relatedness.to_relatedness()
{
// We can use the usual SIFA machinery to skip nodes.
old_state.skip_if_known_noop(access_kind, relatedness)
} else {
ContinueTraversal::Recurse
}
},
|args| {
let node = args.nodes.get_mut(args.idx).unwrap();
let protected = global.borrow().protected_tags.contains_key(&node.tag);
let Some(wildcard_relatedness) = get_relatedness(args.idx, node, args.data) else {
// There doesn't exist a valid exposed reference for this access to
// happen through.
// This can only happen if `root` is the main root: We set
// `max_foreign_access==Write` on all wildcard roots, so at least a foreign access
// is always possible on all nodes in a wildcard subtree.
return Err(no_valid_exposed_references_error(diagnostics));
};
let mut entry = args.data.perms.entry(args.idx);
let perm = entry.or_insert(node.default_location_state());
// We only count exposed nodes through which an access could happen.
if node.is_exposed
&& perm.permission.strongest_allowed_local_access(protected).allows(access_kind)
&& max_local_tag.is_none_or(|max_local_tag| max_local_tag >= node.tag)
{
has_valid_exposed = true;
}
let Some(relatedness) = wildcard_relatedness.to_relatedness() else {
// If the access type is Either, then we do not apply any transition
// to this node, but we still update each of its children.
// This is an imprecision! In the future, maybe we can still do some sort
// of best-effort update here.
return Ok(());
};
// We know the exact relatedness, so we can actually do precise checks.
perm.perform_transition(
args.idx,
args.nodes,
&mut args.data.wildcard_accesses,
access_kind,
relatedness,
protected,
diagnostics,
)
.map_err(|trans| {
let node = args.nodes.get(args.idx).unwrap();
TbError {
error_kind: trans,
access_info: diagnostics,
conflicting_node_info: &node.debug_info,
accessed_node_info: access_source
.map(|idx| &args.nodes.get(idx).unwrap().debug_info),
}
.build()
})
},
)?;
// If there is no exposed node in this tree that allows this access, then the
// access *must* be foreign. So we check if the root of this tree would allow this
// as a foreign access, and if not, then we can error.
// In practice, all wildcard trees accept foreign accesses, but the main tree does
// not, so this catches UB when none of the nodes in the main tree allows this access.
if !has_valid_exposed
&& self
.wildcard_accesses
.get(root)
.unwrap()
.access_relatedness(access_kind, /* only_foreign */ true)
.is_none()
{
return Err(no_valid_exposed_references_error(diagnostics)).into();
}
interp_ok(())
}
}
impl Node {
pub fn default_location_state(&self) -> LocationState {
LocationState::new_non_accessed(
self.default_initial_perm,
self.default_initial_idempotent_foreign_access,
)
}
}
impl VisitProvenance for Tree {
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
// To ensure that the roots never get removed, we visit them.
// FIXME: it should be possible to GC wildcard tree roots.
for id in self.roots.iter().copied() {
visit(None, Some(self.nodes.get(id).unwrap().tag));
}
// We also need to keep around any exposed tags through which
// an access could still happen.
for (_id, node) in self.nodes.iter() {
if node.is_exposed {
visit(None, Some(node.tag))
}
}
}
}
/// Relative position of the access
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AccessRelatedness {
/// The access happened either through the node itself or one of
/// its transitive children.
LocalAccess,
/// The access happened through this nodes ancestor or through
/// a sibling/cousin/uncle/etc.
ForeignAccess,
}
impl AccessRelatedness {
/// Check that access is either Ancestor or Distant, i.e. not
/// a transitive child (initial pointer included).
pub fn is_foreign(self) -> bool {
matches!(self, AccessRelatedness::ForeignAccess)
}
}