diff --git a/Cargo.toml b/Cargo.toml index 12123c260d..ae874236f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ harness = false default = ["stack-cache", "native-lib"] genmc = ["dep:genmc-sys"] stack-cache = [] -stack-cache-consistency-check = ["stack-cache"] +expensive-consistency-checks = ["stack-cache"] tracing = ["serde_json"] native-lib = ["dep:libffi", "dep:libloading", "dep:capstone", "dep:ipc-channel", "dep:nix", "dep:serde"] diff --git a/src/bin/miri.rs b/src/bin/miri.rs index 3c759521c6..598a92d1a2 100644 --- a/src/bin/miri.rs +++ b/src/bin/miri.rs @@ -524,7 +524,6 @@ fn main() { Some(BorrowTrackerMethod::TreeBorrows(TreeBorrowsParams { precise_interior_mut: true, })); - miri_config.provenance_mode = ProvenanceMode::Strict; } else if arg == "-Zmiri-tree-borrows-no-precise-interior-mut" { match &mut miri_config.borrow_tracker { Some(BorrowTrackerMethod::TreeBorrows(params)) => { @@ -720,17 +719,6 @@ fn main() { rustc_args.push(arg); } } - // Tree Borrows implies strict provenance, and is not compatible with native calls. - if matches!(miri_config.borrow_tracker, Some(BorrowTrackerMethod::TreeBorrows { .. })) { - if miri_config.provenance_mode != ProvenanceMode::Strict { - fatal_error!( - "Tree Borrows does not support integer-to-pointer casts, and hence requires strict provenance" - ); - } - if !miri_config.native_lib.is_empty() { - fatal_error!("Tree Borrows is not compatible with calling native functions"); - } - } // Native calls and strict provenance are not compatible. if !miri_config.native_lib.is_empty() && miri_config.provenance_mode == ProvenanceMode::Strict { diff --git a/src/borrow_tracker/mod.rs b/src/borrow_tracker/mod.rs index 89bd93edae..2c54a52aaa 100644 --- a/src/borrow_tracker/mod.rs +++ b/src/borrow_tracker/mod.rs @@ -365,6 +365,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let borrow_tracker = this.machine.borrow_tracker.as_ref().unwrap(); // The body of this loop needs `borrow_tracker` immutably // so we can't move this code inside the following `end_call`. + for (alloc_id, tag) in &frame .extra .borrow_tracker @@ -391,6 +392,28 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } } borrow_tracker.borrow_mut().end_call(&frame.extra); + + // We do a consistency check after the protected_tags get released in `end_call()`. + // Before this point the wildcard tracking datastructure might not be consistent with + // the trees actual permissions. + #[cfg(feature = "expensive-consistency-checks")] + for (alloc_id, _) in &frame + .extra + .borrow_tracker + .as_ref() + .expect("we should have borrow tracking data") + .protected_tags + { + let kind = this.get_alloc_info(*alloc_id).kind; + if matches!(kind, AllocKind::LiveData) { + let alloc_extra = this.get_alloc_extra(*alloc_id)?; + let alloc_borrow_tracker = &alloc_extra.borrow_tracker.as_ref().unwrap(); + if let AllocState::TreeBorrows(tb) = alloc_borrow_tracker { + tb.borrow().verify_wildcard_consistency(borrow_tracker); + } + } + } + interp_ok(()) } } diff --git a/src/borrow_tracker/stacked_borrows/stack.rs b/src/borrow_tracker/stacked_borrows/stack.rs index dc3370f125..905200b754 100644 --- a/src/borrow_tracker/stacked_borrows/stack.rs +++ b/src/borrow_tracker/stacked_borrows/stack.rs @@ -153,7 +153,7 @@ impl<'tcx> Stack { /// Panics if any of the caching mechanisms have broken, /// - The StackCache indices don't refer to the parallel items, /// - There are no Unique items outside of first_unique..last_unique - #[cfg(feature = "stack-cache-consistency-check")] + #[cfg(feature = "expensive-consistency-checks")] fn verify_cache_consistency(&self) { // Only a full cache needs to be valid. Also see the comments in find_granting_cache // and set_unknown_bottom. @@ -197,7 +197,7 @@ impl<'tcx> Stack { tag: ProvenanceExtra, exposed_tags: &FxHashSet, ) -> Result, ()> { - #[cfg(feature = "stack-cache-consistency-check")] + #[cfg(feature = "expensive-consistency-checks")] self.verify_cache_consistency(); let ProvenanceExtra::Concrete(tag) = tag else { @@ -334,7 +334,7 @@ impl<'tcx> Stack { // This primes the cache for the next access, which is almost always the just-added tag. self.cache.add(new_idx, new); - #[cfg(feature = "stack-cache-consistency-check")] + #[cfg(feature = "expensive-consistency-checks")] self.verify_cache_consistency(); } @@ -417,7 +417,7 @@ impl<'tcx> Stack { self.unique_range.end = self.unique_range.end.min(disable_start); } - #[cfg(feature = "stack-cache-consistency-check")] + #[cfg(feature = "expensive-consistency-checks")] self.verify_cache_consistency(); interp_ok(()) @@ -472,7 +472,7 @@ impl<'tcx> Stack { self.unique_range = 0..0; } - #[cfg(feature = "stack-cache-consistency-check")] + #[cfg(feature = "expensive-consistency-checks")] self.verify_cache_consistency(); interp_ok(()) } diff --git a/src/borrow_tracker/tree_borrows/diagnostics.rs b/src/borrow_tracker/tree_borrows/diagnostics.rs index 00f921b0f8..1c92954c68 100644 --- a/src/borrow_tracker/tree_borrows/diagnostics.rs +++ b/src/borrow_tracker/tree_borrows/diagnostics.rs @@ -291,9 +291,10 @@ pub(super) struct TbError<'node> { pub conflicting_info: &'node NodeDebugInfo, // What kind of access caused this error (read, write, reborrow, deallocation) pub access_cause: AccessCause, - /// Which tag the access that caused this error was made through, i.e. + /// Which tag, if any, the access that caused this error was made through, i.e. /// which tag was used to read/write/deallocate. - pub accessed_info: &'node NodeDebugInfo, + /// Not set on wildcard accesses. + pub accessed_info: Option<&'node NodeDebugInfo>, } impl TbError<'_> { @@ -302,10 +303,12 @@ impl TbError<'_> { use TransitionError::*; let cause = self.access_cause; let accessed = self.accessed_info; + let accessed_str = + self.accessed_info.map(|v| format!("{v}")).unwrap_or_else(|| "wildcard".into()); let conflicting = self.conflicting_info; - let accessed_is_conflicting = accessed.tag == conflicting.tag; + let accessed_is_conflicting = accessed.map(|a| a.tag == conflicting.tag).unwrap_or(false); let title = format!( - "{cause} through {accessed} at {alloc_id:?}[{offset:#x}] is forbidden", + "{cause} through {accessed_str} at {alloc_id:?}[{offset:#x}] is forbidden", alloc_id = self.alloc_id, offset = self.error_offset ); @@ -316,7 +319,7 @@ impl TbError<'_> { let mut details = Vec::new(); if !accessed_is_conflicting { details.push(format!( - "the accessed tag {accessed} is a child of the conflicting tag {conflicting}" + "the accessed tag {accessed_str} is a child of the conflicting tag {conflicting}" )); } let access = cause.print_as_access(/* is_foreign */ false); @@ -330,7 +333,7 @@ impl TbError<'_> { let access = cause.print_as_access(/* is_foreign */ true); let details = vec![ format!( - "the accessed tag {accessed} is foreign to the {conflicting_tag_name} tag {conflicting} (i.e., it is not a child)" + "the accessed tag {accessed_str} is foreign to the {conflicting_tag_name} tag {conflicting} (i.e., it is not a child)" ), format!( "this {access} would cause the {conflicting_tag_name} tag {conflicting} (currently {before_disabled}) to become Disabled" @@ -343,7 +346,7 @@ impl TbError<'_> { let conflicting_tag_name = "strongly protected"; let details = vec![ format!( - "the allocation of the accessed tag {accessed} also contains the {conflicting_tag_name} tag {conflicting}" + "the allocation of the accessed tag {accessed_str} also contains the {conflicting_tag_name} tag {conflicting}" ), format!("the {conflicting_tag_name} tag {conflicting} disallows deallocations"), ]; @@ -351,8 +354,10 @@ impl TbError<'_> { } }; let mut history = HistoryData::default(); - if !accessed_is_conflicting { - history.extend(self.accessed_info.history.forget(), "accessed", false); + if let Some(accessed_info) = self.accessed_info + && !accessed_is_conflicting + { + history.extend(accessed_info.history.forget(), "accessed", false); } history.extend( self.conflicting_info.history.extract_relevant(self.error_offset, self.error_kind), @@ -362,6 +367,21 @@ impl TbError<'_> { err_machine_stop!(TerminationInfo::TreeBorrowsUb { title, details, history }) } } +/// Cannot access this allocation with wildcard provenance, as there are no +/// valid exposed references for this access kind. +pub fn no_valid_exposed_references_error<'tcx>( + alloc_id: AllocId, + offset: u64, + access_cause: AccessCause, +) -> InterpErrorKind<'tcx> { + let title = + format!("{access_cause} through wildcard at {alloc_id:?}[{offset:#x}] is forbidden"); + let details = vec![format!( + "there are no exposed references who have {access_cause} permissions to this location" + )]; + let history = HistoryData::default(); + err_machine_stop!(TerminationInfo::TreeBorrowsUb { title, details, history }) +} type S = &'static str; /// Pretty-printing details @@ -623,10 +643,10 @@ impl DisplayRepr { } else { // We take this node let rperm = tree - .rperms + .locations .iter_all() - .map(move |(_offset, perms)| { - let perm = perms.get(idx); + .map(move |(_offset, loc)| { + let perm = loc.perms.get(idx); perm.cloned() }) .collect::>(); @@ -788,7 +808,7 @@ impl<'tcx> Tree { show_unnamed: bool, ) -> InterpResult<'tcx> { let mut indenter = DisplayIndent::new(); - let ranges = self.rperms.iter_all().map(|(range, _perms)| range).collect::>(); + let ranges = self.locations.iter_all().map(|(range, _loc)| range).collect::>(); if let Some(repr) = DisplayRepr::from(self, show_unnamed) { repr.print( &DEFAULT_FORMATTER, diff --git a/src/borrow_tracker/tree_borrows/mod.rs b/src/borrow_tracker/tree_borrows/mod.rs index 865097af10..0d21460a9c 100644 --- a/src/borrow_tracker/tree_borrows/mod.rs +++ b/src/borrow_tracker/tree_borrows/mod.rs @@ -5,6 +5,7 @@ use rustc_middle::ty::{self, Ty}; use self::foreign_access_skipping::IdempotentForeignAccess; use self::tree::LocationState; +use self::wildcard::WildcardState; use crate::borrow_tracker::{GlobalState, GlobalStateInner, ProtectorKind}; use crate::concurrency::data_race::NaReadType; use crate::*; @@ -14,6 +15,7 @@ mod foreign_access_skipping; mod perms; mod tree; mod unimap; +mod wildcard; #[cfg(test)] mod exhaustive; @@ -54,16 +56,10 @@ impl<'tcx> Tree { interpret::Pointer::new(alloc_id, range.start), range.size.bytes(), ); - // TODO: for now we bail out on wildcard pointers. Eventually we should - // handle them as much as we can. - let tag = match prov { - ProvenanceExtra::Concrete(tag) => tag, - ProvenanceExtra::Wildcard => return interp_ok(()), - }; let global = machine.borrow_tracker.as_ref().unwrap(); let span = machine.current_user_relevant_span(); self.perform_access( - tag, + prov, Some((range, access_kind, diagnostics::AccessCause::Explicit(access_kind))), global, alloc_id, @@ -79,19 +75,9 @@ impl<'tcx> Tree { size: Size, machine: &MiriMachine<'tcx>, ) -> InterpResult<'tcx> { - // TODO: for now we bail out on wildcard pointers. Eventually we should - // handle them as much as we can. - let tag = match prov { - ProvenanceExtra::Concrete(tag) => tag, - ProvenanceExtra::Wildcard => return interp_ok(()), - }; let global = machine.borrow_tracker.as_ref().unwrap(); let span = machine.current_user_relevant_span(); - self.dealloc(tag, alloc_range(Size::ZERO, size), global, alloc_id, span) - } - - pub fn expose_tag(&mut self, _tag: BorTag) { - // TODO + self.dealloc(prov, alloc_range(Size::ZERO, size), global, alloc_id, span) } /// A tag just lost its protector. @@ -109,7 +95,32 @@ impl<'tcx> Tree { ) -> InterpResult<'tcx> { let span = machine.current_user_relevant_span(); // `None` makes it the magic on-protector-end operation - self.perform_access(tag, None, global, alloc_id, span) + self.perform_access(ProvenanceExtra::Concrete(tag), None, global, alloc_id, span)?; + + // Changing the protector status of a pointer can change which child accesses are + // allowed to it so we need to update the wildcard tracking info with the new strongest allowed child access. + let idx = self.tag_mapping.get(&tag).unwrap(); + if self.nodes.get(idx).unwrap().is_exposed { + for (_, loc) in self.locations.iter_mut_all() { + if let Some(p) = loc.perms.get(idx) + && loc.wildcard_accesses.get(idx).is_some() + { + // This temporarily desyncs the wildcard datastructure from the actual permissions represented in the tree. + // This happens because the protected_tags map still contains all protected_tags, they only get removed after + // `release_protector` got called on all the relevant tags. + // + // This is also why we dont call `WildcardState::verify_external_consistency` here. + let access_level = p.permission().strongest_allowed_child_access(false); + WildcardState::update_exposure( + idx, + access_level, + &self.nodes, + &mut loc.wildcard_accesses, + ); + } + } + } + interp_ok(()) } } @@ -235,38 +246,56 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // After all, the pointer may be lazily initialized outside this initial range. data } - Err(_) => { + _ => { assert_eq!(ptr_size, Size::ZERO); // we did the deref check above, size has to be 0 here // This pointer doesn't come with an AllocId, so there's no // memory to do retagging in. + let new_prov = place.ptr().provenance; trace!( "reborrow of size 0: reference {:?} derived from {:?} (pointee {})", - new_tag, + new_prov, place.ptr(), place.layout.ty, ); log_creation(this, None)?; // Keep original provenance. - return interp_ok(place.ptr().provenance); + return interp_ok(new_prov); } }; + + let is_wildcard = matches!(parent_prov, ProvenanceExtra::Wildcard); + + if is_wildcard && ptr_size == Size::ZERO { + let new_prov = Provenance::Wildcard; + trace!( + "reborrow of size 0: reference {:?} derived from {:?} (pointee {})", + new_prov, + place.ptr(), + place.layout.ty, + ); + log_creation(this, None)?; + // Keep original provenance. + return interp_ok(Some(new_prov)); + } + log_creation(this, Some((alloc_id, base_offset, parent_prov)))?; - let orig_tag = match parent_prov { - ProvenanceExtra::Wildcard => return interp_ok(place.ptr().provenance), // TODO: handle wildcard pointers - ProvenanceExtra::Concrete(tag) => tag, + let new_prov = match parent_prov { + ProvenanceExtra::Wildcard => Provenance::Wildcard, // TODO: handle retagging wildcard pointers + ProvenanceExtra::Concrete(_) => Provenance::Concrete { alloc_id, tag: new_tag }, }; trace!( "reborrow: reference {:?} derived from {:?} (pointee {}): {:?}, size {}", new_tag, - orig_tag, + parent_prov, place.layout.ty, interpret::Pointer::new(alloc_id, base_offset), ptr_size.bytes() ); - if let Some(protect) = new_perm.protector { + // TODO support protecting wildcard pointers. + if !is_wildcard && let Some(protect) = new_perm.protector { // We register the protection in two different places. // This makes creating a protector slower, but checking whether a tag // is protected faster. @@ -291,7 +320,7 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { assert_eq!(ptr_size, Size::ZERO); // we did the deref check above, size has to be 0 here // There's not actually any bytes here where accesses could even be tracked. // Just produce the new provenance, nothing else to do. - return interp_ok(Some(Provenance::Concrete { alloc_id, tag: new_tag })); + return interp_ok(Some(new_prov)); } let protected = new_perm.protector.is_some(); @@ -354,9 +383,8 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { start: Size::from_bytes(perm_range.start) + base_offset, size: Size::from_bytes(perm_range.end - perm_range.start), }; - tree_borrows.perform_access( - orig_tag, + parent_prov, Some((range_in_alloc, AccessKind::Read, diagnostics::AccessCause::Reborrow)), this.machine.borrow_tracker.as_ref().unwrap(), alloc_id, @@ -377,20 +405,21 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } } } - - // Record the parent-child pair in the tree. - tree_borrows.new_child( - base_offset, - orig_tag, - new_tag, - inside_perms, - new_perm.outside_perm, - protected, - this.machine.current_user_relevant_span(), - )?; + if let ProvenanceExtra::Concrete(orig_tag) = parent_prov { + // Record the parent-child pair in the tree. + tree_borrows.new_child( + base_offset, + orig_tag, + new_tag, + inside_perms, + new_perm.outside_perm, + protected, + this.machine.current_user_relevant_span(), + )?; + } drop(tree_borrows); - interp_ok(Some(Provenance::Concrete { alloc_id, tag: new_tag })) + interp_ok(Some(new_prov)) } fn tb_retag_place( @@ -599,6 +628,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // This is okay because accessing them is UB anyway, no need for any Tree Borrows checks. // NOT using `get_alloc_extra_mut` since this might be a read-only allocation! let kind = this.get_alloc_info(alloc_id).kind; + match kind { AllocKind::LiveData => { // This should have alloc_extra data, but `get_alloc_extra` can still fail @@ -606,7 +636,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // uncovers a non-supported `extern static`. let alloc_extra = this.get_alloc_extra(alloc_id)?; trace!("Tree Borrows tag {tag:?} exposed in {alloc_id:?}"); - alloc_extra.borrow_tracker_tb().borrow_mut().expose_tag(tag); + let global = this.machine.borrow_tracker.as_ref().unwrap(); + alloc_extra.borrow_tracker_tb().borrow_mut().expose_tag(tag, global); } AllocKind::Function | AllocKind::VTable | AllocKind::TypeId | AllocKind::Dead => { // No tree borrows on these allocations. diff --git a/src/borrow_tracker/tree_borrows/perms.rs b/src/borrow_tracker/tree_borrows/perms.rs index e21775c9f2..d769f2e5a7 100644 --- a/src/borrow_tracker/tree_borrows/perms.rs +++ b/src/borrow_tracker/tree_borrows/perms.rs @@ -53,6 +53,7 @@ enum PermissionPriv { } use self::PermissionPriv::*; use super::foreign_access_skipping::IdempotentForeignAccess; +use super::wildcard::WildcardAccessLevel; impl PartialOrd for PermissionPriv { /// PermissionPriv is ordered by the reflexive transitive closure of @@ -372,6 +373,23 @@ impl Permission { pub fn strongest_idempotent_foreign_access(&self, prot: bool) -> IdempotentForeignAccess { self.inner.strongest_idempotent_foreign_access(prot) } + + /// Returns the strongest access allowed from a child to this node without + /// causing UB (only considers possible transitions to this permission). + pub fn strongest_allowed_child_access(&self, protected: bool) -> WildcardAccessLevel { + match self.inner { + // everything except disabled can be accessed by read access + Disabled => WildcardAccessLevel::None, + // frozen references cannot be written to by a child + Frozen => WildcardAccessLevel::Read, + // If the `conflicted` flag is set, then there was a foreign read during + // the function call that is still ongoing (still `protected`), + // this is UB (`noalias` violation). + ReservedFrz { conflicted: true } if protected => WildcardAccessLevel::Read, + // everything else allows writes + _ => WildcardAccessLevel::Write, + } + } } impl PermTransition { @@ -640,7 +658,17 @@ mod propagation_optimization_checks { impl Exhaustive for AccessRelatedness { fn exhaustive() -> Box> { use AccessRelatedness::*; - Box::new(vec![This, StrictChildAccess, AncestorAccess, CousinAccess].into_iter()) + Box::new( + vec![ + This, + StrictChildAccess, + AncestorAccess, + CousinAccess, + WildcardChildAccess, + WildcardForeignAccess, + ] + .into_iter(), + ) } } diff --git a/src/borrow_tracker/tree_borrows/tree.rs b/src/borrow_tracker/tree_borrows/tree.rs index c4345c6328..672725e5df 100644 --- a/src/borrow_tracker/tree_borrows/tree.rs +++ b/src/borrow_tracker/tree_borrows/tree.rs @@ -18,9 +18,10 @@ use rustc_data_structures::fx::FxHashSet; use rustc_span::Span; use smallvec::SmallVec; +use super::wildcard::WildcardState; use crate::borrow_tracker::tree_borrows::Permission; use crate::borrow_tracker::tree_borrows::diagnostics::{ - self, NodeDebugInfo, TbError, TransitionError, + self, NodeDebugInfo, TbError, TransitionError, no_valid_exposed_references_error, }; use crate::borrow_tracker::tree_borrows::foreign_access_skipping::IdempotentForeignAccess; use crate::borrow_tracker::tree_borrows::perms::PermTransition; @@ -30,7 +31,7 @@ use crate::*; mod tests; -/// Data for a single *location*. +/// 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 @@ -212,6 +213,28 @@ impl fmt::Display for LocationState { } } +/// State associated with each location of the allocation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LocationTree { + /// Maps a tag to a perm, with possible lazy initialization. + /// + /// NOTE: not all tags registered in `nodes` are necessarily in all + /// ranges of `rperms`, because `rperms` 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, + /// Maps a tag 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 initialisation as for `perms` + pub wildcard_accesses: UniValMap, +} + /// Tree structure with both parents and children since we want to be /// able to traverse the tree efficiently in both directions. #[derive(Clone, Debug)] @@ -225,16 +248,8 @@ pub struct Tree { pub(super) tag_mapping: UniKeyMap, /// All nodes of this tree. pub(super) nodes: UniValMap, - /// Maps a tag and a location to a perm, with possible lazy - /// initialization. - /// - /// NOTE: not all tags registered in `nodes` are necessarily in all - /// ranges of `rperms`, because `rperms` 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(super) rperms: DedupRangeMap>, + /// Associates with each location its state and wildcard access tracking. + pub(super) locations: DedupRangeMap, /// The index of the root node. pub(super) root: UniIndex, } @@ -260,6 +275,8 @@ pub(super) struct Node { /// 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, } @@ -292,6 +309,7 @@ struct TreeVisitor<'tree> { tag_mapping: &'tree UniKeyMap, nodes: &'tree mut UniValMap, perms: &'tree mut UniValMap, + wildcard_accesses: Option<&'tree mut UniValMap>, } /// Whether to continue exploring the children recursively or not. @@ -339,7 +357,7 @@ impl TreeVisitorStack where NodeContinue: Fn(&NodeAppArgs<'_>) -> ContinueTraversal, - NodeApp: Fn(NodeAppArgs<'_>) -> Result<(), InnErr>, + NodeApp: Fn(UniIndex, AccessRelatedness, &mut TreeVisitor<'_>) -> Result<(), InnErr>, ErrHandler: Fn(ErrHandlerArgs<'_, InnErr>) -> OutErr, { fn should_continue_at( @@ -359,16 +377,13 @@ where idx: UniIndex, rel_pos: AccessRelatedness, ) -> Result<(), OutErr> { - let node = this.nodes.get_mut(idx).unwrap(); - (self.f_propagate)(NodeAppArgs { node, perm: this.perms.entry(idx), rel_pos }).map_err( - |error_kind| { - (self.err_builder)(ErrHandlerArgs { - error_kind, - conflicting_info: &this.nodes.get(idx).unwrap().debug_info, - accessed_info: &this.nodes.get(self.initial).unwrap().debug_info, - }) - }, - ) + (self.f_propagate)(idx, rel_pos, this).map_err(|error_kind| { + (self.err_builder)(ErrHandlerArgs { + error_kind, + conflicting_info: &this.nodes.get(idx).unwrap().debug_info, + accessed_info: &this.nodes.get(self.initial).unwrap().debug_info, + }) + }) } fn go_upwards_from_accessed( @@ -508,11 +523,28 @@ impl<'tree> TreeVisitor<'tree> { /// diagnostics. It also affects tree traversal optimizations built on top of this, so /// those need to be reviewed carefully as well whenever this changes. fn traverse_this_parents_children_other( - mut self, + self, start: BorTag, f_continue: impl Fn(&NodeAppArgs<'_>) -> ContinueTraversal, f_propagate: impl Fn(NodeAppArgs<'_>) -> Result<(), InnErr>, err_builder: impl Fn(ErrHandlerArgs<'_, InnErr>) -> OutErr, + ) -> Result<(), OutErr> { + let f_propagate = |idx, rel_pos, this: &mut TreeVisitor<'_>| { + let node = this.nodes.get_mut(idx).unwrap(); + let perm = this.perms.entry(idx); + f_propagate(NodeAppArgs { node, perm, rel_pos }) + }; + self.traverse_this_parents_children_other_raw(start, f_continue, f_propagate, err_builder) + } + + /// The same as `traverse_this_parents_children_other`, but gives access to the full visitor + /// inside the `f_propagate` callback. + fn traverse_this_parents_children_other_raw( + mut self, + start: BorTag, + f_continue: impl Fn(&NodeAppArgs<'_>) -> ContinueTraversal, + f_propagate: impl Fn(UniIndex, AccessRelatedness, &mut TreeVisitor<'_>) -> Result<(), InnErr>, + err_builder: impl Fn(ErrHandlerArgs<'_, InnErr>) -> OutErr, ) -> Result<(), OutErr> { let start_idx = self.tag_mapping.get(&start).unwrap(); let mut stack = TreeVisitorStack::new(start_idx, f_continue, f_propagate, err_builder); @@ -531,12 +563,12 @@ impl<'tree> TreeVisitor<'tree> { stack.finish_foreign_accesses(&mut self) } - /// Like `traverse_this_parents_children_other`, but skips the children of `start`. - fn traverse_nonchildren( + /// Like `traverse_this_parents_children_other_raw`, but skips the children of `start`. + fn traverse_nonchildren_raw( mut self, start: BorTag, f_continue: impl Fn(&NodeAppArgs<'_>) -> ContinueTraversal, - f_propagate: impl Fn(NodeAppArgs<'_>) -> Result<(), InnErr>, + f_propagate: impl Fn(UniIndex, AccessRelatedness, &mut TreeVisitor<'_>) -> Result<(), InnErr>, err_builder: impl Fn(ErrHandlerArgs<'_, InnErr>) -> OutErr, ) -> Result<(), OutErr> { let start_idx = self.tag_mapping.get(&start).unwrap(); @@ -578,6 +610,7 @@ impl Tree { 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, }, ); @@ -596,9 +629,10 @@ impl Tree { IdempotentForeignAccess::None, ), ); - DedupRangeMap::new(size, perms) + let wildcard_accesses = UniValMap::default(); + DedupRangeMap::new(size, LocationTree { perms, wildcard_accesses }) }; - Self { root: root_idx, nodes, rperms, tag_mapping } + Self { root: root_idx, nodes, locations: rperms, tag_mapping } } } @@ -634,11 +668,13 @@ impl<'tcx> Tree { 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), }, ); + let parent_node = self.nodes.get_mut(parent_idx).unwrap(); // Register new_tag as a child of parent_tag - self.nodes.get_mut(parent_idx).unwrap().children.push(idx); + parent_node.children.push(idx); // We need to know the weakest SIFA for `update_idempotent_foreign_access_after_retag`. let mut min_sifa = default_strongest_idempotent; @@ -652,11 +688,19 @@ impl<'tcx> Tree { ); min_sifa = cmp::min(min_sifa, perm.idempotent_foreign_access); - for (_perms_range, perms) in self - .rperms + for (_, loc) in self + .locations .iter_mut(Size::from_bytes(start) + base_offset, Size::from_bytes(end - start)) { - perms.insert(idx, perm); + 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. + for (_, loc) in self.locations.iter_mut_all() { + if let Some(parent_access) = loc.wildcard_accesses.get(parent_idx) { + loc.wildcard_accesses.insert(idx, parent_access.get_new_child()); } } @@ -690,9 +734,9 @@ impl<'tcx> Tree { // 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 (_, map) in self.rperms.iter_mut_all() { + for (_, 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) = map.get_mut(current) { + 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); @@ -721,55 +765,65 @@ impl<'tcx> Tree { /// - the absence of Strong Protectors anywhere in the allocation pub fn dealloc( &mut self, - tag: BorTag, + prov: ProvenanceExtra, access_range: AllocRange, global: &GlobalState, alloc_id: AllocId, // diagnostics span: Span, // diagnostics ) -> InterpResult<'tcx> { self.perform_access( - tag, + prov, Some((access_range, AccessKind::Write, diagnostics::AccessCause::Dealloc)), global, alloc_id, span, )?; - for (perms_range, perms) in self.rperms.iter_mut(access_range.start, access_range.size) { - TreeVisitor { nodes: &mut self.nodes, tag_mapping: &self.tag_mapping, perms } - .traverse_this_parents_children_other( - tag, - // visit all children, skipping none - |_| ContinueTraversal::Recurse, - |args: NodeAppArgs<'_>| -> Result<(), TransitionError> { - let NodeAppArgs { node, perm, .. } = args; - let perm = - perm.get().copied().unwrap_or_else(|| node.default_location_state()); - if global.borrow().protected_tags.get(&node.tag) + for (loc_range, loc) in self.locations.iter_mut(access_range.start, access_range.size) { + let start_tag = match prov { + ProvenanceExtra::Concrete(tag) => tag, + // The order in which we check if any nodes are invalidated doesn't matter, + // so we use the root as a default tag. + ProvenanceExtra::Wildcard => self.nodes.get(self.root).unwrap().tag, + }; + TreeVisitor { + nodes: &mut self.nodes, + tag_mapping: &self.tag_mapping, + perms: &mut loc.perms, + wildcard_accesses: None, + } + .traverse_this_parents_children_other( + start_tag, + // visit all children, skipping none + |_| ContinueTraversal::Recurse, + |args: NodeAppArgs<'_>| -> Result<(), TransitionError> { + let NodeAppArgs { node, perm, .. } = args; + let perm = perm.get().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(TransitionError::ProtectedDealloc) - } else { - Ok(()) - } - }, - |args: ErrHandlerArgs<'_, TransitionError>| -> InterpErrorKind<'tcx> { - let ErrHandlerArgs { error_kind, conflicting_info, accessed_info } = args; - TbError { - conflicting_info, - access_cause: diagnostics::AccessCause::Dealloc, - alloc_id, - error_offset: perms_range.start, - error_kind, - accessed_info, - } - .build() - }, - )?; + { + Err(TransitionError::ProtectedDealloc) + } else { + Ok(()) + } + }, + |args: ErrHandlerArgs<'_, TransitionError>| -> InterpErrorKind<'tcx> { + let ErrHandlerArgs { error_kind, conflicting_info, accessed_info } = args; + TbError { + conflicting_info, + access_cause: diagnostics::AccessCause::Dealloc, + alloc_id, + error_offset: loc_range.start, + error_kind, + accessed_info: Some(accessed_info), + } + .build() + }, + )?; } interp_ok(()) } @@ -794,13 +848,18 @@ impl<'tcx> Tree { /// - recording the history. pub fn perform_access( &mut self, - tag: BorTag, + prov: ProvenanceExtra, access_range_and_kind: Option<(AllocRange, AccessKind, diagnostics::AccessCause)>, global: &GlobalState, alloc_id: AllocId, // diagnostics span: Span, // diagnostics ) -> InterpResult<'tcx> { use std::ops::Range; + + let ProvenanceExtra::Concrete(tag) = prov else { + return self.perform_wildcard_access(access_range_and_kind, global, alloc_id, span); + }; + // Performs the per-node work: // - insert the permission if it does not exist // - perform the access @@ -820,19 +879,21 @@ impl<'tcx> Tree { let node_app = |perms_range: Range, access_kind: AccessKind, access_cause: diagnostics::AccessCause, - args: NodeAppArgs<'_>| + idx: UniIndex, + rel_pos: AccessRelatedness, + this: &mut TreeVisitor<'_>| -> Result<(), TransitionError> { - let NodeAppArgs { node, mut perm, rel_pos } = args; - - let old_state = perm.or_insert(node.default_location_state()); + let node = this.nodes.get_mut(idx).unwrap(); + let mut perm = this.perms.entry(idx); + let state = perm.or_insert(node.default_location_state()); // Call this function now, which ensures it is only called when // `skip_if_known_noop` returns `Recurse`, due to the contract of // `traverse_this_parents_children_other`. - old_state.record_new_access(access_kind, rel_pos); + state.record_new_access(access_kind, rel_pos); let protected = global.borrow().protected_tags.contains_key(&node.tag); - let transition = old_state.perform_access(access_kind, rel_pos, protected)?; + let transition = state.perform_access(access_kind, rel_pos, protected)?; // Record the event as part of the history if !transition.is_noop() { node.debug_info.history.push(diagnostics::Event { @@ -843,6 +904,23 @@ impl<'tcx> Tree { transition_range: perms_range, span, }); + + // We need to update the wildcard access tracking information, + // if the permission of an exposed pointer changes. + if node.is_exposed { + let wildcard_accesses = this.wildcard_accesses.as_deref_mut().unwrap(); + //Protected doesnt change during access. + let access_type = state.permission.strongest_allowed_child_access(protected); + WildcardState::update_exposure(idx, access_type, this.nodes, wildcard_accesses); + #[cfg(feature = "expensive-consistency-checks")] + WildcardState::verify_external_consistency( + idx, + this.nodes, + this.perms, + wildcard_accesses, + &global.borrow().protected_tags, + ); + } } Ok(()) }; @@ -860,7 +938,7 @@ impl<'tcx> Tree { alloc_id, error_offset: perms_range.start, error_kind, - accessed_info, + accessed_info: Some(accessed_info), } .build() }; @@ -868,15 +946,21 @@ impl<'tcx> Tree { if let Some((access_range, access_kind, access_cause)) = access_range_and_kind { // Default branch: this is a "normal" access through a known range. // We iterate over affected locations and traverse the tree for each of them. - for (perms_range, perms) in self.rperms.iter_mut(access_range.start, access_range.size) - { - TreeVisitor { nodes: &mut self.nodes, tag_mapping: &self.tag_mapping, perms } - .traverse_this_parents_children_other( - tag, - |args| node_skipper(access_kind, args), - |args| node_app(perms_range.clone(), access_kind, access_cause, args), - |args| err_handler(perms_range.clone(), access_cause, args), - )?; + for (loc_range, loc) in self.locations.iter_mut(access_range.start, access_range.size) { + TreeVisitor { + nodes: &mut self.nodes, + tag_mapping: &self.tag_mapping, + perms: &mut loc.perms, + wildcard_accesses: Some(&mut loc.wildcard_accesses), + } + .traverse_this_parents_children_other_raw( + tag, + |args| node_skipper(access_kind, args), + |idx, rel_pos, this| { + node_app(loc_range.clone(), access_kind, access_cause, idx, rel_pos, this) + }, + |args| err_handler(loc_range.clone(), access_cause, args), + )?; } } else { // This is a special access through the entire allocation. @@ -888,20 +972,34 @@ impl<'tcx> Tree { // 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 (perms_range, perms) in self.rperms.iter_mut_all() { + for (loc_range, loc) in self.locations.iter_mut_all() { let idx = self.tag_mapping.get(&tag).unwrap(); // Only visit accessed permissions - if let Some(p) = perms.get(idx) + if let Some(p) = loc.perms.get(idx) && let Some(access_kind) = p.permission.protector_end_access() && p.accessed { let access_cause = diagnostics::AccessCause::FnExit(access_kind); - TreeVisitor { nodes: &mut self.nodes, tag_mapping: &self.tag_mapping, perms } - .traverse_nonchildren( + TreeVisitor { + nodes: &mut self.nodes, + tag_mapping: &self.tag_mapping, + perms: &mut loc.perms, + wildcard_accesses: Some(&mut loc.wildcard_accesses), + } + .traverse_nonchildren_raw( tag, |args| node_skipper(access_kind, args), - |args| node_app(perms_range.clone(), access_kind, access_cause, args), - |args| err_handler(perms_range.clone(), access_cause, args), + |idx, rel_pos, this| { + node_app( + loc_range.clone(), + access_kind, + access_cause, + idx, + rel_pos, + this, + ) + }, + |args| err_handler(loc_range.clone(), access_cause, args), )?; } } @@ -918,7 +1016,7 @@ impl Tree { // 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.rperms.merge_adjacent_thorough(); + self.locations.merge_adjacent_thorough(); } /// Checks if a node is useless and should be GC'ed. @@ -950,10 +1048,14 @@ impl Tree { 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 (_, data) in self.rperms.iter_all() { - let parent_perm = - data.get(idx).map(|x| x.permission).unwrap_or_else(|| node.default_initial_perm); - let child_perm = data + for (_, 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); @@ -977,8 +1079,9 @@ impl Tree { // before we can safely apply `UniKeyMap::remove` to truly remove // this tag from the `tag_mapping`. let node = self.nodes.remove(this).unwrap(); - for (_perms_range, perms) in self.rperms.iter_mut_all() { - perms.remove(this); + for (_, loc) in self.locations.iter_mut_all() { + loc.perms.remove(this); + loc.wildcard_accesses.remove(this); } self.tag_mapping.remove(&node.tag); } @@ -1055,6 +1158,136 @@ impl Tree { } } +/// Methods for wildcard borrows. +impl<'tcx> Tree { + /// Analogous to `perform_access`, but we do not know from which exposed reference the access happens. + pub fn perform_wildcard_access( + &mut self, + access_range_and_kind: Option<(AllocRange, AccessKind, diagnostics::AccessCause)>, + global: &GlobalState, + alloc_id: AllocId, // diagnostics + span: Span, // diagnostics + ) -> InterpResult<'tcx> { + if let Some((access_range, access_kind, access_cause)) = access_range_and_kind { + for (loc_range, loc) in self.locations.iter_mut(access_range.start, access_range.size) { + // We can simply iterate over the entire graph in arbitrary order, as the + // relatedness of the access for each node can be calculated with only + // the tracking information attached to each node. + let mut stack = vec![self.root]; + while let Some(id) = stack.pop() { + let node = self.nodes.get_mut(id).unwrap(); + let mut entry = loc.perms.entry(id); + let perm = entry.or_insert(node.default_location_state()); + // TODO narrowing based on protector + let protected = global.borrow().protected_tags.contains_key(&node.tag); + let mut entry = loc.wildcard_accesses.entry(id); + let wildcard_access = entry.or_insert(Default::default()); + + let Some(wildcard_relatedness) = + wildcard_access.access_relatedness(access_kind) + else { + // There doesn't exist a valid exposed reference for this access + // to happen through. + // If this fails for one id, then it fails for all ids. + return Err(no_valid_exposed_references_error( + alloc_id, + loc_range.start, + access_cause, + )) + .into(); + }; + if let Some(relatedness) = wildcard_relatedness.to_relatedness() + && matches!( + perm.skip_if_known_noop(access_kind, relatedness), + ContinueTraversal::SkipSelfAndChildren + ) + { + continue; + } + + // Add children to stack. + stack.extend(node.children.iter().copied()); + 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 child. + continue; + }; + + // Update idempototent foreign access data. + if matches!( + perm.skip_if_known_noop(access_kind, relatedness), + ContinueTraversal::Recurse + ) { + perm.record_new_access(access_kind, relatedness); + } + + let transition = perm + .perform_access(access_kind, relatedness, protected) + .map_err(|trans| { + // If another pointer undergoes an invalid transformation, return an error. + TbError { + conflicting_info: &node.debug_info, + access_cause, + alloc_id, + error_offset: loc_range.start, + error_kind: trans, + accessed_info: None, + } + .build() + })?; + // Record the event as part of the history. + if !transition.is_noop() { + node.debug_info.history.push(diagnostics::Event { + transition, + is_foreign: relatedness.is_foreign(), + access_cause, + access_range: access_range_and_kind.map(|x| x.0), + transition_range: loc_range.clone(), + span, + }); + + // We need to update the wildcard access tracking information, + // if the permission of an exposed pointer changes. + if node.is_exposed { + let access_type = + perm.permission.strongest_allowed_child_access(protected); + WildcardState::update_exposure( + id, + access_type, + &self.nodes, + &mut loc.wildcard_accesses, + ); + #[cfg(feature = "expensive-consistency-checks")] + WildcardState::verify_external_consistency( + id, + &self.nodes, + &loc.perms, + &loc.wildcard_accesses, + &global.borrow().protected_tags, + ); + } + } + } + } + } else { + // 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. + // + // TODO currently we do not assign protectors to wildcard references, + // so this codepath never gets called + todo!() + }; + interp_ok(()) + } +} + impl Node { pub fn default_location_state(&self) -> LocationState { LocationState::new_non_accessed( @@ -1068,7 +1301,11 @@ impl VisitProvenance for Tree { fn visit_provenance(&self, visit: &mut VisitWith<'_>) { // To ensure that the root never gets removed, we visit it // (the `root` node of `Tree` is not an `Option<_>`) - visit(None, Some(self.nodes.get(self.root).unwrap().tag)) + visit(None, Some(self.nodes.get(self.root).unwrap().tag)); + + // We also need to keep around any exposed tags through which + // an access could still happen. + self.visit_wildcards(visit); } } @@ -1087,12 +1324,21 @@ pub enum AccessRelatedness { /// The accessed pointer is neither of the above. // It's a cousin/uncle/etc., something in a side branch. CousinAccess, + /// We do not know the accessed pointer, but we know that it is a child of this pointer. + WildcardChildAccess, + /// We do not know the accessed pointer, but we know that it is foreign to this pointer. + WildcardForeignAccess, } 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::AncestorAccess | AccessRelatedness::CousinAccess) + matches!( + self, + AccessRelatedness::AncestorAccess + | AccessRelatedness::CousinAccess + | AccessRelatedness::WildcardForeignAccess + ) } } diff --git a/src/borrow_tracker/tree_borrows/unimap.rs b/src/borrow_tracker/tree_borrows/unimap.rs index ad0a565dfd..15616501f0 100644 --- a/src/borrow_tracker/tree_borrows/unimap.rs +++ b/src/borrow_tracker/tree_borrows/unimap.rs @@ -12,6 +12,7 @@ #![allow(dead_code)] +use std::fmt::Debug; use std::hash::Hash; use std::mem; @@ -20,10 +21,15 @@ use rustc_data_structures::fx::FxHashMap; use crate::helpers::ToUsize; /// Intermediate key between a UniKeyMap and a UniValMap. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct UniIndex { idx: u32, } +impl Debug for UniIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.idx.fmt(f) + } +} /// From K to UniIndex #[derive(Debug, Clone, Default)] @@ -201,6 +207,12 @@ impl UniValMap { mem::swap(&mut res, &mut self.data[idx.idx.to_usize()]); res } + + /// Weather the datastructure has been allocated. + /// Does not mean the map contains any values. + pub fn is_initialized(&self) -> bool { + self.data.capacity() != 0 + } } /// An access to a single value of the map. diff --git a/src/borrow_tracker/tree_borrows/wildcard.rs b/src/borrow_tracker/tree_borrows/wildcard.rs new file mode 100644 index 0000000000..d9a5c3361d --- /dev/null +++ b/src/borrow_tracker/tree_borrows/wildcard.rs @@ -0,0 +1,592 @@ +use std::cmp::max; +use std::fmt::Debug; + +#[cfg(feature = "expensive-consistency-checks")] +use { + super::LocationState, crate::borrow_tracker::ProtectorKind, + rustc_data_structures::fx::FxHashMap, +}; + +use super::Tree; +use super::tree::{AccessRelatedness, Node}; +use super::unimap::{UniIndex, UniValMap}; +use crate::borrow_tracker::GlobalState; +use crate::{AccessKind, BorTag, VisitWith}; + +/// Represensts the maximum access level that is possible. +/// +/// Note that we derive Ord and PartialOrd, so the order in which variants are listed below matters: +/// None < Read < Write. Do not change that order. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +pub enum WildcardAccessLevel { + #[default] + None, + Read, + Write, +} + +/// Were relative to the node the access happened from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WildcardAccessRelatedness { + /// The access definitively happened through a local node. + LocalAccess, + /// The access definitively happened through a foreign node. + ForeignAccess, + /// We do not know if the access is foreign or local. + EitherAccess, +} +impl WildcardAccessRelatedness { + pub fn to_relatedness(self) -> Option { + match self { + Self::LocalAccess => Some(AccessRelatedness::WildcardChildAccess), + Self::ForeignAccess => Some(AccessRelatedness::WildcardForeignAccess), + Self::EitherAccess => None, + } + } +} + +/// State per location per node keeping track of where relative to this +/// node exposed nodes are and what access permissions they have. +/// +/// Designed to be completely determined by its parent, siblings and +/// direct children's max_local_access/max_foreign_access. +#[derive(Clone, Default, PartialEq, Eq)] +pub struct WildcardState { + /// How many of this node's direct children have `max_local_access()==Write`. + child_writes: u16, + /// How many of this node's direct children have `max_local_access()>=Read`. + child_reads: u16, + /// The maximum access level that could happen from an exposed node + /// that is foreign to this node. + /// + /// This is calculated as the `max()` of parents `max_foreign_access`, + /// `exposed_as` and its siblings `max_local_access()`. + max_foreign_access: WildcardAccessLevel, + /// At what access level this node itself is exposed. + exposed_as: WildcardAccessLevel, +} +impl Debug for WildcardState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WildcardState") + .field("child_r/w", &(self.child_reads, self.child_writes)) + .field("foreign", &self.max_foreign_access) + .field("exposed_as", &self.exposed_as) + .finish() + } +} +impl WildcardState { + /// The maximum access level that could happen from an exposed + /// node that is a local to this node. + pub fn max_local_access(&self) -> WildcardAccessLevel { + use WildcardAccessLevel::*; + max( + self.exposed_as, + if self.child_writes > 0 { + Write + } else if self.child_reads > 0 { + Read + } else { + None + }, + ) + } + + /// From where relative to the node with this wildcard info a read or write access could happen. + pub fn access_relatedness(&self, kind: AccessKind) -> Option { + match kind { + AccessKind::Read => self.read_access_relatedness(), + AccessKind::Write => self.write_access_relatedness(), + } + } + + /// From where relative to the node with this wildcard info a read access could happen. + pub fn read_access_relatedness(&self) -> Option { + let has_foreign = self.max_foreign_access >= WildcardAccessLevel::Read; + let has_local = self.max_local_access() >= WildcardAccessLevel::Read; + use WildcardAccessRelatedness as E; + match (has_foreign, has_local) { + (true, true) => Some(E::EitherAccess), + (true, false) => Some(E::ForeignAccess), + (false, true) => Some(E::LocalAccess), + (false, false) => None, + } + } + + /// From where relative to the node with this wildcard info a write access could happen. + pub fn write_access_relatedness(&self) -> Option { + let has_foreign = self.max_foreign_access == WildcardAccessLevel::Write; + let has_local = self.max_local_access() == WildcardAccessLevel::Write; + use WildcardAccessRelatedness as E; + match (has_foreign, has_local) { + (true, true) => Some(E::EitherAccess), + (true, false) => Some(E::ForeignAccess), + (false, true) => Some(E::LocalAccess), + (false, false) => None, + } + } + + /// Gets the access tracking information for a new child node of a parent with this + /// wildcard info. + /// The new node doesn't have any child reads/writes, but calculates max_foreign_access + /// from its parent. + pub fn get_new_child(&self) -> Self { + Self { + max_foreign_access: max(self.max_foreign_access, self.max_local_access()), + ..Default::default() + } + } + + /// Pushes the nodes of `children` onto the stack who's `max_foreign_access` + /// needs to be updated. + /// + /// * `children`: A list of nodes with the same parent. `children` doesn't + /// necessarily have to contain all children of parent, but can just be + /// a subset. + /// + /// * `child_reads`, `child_writes`: How many of `children` have `max_local_access()` + /// of at least `read`/`write` + /// + /// * `new_foreign_access`, `old_foreign_access`: + /// The max possible access level, that is foreign to all `children`. + /// This can be calculated as the max of the parent's `exposed_as()`, `max_foreign_access` + /// and of all `max_local_access()` of any nodes with the same parent, that are + /// not listed in `children`. + /// + /// This access level changed from `old` to `new`, which is why we need to + /// update `children`. + fn push_relevant_children( + stack: &mut Vec<(UniIndex, WildcardAccessLevel)>, + new_foreign_access: WildcardAccessLevel, + old_foreign_access: WildcardAccessLevel, + child_reads: u16, + child_writes: u16, + children: impl Iterator, + + wildcard_accesses: &UniValMap, + ) { + use WildcardAccessLevel::*; + + // Nothing changed so we dont need to update anything. + if new_foreign_access == old_foreign_access { + return; + } + + // We need to consider that the children's `max_local_access()` affect each + // others `max_foreign_access`, but do not affect their own `max_foreign_access`. + + // The new `max_foreign_acces` for children with `max_locala_access()==Write`. + let write_foreign_access = max( + new_foreign_access, + if child_writes > 1 { + // There exists at least one more child with exposed write access. + // This means that a foreign write through that node is possible. + Write + } else if child_reads > 1 { + // There exists at least one more child with exposed read access, + // but no other with write access. + // This means that a foreign read but no write through that node + // is possible. + Read + } else { + // There are no other nodes with read or write access. + // This means no foreign writes through other children are possible. + None + }, + ); + + // The new `max_foreign_acces` for children with `max_local_access()==Read`. + let read_foreign_access = max( + new_foreign_access, + if child_writes > 0 { + // There exists at least one child with write access (and it's not this one). + Write + } else if child_reads > 1 { + // There exists at least one more child with exposed read access, + // but no other with write access. + Read + } else { + // There are no other nodes with read or write access, + None + }, + ); + + // The new `max_foreign_acces` for children with `max_local_access()==None`. + let none_foreign_access = max( + new_foreign_access, + if child_writes > 0 { + // There exists at least one child with write access (and it's not this one). + Write + } else if child_reads > 0 { + // There exists at least one child with read access (and it's not this one), + // but none with write access. + Read + } else { + // No children are exposed as read or write. + None + }, + ); + + stack.extend(children.filter_map(|child| { + let state = wildcard_accesses.get(child).cloned().unwrap_or_default(); + + let new_foreign_access = match state.max_local_access() { + Write => write_foreign_access, + Read => read_foreign_access, + None => none_foreign_access, + }; + + if new_foreign_access != state.max_foreign_access { + Some((child, new_foreign_access)) + } else { + Option::None + } + })); + } + + /// Update the tracking information of a tree, to reflect that the node specified by `id` is + /// now exposed with `new_exposed_as`. + /// + /// Propagates the Willard access information over the tree. This needs to be called every + /// time the access level of an exposed node changes, to keep the state in sync with + /// the rest of the tree. + pub fn update_exposure( + id: UniIndex, + new_exposed_as: WildcardAccessLevel, + nodes: &UniValMap, + wildcard_accesses: &mut UniValMap, + ) { + let mut entry = wildcard_accesses.entry(id); + let src_state = entry.or_insert(Default::default()); + let old_exposed_as = src_state.exposed_as; + + // If the exposure doesn't change, then we don't need to update anything. + if old_exposed_as == new_exposed_as { + return; + } + // Whether we are upgrading or downgrading the allowed access rights. + let is_upgrade = old_exposed_as < new_exposed_as; + + let src_old_local_access = src_state.max_local_access(); + + src_state.exposed_as = new_exposed_as; + + let src_new_local_access = src_state.max_local_access(); + + // Stack of nodes for which the max_foreign_access field needs to be updated. + let mut stack: Vec<(UniIndex, WildcardAccessLevel)> = Vec::new(); + + // Update the direct children of this node. + { + let node = nodes.get(id).unwrap(); + Self::push_relevant_children( + &mut stack, + max(src_state.max_foreign_access, new_exposed_as), + max(src_state.max_foreign_access, old_exposed_as), + src_state.child_reads, + src_state.child_writes, + node.children.iter().copied(), + wildcard_accesses, + ); + } + // We need to propagate the tracking info up the tree, for this we traverse up the parents. + // We can skip propagating info to the parent and siblings of a node if its access didn't change. + { + // The child from which we came from. + let mut child = id; + // This is the `max_local_access()` of the child we came from, before this update... + let mut old_child_access = src_old_local_access; + // and after this update. + let mut new_child_access = src_new_local_access; + while let Some(parent_id) = nodes.get(child).unwrap().parent { + let parent_node = nodes.get(parent_id).unwrap(); + let mut entry = wildcard_accesses.entry(parent_id); + let parent_state = entry.or_insert(Default::default()); + + let old_parent_state = parent_state.clone(); + use WildcardAccessLevel::*; + // Updating this node's tracking state for its children. + if is_upgrade { + if new_child_access == Write { + // None -> Write + // Read -> Write + parent_state.child_writes += 1; + } + if old_child_access == None { + // None -> Read + // None -> Write + parent_state.child_reads += 1; + } + } else { + if old_child_access == Write { + // Write -> None + // Write -> Read + parent_state.child_writes -= 1; + } + if new_child_access == None { + // Read -> None + // Write -> None + parent_state.child_reads -= 1; + } + } + + let old_parent_local_access = old_parent_state.max_local_access(); + let new_parent_local_access = parent_state.max_local_access(); + + { + // We need to update the `max_foreign_access` of `child`'s siblings. + // For this we can reuse the `push_relevant_children` function. + // + // We pass it just the siblings without child itself. Since `child`'s + // `max_local_access()` is foreign to all of its siblings we can pass + // it as part of the foreign access. + + // it doesnt matter wether we read these from old or new + let constant_factors = + max(parent_state.exposed_as, parent_state.max_foreign_access); + + // `state` contains the correct child_writes/reads counts for just the + // siblings excluding `child`. + let state = if is_upgrade { &old_parent_state } else { parent_state }; + + Self::push_relevant_children( + &mut stack, + max(constant_factors, new_child_access), + max(constant_factors, old_child_access), + state.child_reads, + state.child_writes, + parent_node.children.iter().copied().filter(|id| child != *id), + wildcard_accesses, + ); + } + if old_parent_local_access == new_parent_local_access { + // We didn't change `max_local_access()` for parent, so we don't need to propagate further upwards. + break; + } + + old_child_access = old_parent_local_access; + new_child_access = new_parent_local_access; + child = parent_id; + } + } + // Traverses up the tree to update max_foreign_access fields of children and cousins who need to be updated. + while let Some((id, new_access)) = stack.pop() { + let node = nodes.get(id).unwrap(); + let mut entry = wildcard_accesses.entry(id); + let state = entry.or_insert(Default::default()); + + let old_access = state.max_foreign_access; + state.max_foreign_access = new_access; + + Self::push_relevant_children( + &mut stack, + max(state.exposed_as, new_access), + max(state.exposed_as, old_access), + state.child_reads, + state.child_writes, + node.children.iter().copied(), + wildcard_accesses, + ); + } + + // Calling `update_exposure()` should always result in an internally consistent state. + #[cfg(feature = "expensive-consistency-checks")] + Self::verify_internal_consistency(id, nodes, wildcard_accesses); + } +} + +impl Tree { + /// Marks the tag as exposed & updates the wildcard tracking data structure + /// to represent its access level. + pub fn expose_tag(&mut self, tag: BorTag, global: &GlobalState) { + let id = self.tag_mapping.get(&tag).unwrap(); + let node = self.nodes.get_mut(id).unwrap(); + node.is_exposed = true; + let node = self.nodes.get(id).unwrap(); + let protected_tags = &global.borrow().protected_tags; + let protected = protected_tags.contains_key(&tag); + // TODO: Only initialize neccessary ranges. + for (_, loc) in self.locations.iter_mut_all() { + let perm = *loc.perms.entry(id).or_insert(node.default_location_state()); + + let access_type = perm.permission().strongest_allowed_child_access(protected); + WildcardState::update_exposure( + id, + access_type, + &self.nodes, + &mut loc.wildcard_accesses, + ); + } + } + pub(super) fn visit_wildcards(&self, visit: &mut VisitWith<'_>) { + for (_, loc) in self.locations.iter_all() { + let mut stack = vec![self.root]; + let mut has_exposed = false; + while let Some(id) = stack.pop() { + let node = self.nodes.get(id).unwrap(); + + if node.is_exposed + && let Some(perm) = loc.perms.get(id) + { + // If the node is already protected, then it gets already visited + // through the protector. So we can assume `protected=false`. + let access_level = perm.permission().strongest_allowed_child_access(false); + if access_level != WildcardAccessLevel::None { + visit(None, Some(node.tag)) + } + } + has_exposed |= node.is_exposed; + stack.extend(&node.children); + } + // If there are no exposed nodes, then we don't need to check other + // locations of this allocation. + if !has_exposed { + break; + } + } + } +} + +#[cfg(feature = "expensive-consistency-checks")] +impl Tree { + /// Checks that the wildcard tracking datastructure is internally consistent and has + /// the correct `exposed_as` values. + pub fn verify_wildcard_consistency(&self, global: &GlobalState) { + for (_, loc) in self.locations.iter_all() { + WildcardState::verify_internal_consistency( + self.root, + &self.nodes, + &loc.wildcard_accesses, + ); + + WildcardState::verify_external_consistency( + self.root, + &self.nodes, + &loc.perms, + &loc.wildcard_accesses, + &global.borrow().protected_tags, + ); + } + } +} + +#[cfg(feature = "expensive-consistency-checks")] +impl WildcardState { + /// Verifies that the access tracking state is self consistent. + /// + /// Panics if invalid. + pub fn verify_internal_consistency( + id: UniIndex, + nodes: &UniValMap, + wildcard_accesses: &UniValMap, + ) { + // Find the root node. + let mut root = id; + while let Some(parent) = nodes.get(root).unwrap().parent { + root = parent; + } + + // Checks if accesses is empty. + if !wildcard_accesses.is_initialized() { + return; + } + + let mut stack = vec![root]; + while let Some(id) = stack.pop() { + let node = nodes.get(id).unwrap(); + stack.extend(node.children.iter()); + + let access = wildcard_accesses.get(id).unwrap(); + + let expected_max_foreign_access = if let Some(parent) = node.parent { + let parent_node = nodes.get(parent).unwrap(); + let parent_state = wildcard_accesses.get(parent).unwrap(); + + let max_sibling_access = parent_node + .children + .iter() + .copied() + .filter(|child| *child != id) + .map(|child| { + let state = wildcard_accesses.get(child).unwrap(); + state.max_local_access() + }) + .fold(WildcardAccessLevel::None, max); + + max_sibling_access.max(parent_state.max_foreign_access).max(parent_state.exposed_as) + } else { + WildcardAccessLevel::None + }; + + let child_accesses = node.children.iter().copied().map(|child| { + let state = wildcard_accesses.get(child).unwrap(); + state.max_local_access() + }); + let expected_child_reads = + child_accesses.clone().filter(|a| *a >= WildcardAccessLevel::Read).count(); + let expected_child_writes = + child_accesses.filter(|a| *a >= WildcardAccessLevel::Write).count(); + + assert_eq!( + expected_max_foreign_access, access.max_foreign_access, + "expected {:?}'s (id:{id:?}) max_foreign_access to be {:?} instead of {:?}", + node.tag, expected_max_foreign_access, access.max_foreign_access + ); + let child_reads: usize = access.child_reads.into(); + assert_eq!( + expected_child_reads, child_reads, + "expected {:?}'s (id:{id:?}) child_reads to be {} instead of {}", + node.tag, expected_child_reads, child_reads + ); + let child_writes: usize = access.child_writes.into(); + assert_eq!( + expected_child_writes, child_writes, + "expected {:?}'s (id:{id:?}) child_writes to be {} instead of {}", + node.tag, expected_child_writes, child_writes + ); + } + } + + /// Ensures that the access tracking state is consistent with the rest of the tree. + /// This means that for every node the `exposed_as` field is equal to the strongest + /// possible access through that node. + pub fn verify_external_consistency( + id: UniIndex, + nodes: &UniValMap, + perms: &UniValMap, + wildcard_accesses: &UniValMap, + protected_tags: &FxHashMap, + ) { + // Find the root node. + let mut root = id; + while let Some(parent) = nodes.get(root).unwrap().parent { + root = parent; + } + + let mut stack = vec![root]; + while let Some(id) = stack.pop() { + let node = nodes.get(id).unwrap(); + if node.is_exposed { + let state = wildcard_accesses.get(id).unwrap(); + let perm = perms.get(id).unwrap(); + let access_level = perm + .permission() + .strongest_allowed_child_access(protected_tags.contains_key(&node.tag)); + assert_eq!( + access_level, state.exposed_as, + "tag {:?} (id:{id:?}) should be exposed as {access_level:?} but is exposed as {:?}", + node.tag, state.exposed_as + ); + } else { + if let Some(state) = wildcard_accesses.get(id) + && state.exposed_as != WildcardAccessLevel::None + { + panic!( + "tag {:?} (id:{id:?}) should be exposed as None but is exposed as {:?}", + node.tag, state.exposed_as + ) + } + } + stack.extend(&node.children); + } + } +} diff --git a/tests/fail/tree_borrows/wildcard/activated.rs b/tests/fail/tree_borrows/wildcard/activated.rs new file mode 100644 index 0000000000..e8fd9458db --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/activated.rs @@ -0,0 +1,20 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 0; + + let ref1 = &mut x; + + let int = ref1 as *mut u32 as usize; + let wild = int as *mut u32; + + // Activates ref1 + unsafe { wild.write(41) }; + + // ref2 needs to be created after the write, because otherwise it gets disabled. + // this reborrow causes an implicit read, disabling sibling ref1. + let ref2 = &x; + + unsafe { wild.write(0) }; //~ ERROR: /write access through wildcard at .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/activated.stderr b/tests/fail/tree_borrows/wildcard/activated.stderr new file mode 100644 index 0000000000..dee40e1e46 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/activated.stderr @@ -0,0 +1,16 @@ +error: Undefined Behavior: write access through wildcard at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/activated.rs:LL:CC + | +LL | unsafe { wild.write(0) }; + | ^^^^^^^^^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: there are no exposed references who have write access permissions to this location + = note: BACKTRACE: + = note: inside `main` at tests/fail/tree_borrows/wildcard/activated.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/dealloc.rs b/tests/fail/tree_borrows/wildcard/dealloc.rs new file mode 100644 index 0000000000..f76da44592 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/dealloc.rs @@ -0,0 +1,18 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + use std::alloc::Layout; + let x = unsafe { std::alloc::alloc_zeroed(Layout::new::()) as *mut u32 }; + + let ref1 = unsafe { &mut *x }; + let ref2 = unsafe { &mut *x }; + + let int = ref1 as *mut u32 as usize; + let wild = int as *mut u32; + // Disables ref1 and therefore also wild. + *ref2 = 14; + + // tries to dealloc through a disabled wildcard reference + unsafe { std::alloc::dealloc(wild as *mut u8, Layout::new::()) }; //~ ERROR: /deallocation through wildcard .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/dealloc.stderr b/tests/fail/tree_borrows/wildcard/dealloc.stderr new file mode 100644 index 0000000000..0f4338acc8 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/dealloc.stderr @@ -0,0 +1,16 @@ +error: Undefined Behavior: deallocation through wildcard at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/dealloc.rs:LL:CC + | +LL | unsafe { std::alloc::dealloc(wild as *mut u8, Layout::new::()) }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: there are no exposed references who have deallocation permissions to this location + = note: BACKTRACE: + = note: inside `main` at tests/fail/tree_borrows/wildcard/dealloc.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/gc.rs b/tests/fail/tree_borrows/wildcard/gc.rs new file mode 100644 index 0000000000..2a6c9faf26 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/gc.rs @@ -0,0 +1,20 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[path = "../../../utils/mod.rs"] +mod utils; + +#[allow(unused_variables, unused_assignments)] +fn main() { + let mut x: u32 = 4; + let int = { + let y = &x; + y as *const u32 as usize + }; + // If y wasn't exposed, this would gc it. + utils::run_provenance_gc(); + // This should disable y. + x = 5; + let wild = int as *const u32; + + let fail = unsafe { *wild }; //~ ERROR: /read access through wildcard at .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/gc.stderr b/tests/fail/tree_borrows/wildcard/gc.stderr new file mode 100644 index 0000000000..be96599ebe --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/gc.stderr @@ -0,0 +1,16 @@ +error: Undefined Behavior: read access through wildcard at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/gc.rs:LL:CC + | +LL | let fail = unsafe { *wild }; + | ^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: there are no exposed references who have read access permissions to this location + = note: BACKTRACE: + = note: inside `main` at tests/fail/tree_borrows/wildcard/gc.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/parallel.rs b/tests/fail/tree_borrows/wildcard/parallel.rs new file mode 100644 index 0000000000..7c785b943f --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/parallel.rs @@ -0,0 +1,37 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + let ref3 = unsafe { &mut *ptr_base }; + + // both references get exposed + let int1 = ref1 as *mut u32 as usize; + let int2 = ref2 as *mut u32 as usize; + + let wild = int1 as *mut u32; + + // ┌────────────┐ + // │ │ + // │ ptr_base ├──────────────┬───────────────────┐ + // │ │ │ │ + // └──────┬─────┘ │ │ + // │ │ │ + // │ │ │ + // ▼ ▼ ▼ + // ┌────────────┐ ┌────────────┐ ┌───────────┐ + // │ │ │ │ │ │ + // │ ref1(Res)* │ │ ref2(Res)* │ │ ref3(Res) │ + // │ │ │ │ │ │ + // └────────────┘ └────────────┘ └───────────┘ + + // disables ref1,ref2 + *ref3 = 13; + + // both exposed pointers are disabled so this fails + let fail = unsafe { *wild }; //~ ERROR: /read access through .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/parallel.stderr b/tests/fail/tree_borrows/wildcard/parallel.stderr new file mode 100644 index 0000000000..b0f413764d --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/parallel.stderr @@ -0,0 +1,16 @@ +error: Undefined Behavior: read access through wildcard at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/parallel.rs:LL:CC + | +LL | let fail = unsafe { *wild }; + | ^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: there are no exposed references who have read access permissions to this location + = note: BACKTRACE: + = note: inside `main` at tests/fail/tree_borrows/wildcard/parallel.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/parallel2.rs b/tests/fail/tree_borrows/wildcard/parallel2.rs new file mode 100644 index 0000000000..84a6c2949c --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/parallel2.rs @@ -0,0 +1,37 @@ +//@compile-flags: -Zmiri-tree-borrows + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + let ref3 = unsafe { &mut *ptr_base }; + + // Both references get exposed. + let int1 = ref1 as *mut u32 as usize; + let int2 = ref2 as *mut u32 as usize; + + let wild = int1 as *mut u32; + + // ┌────────────┐ + // │ │ + // │ ptr_base ├──────────────┬───────────────────┐ + // │ │ │ │ + // └──────┬─────┘ │ │ + // │ │ │ + // │ │ │ + // ▼ ▼ ▼ + // ┌────────────┐ ┌────────────┐ ┌───────────┐ + // │ │ │ │ │ │ + // │ ref1(Res)* │ │ ref2(Res)* │ │ ref3(Res) │ + // │ │ │ │ │ │ + // └────────────┘ └────────────┘ └───────────┘ + + // Disables ref3 as both exposed pointers are foreign to it. + unsafe { wild.write(13) }; + + // Fails because ref3 is disabled. + let fail = *ref3; //~ ERROR: /read access through .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/parallel2.stderr b/tests/fail/tree_borrows/wildcard/parallel2.stderr new file mode 100644 index 0000000000..1ffd6da076 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/parallel2.stderr @@ -0,0 +1,41 @@ +warning: integer-to-pointer cast + --> tests/fail/tree_borrows/wildcard/parallel2.rs:LL:CC + | +LL | let wild = int1 as *mut u32; + | ^^^^^^^^^^^^^^^^ integer-to-pointer cast + | + = help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program + = help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation + = help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead + = help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics + = help: Tree Borrows does not support integer-to-pointer casts, so the program is likely to go wrong when this pointer gets used + = note: BACKTRACE: + = note: inside `main` at tests/fail/tree_borrows/wildcard/parallel2.rs:LL:CC + +error: Undefined Behavior: read access through at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/parallel2.rs:LL:CC + | +LL | let fail = *ref3; + | ^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: the accessed tag has state Disabled which forbids this child read access +help: the accessed tag was created here, in the initial state Reserved + --> tests/fail/tree_borrows/wildcard/parallel2.rs:LL:CC + | +LL | let ref3 = unsafe { &mut *ptr_base }; + | ^^^^^^^^^^^^^^ +help: the accessed tag later transitioned to Disabled due to a foreign write access at offsets [0x0..0x4] + --> tests/fail/tree_borrows/wildcard/parallel2.rs:LL:CC + | +LL | unsafe { wild.write(13) }; + | ^^^^^^^^^^^^^^ + = help: this transition corresponds to a loss of read and write permissions + = note: BACKTRACE (of the first span): + = note: inside `main` at tests/fail/tree_borrows/wildcard/parallel2.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error; 1 warning emitted + diff --git a/tests/fail/tree_borrows/wildcard/parallel_frz.rs b/tests/fail/tree_borrows/wildcard/parallel_frz.rs new file mode 100644 index 0000000000..6477a2e528 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/parallel_frz.rs @@ -0,0 +1,36 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &*ptr_base }; + + // Both references get exposed. + let int1 = ref1 as *mut u32 as usize; + let int2 = ref2 as *const u32 as usize; + + let wild = int1 as *mut u32; + + // ┌────────────┐ + // │ │ + // │ ptr_base ├──────────────┐ + // │ │ │ + // └──────┬─────┘ │ + // │ │ + // │ │ + // ▼ ▼ + // ┌────────────┐ ┌────────────┐ + // │ │ │ │ + // │ ref1(Res)* │ │ ref2(Frz)* │ + // │ │ │ │ + // └────────────┘ └────────────┘ + + // Disables ref2 as the only write could happen through ref1. + unsafe { wild.write(13) }; + + // Fails because ref2 is disabled. + let fail = *ref2; //~ ERROR: /read access through .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/parallel_frz.stderr b/tests/fail/tree_borrows/wildcard/parallel_frz.stderr new file mode 100644 index 0000000000..c4265d0212 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/parallel_frz.stderr @@ -0,0 +1,27 @@ +error: Undefined Behavior: read access through at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/parallel_frz.rs:LL:CC + | +LL | let fail = *ref2; + | ^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: the accessed tag has state Disabled which forbids this child read access +help: the accessed tag was created here, in the initial state Frozen + --> tests/fail/tree_borrows/wildcard/parallel_frz.rs:LL:CC + | +LL | let ref2 = unsafe { &*ptr_base }; + | ^^^^^^^^^^ +help: the accessed tag later transitioned to Disabled due to a foreign write access at offsets [0x0..0x4] + --> tests/fail/tree_borrows/wildcard/parallel_frz.rs:LL:CC + | +LL | unsafe { wild.write(13) }; + | ^^^^^^^^^^^^^^ + = help: this transition corresponds to a loss of read permissions + = note: BACKTRACE (of the first span): + = note: inside `main` at tests/fail/tree_borrows/wildcard/parallel_frz.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/protector_conflicted.rs b/tests/fail/tree_borrows/wildcard/protector_conflicted.rs new file mode 100644 index 0000000000..bb174764ce --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/protector_conflicted.rs @@ -0,0 +1,24 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + + let protect = |arg: &mut u32| { + // Expose arg. + let int = arg as *mut u32 as usize; + let wild = int as *mut u32; + + // Does a foreign read to arg marking it as conflicted and making child_writes UB while it's protected. + let _x = *ref2; + + // UB because it tries to write through arg. + unsafe { *wild = 4 }; //~ ERROR: /write access through wildcard at .* is forbidden/ + }; + + protect(ref1); +} diff --git a/tests/fail/tree_borrows/wildcard/protector_conflicted.stderr b/tests/fail/tree_borrows/wildcard/protector_conflicted.stderr new file mode 100644 index 0000000000..d9f172447d --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/protector_conflicted.stderr @@ -0,0 +1,21 @@ +error: Undefined Behavior: write access through wildcard at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/protector_conflicted.rs:LL:CC + | +LL | unsafe { *wild = 4 }; + | ^^^^^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: there are no exposed references who have write access permissions to this location + = note: BACKTRACE: + = note: inside closure at tests/fail/tree_borrows/wildcard/protector_conflicted.rs:LL:CC +note: inside `main` + --> tests/fail/tree_borrows/wildcard/protector_conflicted.rs:LL:CC + | +LL | protect(ref1); + | ^^^^^^^^^^^^^ + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/sequence.rs b/tests/fail/tree_borrows/wildcard/sequence.rs new file mode 100644 index 0000000000..951cefad6e --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/sequence.rs @@ -0,0 +1,61 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ref1 = &mut x; + let int1 = ref1 as *mut u32 as usize; + + let ref2 = &mut *ref1; + + let ref3 = &mut *ref2; + let int3 = ref3 as *mut u32 as usize; + + // Write through ref3 so that all references are active. + *ref3 = 43; + + let wild = int1 as *mut u32; + + // graph TD + // ref1(Res)* --> ref2(Res) --> ref3(Res)* + // + // ┌────────────┐ + // │ │ + // │ ref1(Act)* │ + // │ │ + // └──────┬─────┘ + // │ + // │ + // ▼ + // ┌────────────┐ + // │ │ + // │ ref2(Act) │ + // │ │ + // └──────┬─────┘ + // │ + // │ + // ▼ + // ┌────────────┐ + // │ │ + // │ ref3(Act)* │ + // │ │ + // └────────────┘ + + // Writes through either ref1 or ref3, which is either a child or foreign access to ref2. + unsafe { wild.write(42) }; + + // Reading from ref2 still works, since the previous access could have been through its child. + // This also freezes ref3. + let x = *ref2; + + // We can still write through wild, as there is still the exposed ref1 with write permissions + // under proper exposed provenance, this would be UB as the only tag wild can assume to not + // invalidate ref2 is ref3, which we just invalidated. + // + // This disables ref2, ref3. + unsafe { wild.write(43) }; + + // Fails because ref2 is disabled. + let fail = *ref2; //~ ERROR: /read access through .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/sequence.stderr b/tests/fail/tree_borrows/wildcard/sequence.stderr new file mode 100644 index 0000000000..59e0191e2a --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/sequence.stderr @@ -0,0 +1,27 @@ +error: Undefined Behavior: read access through at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/sequence.rs:LL:CC + | +LL | let fail = *ref2; + | ^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: the accessed tag has state Disabled which forbids this child read access +help: the accessed tag was created here, in the initial state Reserved + --> tests/fail/tree_borrows/wildcard/sequence.rs:LL:CC + | +LL | let ref2 = &mut *ref1; + | ^^^^^^^^^^ +help: the accessed tag later transitioned to Disabled due to a foreign write access at offsets [0x0..0x4] + --> tests/fail/tree_borrows/wildcard/sequence.rs:LL:CC + | +LL | unsafe { wild.write(43) }; + | ^^^^^^^^^^^^^^ + = help: this transition corresponds to a loss of read and write permissions + = note: BACKTRACE (of the first span): + = note: inside `main` at tests/fail/tree_borrows/wildcard/sequence.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/sequence_frz.rs b/tests/fail/tree_borrows/wildcard/sequence_frz.rs new file mode 100644 index 0000000000..0a26b26ba8 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/sequence_frz.rs @@ -0,0 +1,48 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ref1 = &mut x; + let int1 = ref1 as *mut u32 as usize; + + let ref2 = &mut *ref1; + + let ref3 = &*ref2; + let int3 = ref3 as *const u32 as usize; + + let wild = int1 as *mut u32; + + // graph TD + // ref1(Res)* --> ref2(Res) --> ref3(Frz)* + // + // ┌────────────┐ + // │ │ + // │ ref1(Res)* │ + // │ │ + // └──────┬─────┘ + // │ + // │ + // ▼ + // ┌────────────┐ + // │ │ + // │ ref2(Res) │ + // │ │ + // └──────┬─────┘ + // │ + // │ + // ▼ + // ┌────────────┐ + // │ │ + // │ ref3(Frz)* │ + // │ │ + // └────────────┘ + + // Writes through ref1 as we cannot write through ref3 since it's frozen. + // Disables ref2, ref3. + unsafe { wild.write(42) }; + + // ref2 is disabled. + let fail = *ref2; //~ ERROR: /read access through .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/sequence_frz.stderr b/tests/fail/tree_borrows/wildcard/sequence_frz.stderr new file mode 100644 index 0000000000..427271b7d0 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/sequence_frz.stderr @@ -0,0 +1,27 @@ +error: Undefined Behavior: read access through at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/sequence_frz.rs:LL:CC + | +LL | let fail = *ref2; + | ^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: the accessed tag has state Disabled which forbids this child read access +help: the accessed tag was created here, in the initial state Reserved + --> tests/fail/tree_borrows/wildcard/sequence_frz.rs:LL:CC + | +LL | let ref2 = &mut *ref1; + | ^^^^^^^^^^ +help: the accessed tag later transitioned to Disabled due to a foreign write access at offsets [0x0..0x4] + --> tests/fail/tree_borrows/wildcard/sequence_frz.rs:LL:CC + | +LL | unsafe { wild.write(42) }; + | ^^^^^^^^^^^^^^ + = help: this transition corresponds to a loss of read and write permissions + = note: BACKTRACE (of the first span): + = note: inside `main` at tests/fail/tree_borrows/wildcard/sequence_frz.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/single.rs b/tests/fail/tree_borrows/wildcard/single.rs new file mode 100644 index 0000000000..a168d0be2f --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/single.rs @@ -0,0 +1,36 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + + let int1 = ref1 as *mut u32 as usize; + let wild = int1 as *mut u32; + + // graph TD + // ptr_base --> ref1(Res)* & ref2(Res) + // + // ┌────────────┐ + // │ │ + // │ ptr_base ├───────────┐ + // │ │ │ + // └──────┬─────┘ │ + // │ │ + // │ │ + // ▼ ▼ + // ┌────────────┐ ┌───────────┐ + // │ │ │ │ + // │ ref1(Res)* │ │ ref2(Res) │ + // │ │ │ │ + // └────────────┘ └───────────┘ + + // Write thourgh the wildcard to the only exposed reference ref1, + // disabling ref2. + unsafe { wild.write(13) }; + + let fail = *ref2; //~ ERROR: /read access through .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/single.stderr b/tests/fail/tree_borrows/wildcard/single.stderr new file mode 100644 index 0000000000..8ecd5fcecf --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/single.stderr @@ -0,0 +1,27 @@ +error: Undefined Behavior: read access through at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/single.rs:LL:CC + | +LL | let fail = *ref2; + | ^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: the accessed tag has state Disabled which forbids this child read access +help: the accessed tag was created here, in the initial state Reserved + --> tests/fail/tree_borrows/wildcard/single.rs:LL:CC + | +LL | let ref2 = unsafe { &mut *ptr_base }; + | ^^^^^^^^^^^^^^ +help: the accessed tag later transitioned to Disabled due to a foreign write access at offsets [0x0..0x4] + --> tests/fail/tree_borrows/wildcard/single.rs:LL:CC + | +LL | unsafe { wild.write(13) }; + | ^^^^^^^^^^^^^^ + = help: this transition corresponds to a loss of read and write permissions + = note: BACKTRACE (of the first span): + = note: inside `main` at tests/fail/tree_borrows/wildcard/single.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/fail/tree_borrows/wildcard/single2.rs b/tests/fail/tree_borrows/wildcard/single2.rs new file mode 100644 index 0000000000..26e2a3405f --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/single2.rs @@ -0,0 +1,33 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + + let int1 = ref1 as *mut u32 as usize; + let wild = int1 as *mut u32; + + // ┌────────────┐ + // │ │ + // │ ptr_base ├───────────┐ + // │ │ │ + // └──────┬─────┘ │ + // │ │ + // │ │ + // ▼ ▼ + // ┌────────────┐ ┌───────────┐ + // │ │ │ │ + // │ ref1(Res)* │ │ ref2(Res) │ + // │ │ │ │ + // └────────────┘ └───────────┘ + + // Disables ref1. + *ref2 = 13; + + // Tries to do a wildcard access through the only exposed reference ref1, which is disabled. + let fail = unsafe { *wild }; //~ ERROR: /read access through .* is forbidden/ +} diff --git a/tests/fail/tree_borrows/wildcard/single2.stderr b/tests/fail/tree_borrows/wildcard/single2.stderr new file mode 100644 index 0000000000..2ffd91abf3 --- /dev/null +++ b/tests/fail/tree_borrows/wildcard/single2.stderr @@ -0,0 +1,16 @@ +error: Undefined Behavior: read access through wildcard at ALLOC[0x0] is forbidden + --> tests/fail/tree_borrows/wildcard/single2.rs:LL:CC + | +LL | let fail = unsafe { *wild }; + | ^^^^^ Undefined Behavior occurred here + | + = 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: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information + = help: there are no exposed references who have read access permissions to this location + = note: BACKTRACE: + = note: inside `main` at tests/fail/tree_borrows/wildcard/single2.rs:LL:CC + +note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace + +error: aborting due to 1 previous error + diff --git a/tests/pass/tree_borrows/wildcard/proper_provenance.rs b/tests/pass/tree_borrows/wildcard/proper_provenance.rs new file mode 100644 index 0000000000..ef861943dd --- /dev/null +++ b/tests/pass/tree_borrows/wildcard/proper_provenance.rs @@ -0,0 +1,43 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +// NOTE: This function has UB that is not detected by wildcard provenance. +// We would need proper exposed provenance handling to support it. +#[allow(unused_variables)] +pub fn main() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + // We create 2 mutable references, each with a unique tag. + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + + // Both references get exposed. + let int1 = ref1 as *mut u32 as usize; + let int2 = ref2 as *mut u32 as usize; + //ref1 : Reserved + //ref2 : Reserved + + // We need to pick the "correct" tag for wild from the exposed tags. + let wild = int1 as *mut u32; + // wild=ref1 wild=ref2 + //ref1 : Reserved Reserved + //ref2 : Reserved Reserved + + // We write to wild, disabling the other tag. + unsafe { wild.write(13) }; + // wild=ref1 wild=ref2 + //ref1 : Unique Disabled + //ref2 : Disabled Unique + + // We access both references, even though one of them should be + // disabled under proper exposed provenance. + // This is UB, however, wildcard provenance cannot detect this. + assert_eq!(*ref1, 13); + // wild=ref1 wild=ref2 + //ref1 : Unique UB + //ref2 : Disabled Frozen + assert_eq!(*ref2, 13); + // wild=ref1 wild=ref2 + //ref1 : Frozen UB + //ref2 : UB Frozen +} diff --git a/tests/pass/tree_borrows/wildcard/protector_narrowing.rs b/tests/pass/tree_borrows/wildcard/protector_narrowing.rs new file mode 100644 index 0000000000..5c3497f56b --- /dev/null +++ b/tests/pass/tree_borrows/wildcard/protector_narrowing.rs @@ -0,0 +1,94 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +// NOTE: This file documents UB that is not detected by wildcard provenance. +pub fn main() { + protected_exposed(); + protected_wildcard(); +} + +// If a reference is protected, all foreign writes to it cause UB, +// this effectively means any write needs to happen through a child +// of the protected reference. +// This information would allow us to further narrow the possible +// candidates for a wildcard write. +#[allow(unused_variables)] +pub fn protected_exposed() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + + let int2 = ref2 as *mut u32 as usize; + + let wild = int2 as *mut u32; + fn protect(ref3: &mut u32) { + let int3 = ref3 as *mut u32 as usize; + // ┌────────────┐ + // │ │ + // │ ptr_base ├──────────────┐ + // │ │ │ + // └──────┬─────┘ │ + // │ │ + // │ │ + // ▼ ▼ + // ┌────────────┐ ┌────────────┐ + // │ │ │ │ + // │ ref1(Res) │ │ ref2(Res)* │ + // │ │ │ │ + // └──────┬─────┘ └────────────┘ + // │ + // │ + // ▼ + // ┌────────────┐ + // │ │ + // │ ref3(Res)* │ + // │ │ + // └────────────┘ + + // Since ref3 is protected, we know that every write from outside it will be UB. + // This means we know that the access is through ref3, disabling ref2. + let wild = int3 as *mut u32; + unsafe { wild.write(13) } + } + protect(ref1); + + // ref 2 is disabled, so this read causes UB. + let fail = *ref2; +} + +// We currently ignore, if a wildcard pointer has a protector +#[allow(unused_variables)] +pub fn protected_wildcard() { + let mut x: u32 = 32; + let ref1 = &mut x; + let ref2 = &mut *ref1; + + let int = ref2 as *mut u32 as usize; + let wild = int as *mut u32; + let wild_ref = unsafe { &mut *wild }; + + let mut protect = |arg: &mut u32| { + // arg is a protected pointer with wildcard provenance + // ┌────────────┐ + // │ │ + // │ ref1(Res) │ + // │ │ + // └──────┬─────┘ + // │ + // │ + // ▼ + // ┌────────────┐ + // │ │ + // │ ref2(Res)* │ + // │ │ + // └────────────┘ + + // Writes to ref1, disabling ref2, i.e. all exposed references are disabled. + // Since a wildcard reference is protected, this is UB. + *ref1 = 13; + }; + + // We pass a pointer with wildcard provenance to the function. + protect(wild_ref); +} diff --git a/tests/pass/tree_borrows/wildcard/wild1.rs b/tests/pass/tree_borrows/wildcard/wild1.rs new file mode 100644 index 0000000000..366b692164 --- /dev/null +++ b/tests/pass/tree_borrows/wildcard/wild1.rs @@ -0,0 +1,168 @@ +//@compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance + +pub fn main() { + wildcard_parallel(); + wildcard_sequence(); + dealloc(); + protector(); + protector_conflicted_release(); + returned_mut_is_usable(); +} +#[allow(unused_variables)] +pub fn wildcard_parallel() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + + // Both references get exposed. + let int1 = ref1 as *mut u32 as usize; + let int2 = ref2 as *mut u32 as usize; + + let wild = int1 as *mut u32; + + // graph TD + // ptr_base --> ref1(Res)* & ref2(Res)* + // + // ┌────────────┐ + // │ │ + // │ ptr_base ├────────────┐ + // │ │ │ + // └──────┬─────┘ │ + // │ │ + // │ │ + // ▼ ▼ + // ┌────────────┐ ┌────────────┐ + // │ │ │ │ + // │ ref1(Res)* │ │ ref2(Res)* │ + // │ │ │ │ + // └────────────┘ └────────────┘ + + // Writes through either of the two exposed references. + // We do not know which so we cannot disable the other. + unsafe { wild.write(13) }; + + // Reading through either of these references should be valid. + assert_eq!(*ref2, 13); +} + +#[allow(unused_variables)] +pub fn wildcard_sequence() { + let mut x: u32 = 42; + + let ref1 = &mut x; + let int1 = ref1 as *mut u32 as usize; + + let ref2 = &mut *ref1; + + let ref3 = &mut *ref2; + let int3 = ref3 as *mut u32 as usize; + + let wild = int1 as *mut u32; + + // graph TD + // ref1(Res)* --> ref2(Res) --> ref3(Res)* + // + // ┌────────────┐ + // │ │ + // │ ref1(Res)* │ + // │ │ + // └──────┬─────┘ + // │ + // │ + // ▼ + // ┌────────────┐ + // │ │ + // │ ref2(Res) │ + // │ │ + // └──────┬─────┘ + // │ + // │ + // ▼ + // ┌────────────┐ + // │ │ + // │ ref3(Res)* │ + // │ │ + // └────────────┘ + + // This writes either through ref1 or ref3, which is either a child or foreign access to ref2. + unsafe { wild.write(42) }; + + // Reading from ref2 still works, since the previous access could have been through its child. + // This also freezes ref3. + let x = *ref2; + + // We can still write through wild, as there is still the exposed ref1 with write permissions. + unsafe { wild.write(43) }; +} + +fn dealloc() { + use std::alloc::Layout; + let x = unsafe { std::alloc::alloc_zeroed(Layout::new::()) as *mut u32 }; + let ref1 = unsafe { &mut *x }; + let int = ref1 as *mut u32 as usize; + let wild = int as *mut u32; + + unsafe { std::alloc::dealloc(wild as *mut u8, Layout::new::()) }; +} + +fn protector() { + fn protect(arg: &mut u32) { + *arg = 4; + } + let mut x: u32 = 32; + let ref1 = &mut x; + let int = ref1 as *mut u32 as usize; + let wild = int as *mut u32; + let wild_ref = unsafe { &mut *wild }; + + protect(wild_ref); + + assert_eq!(*ref1, 4); +} + +fn protector_conflicted_release() { + let mut x: u32 = 42; + + let ptr_base = &mut x as *mut u32; + let ref1 = unsafe { &mut *ptr_base }; + let ref2 = unsafe { &mut *ptr_base }; + + let protect = |arg: &mut u32| { + // Expose arg. + let int = arg as *mut u32 as usize; + let wild = int as *mut u32; + + // Do a foreign read to arg marking it as conflicted and making child_writes UB while its protected. + let _x = *ref2; + + return wild; + }; + + let wild = protect(ref1); + + // The protector on arg got released so writes through arg should work again. + unsafe { *wild = 4 }; +} + +// Analogous to same test in `../tree-borrows.rs` but with a protected wildcard pointer. +fn returned_mut_is_usable() { + // NOTE: Currently we ignore protectors on wildcard references. + fn reborrow(x: &mut u8) -> &mut u8 { + let y = &mut *x; + // Activate the reference so that it is vulnerable to foreign reads. + *y = *y; + y + // An implicit read through `x` is inserted here. + } + let mut x: u8 = 0; + let ref1 = &mut x; + let int = ref1 as *mut u8 as usize; + let wild = int as *mut u8; + let wild_ref = unsafe { &mut *wild }; + + let y = reborrow(wild_ref); + + *y = 1; +}