Skip to content

Commit e26779e

Browse files
Make Tree Borrows Provenance GC compact the tree
Follow-up on rust-lang#3833 and rust-lang#3835. In these PRs, the TB GC was fixed to no longer cause a stack overflow. One test that motivated it was the test `fill::horizontal_line` in `tiny_skia`. But not causing stack overflows was not a large improvents, since it did not fix the fundamental issue: The tree was too large. The test now ran, but it required gigabytes of memory and hours of time, whereas it finishes within seconds in Stacked Borrows. The problem in that test was that it used [`slice::chunked`](https://doc.rust-lang.org/std/primitive.slice.html#method.chunks) to iterate a slice in chunks. That iterator is written to reborrow at each call to `next`, which creates a linear tree with a bunch of intermediary nodes, which also fragments the `RangeMap` for that allocation. The solution is to now compact the tree, so that these interior nodes are removed. Care is taken to not remove nodes that are protected, or that otherwise restrict their children.
1 parent 4318bfe commit e26779e

File tree

4 files changed

+185
-17
lines changed

4 files changed

+185
-17
lines changed

src/tools/miri/src/borrow_tracker/tree_borrows/perms.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ mod transition {
130130
Active =>
131131
if protected {
132132
// We wrote, someone else reads -- that's bad.
133-
// (If this is initialized, this move-to-protected will mean insta-UB.)
133+
// (Since Active is always initialized, this move-to-protected will mean insta-UB.)
134134
Disabled
135135
} else {
136136
// We don't want to disable here to allow read-read reordering: it is crucial
@@ -267,6 +267,43 @@ impl Permission {
267267
transition::perform_access(kind, rel_pos, old_state, protected)
268268
.map(|new_state| PermTransition { from: old_state, to: new_state })
269269
}
270+
271+
/// During a provenance GC, we want to compact the tree.
272+
/// For this, we want to merge nodes upwards if they have a singleton parent.
273+
/// But we need to be careful: If the parent is Frozen, and the child is Reserved,
274+
/// we can not do such a merge. In general, such a merge is possible if the parent
275+
/// allows similar accesses, and in particular if the parent never causes UB on its
276+
/// own. This is enforced by a test, namely `tree_compacting_is_sound`. See that
277+
/// test for more information.
278+
/// This method is only sound if the parent is not protected. We never attempt to
279+
/// remove protected parents.
280+
pub fn can_be_replaced_by_child(self, child: Self) -> bool {
281+
match (self.inner, child.inner) {
282+
// ReservedIM can be replaced by anything, as it allows all
283+
// transitions.
284+
(ReservedIM, _) => true,
285+
// Reserved (as parent, where conflictedness does not matter)
286+
// can be replaced by all but ReservedIM,
287+
// since ReservedIM alone would survive foreign writes
288+
(ReservedFrz { .. }, ReservedIM) => false,
289+
(ReservedFrz { .. }, _) => true,
290+
// Active can not be replaced by something surviving
291+
// foreign reads and then remaining writable
292+
(Active, ReservedIM) => false,
293+
(Active, ReservedFrz { .. }) => false,
294+
(Active, Active) => true,
295+
// Active can be replaced by Frozen, since it is not protected
296+
(Active, Frozen) => true,
297+
(Active, Disabled) => true,
298+
// Frozen can only be replaced by Disabled
299+
(Frozen, Frozen) => true,
300+
(Frozen, Disabled) => true,
301+
(Frozen, _) => false,
302+
// Disabled can not be replaced by anything else
303+
(Disabled, Disabled) => true,
304+
(Disabled, _) => false,
305+
}
306+
}
270307
}
271308

272309
impl PermTransition {

src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ impl LocationState {
128128
Ok(transition)
129129
}
130130

131+
/// Like `perform_access`, but ignores the diagnostics, and also is pure.
132+
/// As such, it returns `Some(x)` if the transition succeeded, or `None`
133+
/// if there was an error.
134+
#[allow(unused)]
135+
fn perform_access_no_fluff(
136+
mut self,
137+
access_kind: AccessKind,
138+
rel_pos: AccessRelatedness,
139+
protected: bool,
140+
) -> Option<Self> {
141+
match self.perform_access(access_kind, rel_pos, protected) {
142+
Ok(_) => Some(self),
143+
Err(_) => None,
144+
}
145+
}
146+
131147
// Helper to optimize the tree traversal.
132148
// The optimization here consists of observing thanks to the tests
133149
// `foreign_read_is_noop_after_foreign_write` and `all_transitions_idempotent`,
@@ -840,6 +856,57 @@ impl Tree {
840856
node.children.is_empty() && !live.contains(&node.tag)
841857
}
842858

859+
/// Checks whether a node can be replaced by its only child.
860+
/// If so, returns the index of said only child.
861+
/// If not, returns none.
862+
fn can_be_replaced_by_single_child(
863+
&self,
864+
idx: UniIndex,
865+
live: &FxHashSet<BorTag>,
866+
) -> Option<UniIndex> {
867+
let node = self.nodes.get(idx).unwrap();
868+
if node.children.len() != 1 || live.contains(&node.tag) || node.parent.is_none() {
869+
return None;
870+
}
871+
// Since protected nodes are never GC'd (see `borrow_tracker::GlobalStateInner::visit_provenance`),
872+
// we know that `node` is not protected because otherwise `live` would
873+
// have contained `node.tag`.
874+
let child_idx = node.children[0];
875+
let child = self.nodes.get(child_idx).unwrap();
876+
for (_, data) in self.rperms.iter_all() {
877+
let parent_perm =
878+
data.get(idx).map(|x| x.permission).unwrap_or_else(|| node.default_initial_perm);
879+
let child_perm = data
880+
.get(child_idx)
881+
.map(|x| x.permission)
882+
.unwrap_or_else(|| child.default_initial_perm);
883+
if !parent_perm.can_be_replaced_by_child(child_perm) {
884+
return None;
885+
}
886+
}
887+
888+
Some(child_idx)
889+
}
890+
891+
/// Properly removes a node.
892+
/// The node to be removed should not otherwise be usable. It also
893+
/// should have no children, but this is not checked, so that nodes
894+
/// whose children were rotated somewhere else can be deleted without
895+
/// having to first modify them to clear that array.
896+
/// otherwise (i.e. the GC should have marked it as removable).
897+
fn remove_useless_node(&mut self, this: UniIndex) {
898+
// Due to the API of UniMap we must make sure to call
899+
// `UniValMap::remove` for the key of this node on *all* maps that used it
900+
// (which are `self.nodes` and every range of `self.rperms`)
901+
// before we can safely apply `UniKeyMap::remove` to truly remove
902+
// this tag from the `tag_mapping`.
903+
let node = self.nodes.remove(this).unwrap();
904+
for (_perms_range, perms) in self.rperms.iter_mut_all() {
905+
perms.remove(this);
906+
}
907+
self.tag_mapping.remove(&node.tag);
908+
}
909+
843910
/// Traverses the entire tree looking for useless tags.
844911
/// Removes from the tree all useless child nodes of root.
845912
/// It will not delete the root itself.
@@ -883,23 +950,20 @@ impl Tree {
883950
// Remove all useless children.
884951
children_of_node.retain_mut(|idx| {
885952
if self.is_useless(*idx, live) {
886-
// Note: In the rest of this comment, "this node" refers to `idx`.
887-
// This node has no more children (if there were any, they have already been removed).
888-
// It is also unreachable as determined by the GC, so we can remove it everywhere.
889-
// Due to the API of UniMap we must make sure to call
890-
// `UniValMap::remove` for the key of this node on *all* maps that used it
891-
// (which are `self.nodes` and every range of `self.rperms`)
892-
// before we can safely apply `UniKeyMap::remove` to truly remove
893-
// this tag from the `tag_mapping`.
894-
let node = self.nodes.remove(*idx).unwrap();
895-
for (_perms_range, perms) in self.rperms.iter_mut_all() {
896-
perms.remove(*idx);
897-
}
898-
self.tag_mapping.remove(&node.tag);
899-
// now delete it
953+
// delete it everywhere else
954+
self.remove_useless_node(*idx);
955+
// and delete it from children_of_node
900956
false
901957
} else {
902-
// do nothing, but retain
958+
if let Some(nextchild) = self.can_be_replaced_by_single_child(*idx, live) {
959+
// delete the in-between child
960+
self.remove_useless_node(*idx);
961+
// set the new child's parent
962+
self.nodes.get_mut(nextchild).unwrap().parent = Some(*tag);
963+
// save the new child in children_of_node
964+
*idx = nextchild;
965+
}
966+
// retain it
903967
true
904968
}
905969
});

src/tools/miri/src/borrow_tracker/tree_borrows/tree/tests.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,71 @@ fn all_read_accesses_commute() {
6464
}
6565
}
6666

67+
fn as_foreign_or_child(related: AccessRelatedness) -> &'static str {
68+
if related.is_foreign() { "foreign" } else { "child" }
69+
}
70+
71+
fn as_protected(b: bool) -> &'static str {
72+
if b { " (protected)" } else { "" }
73+
}
74+
75+
fn as_lazy_or_init(b: bool) -> &'static str {
76+
if b { "initialized" } else { "lazy" }
77+
}
78+
79+
/// Test that tree compacting (as performed by the GC) is sound.
80+
/// Specifically, the GC will replace a parent by a child if the parent is not
81+
/// protected, and if `can_be_replaced_by_child(parent, child)` is true.
82+
/// To check that this is sound, the function must be a simulation, i.e.
83+
/// if both are accessed, the results must still be in simulation, and also
84+
/// if an access is UB, it must also be UB if done only at the child.
85+
#[test]
86+
fn tree_compacting_is_sound() {
87+
// The parent is unprotected
88+
let parent_protected = false;
89+
for ([parent, child], child_protected) in <([LocationState; 2], bool)>::exhaustive() {
90+
if child_protected {
91+
precondition!(child.compatible_with_protector())
92+
}
93+
precondition!(parent.permission().can_be_replaced_by_child(child.permission()));
94+
for (kind, rel) in <(AccessKind, AccessRelatedness)>::exhaustive() {
95+
let new_parent = parent.perform_access_no_fluff(kind, rel, parent_protected);
96+
let new_child = child.perform_access_no_fluff(kind, rel, child_protected);
97+
match (new_parent, new_child) {
98+
(Some(np), Some(nc)) => {
99+
assert!(
100+
np.permission().can_be_replaced_by_child(nc.permission()),
101+
"`can_be_replaced_by_child` is not a simulation: on a {} {} to a {} parent and a {} {}{} child, the parent becomes {}, the child becomes {}, and these are not in simulation!",
102+
as_foreign_or_child(rel),
103+
kind,
104+
parent.permission(),
105+
as_lazy_or_init(child.is_initialized()),
106+
child.permission(),
107+
as_protected(child_protected),
108+
np.permission(),
109+
nc.permission()
110+
)
111+
}
112+
(_, None) => {
113+
// the child produced UB, this is fine no matter what the parent does
114+
}
115+
(None, Some(nc)) => {
116+
panic!(
117+
"`can_be_replaced_by_child` does not have the UB property: on a {} {} to a(n) {} parent and a(n) {} {}{} child, only the parent causes UB, while the child becomes {}, and it is not allowed for only the parent to cause UB!",
118+
as_foreign_or_child(rel),
119+
kind,
120+
parent.permission(),
121+
as_lazy_or_init(child.is_initialized()),
122+
child.permission(),
123+
as_protected(child_protected),
124+
nc.permission()
125+
)
126+
}
127+
}
128+
}
129+
}
130+
}
131+
67132
#[test]
68133
#[rustfmt::skip]
69134
// Ensure that of 2 accesses happen, one foreign and one a child, and we are protected, that we

src/tools/miri/tests/pass-dep/concurrency/linux-futex.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ fn wait_wake() {
158158
);
159159
}
160160

161-
assert!((200..1000).contains(&start.elapsed().as_millis()));
161+
// When running this in stress-gc mode, things can take quite long.
162+
// So the timeout is 3000 ms.
163+
assert!((200..3000).contains(&start.elapsed().as_millis()));
162164
t.join().unwrap();
163165
}
164166

0 commit comments

Comments
 (0)