Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BTreeMap::drain_filter should not touch the root during iteration #74762

Merged
merged 2 commits into from
Aug 2, 2020

Conversation

ssomers
Copy link
Contributor

@ssomers ssomers commented Jul 25, 2020

Although Miri doesn't point it out, I believe there is undefined behaviour using drain_filter when draining the 11th-last element from a tree that was larger. When this happens, the last remaining child nodes are merged, the root becomes empty and is popped from the tree. That last step establishes a mutable reference to the node elected root and writes a pointer in node::Root, while iteration continues to visit the same node.

This is mostly code from #74437, slightly adapted.

@rust-highfive
Copy link
Collaborator

r? @Mark-Simulacrum

(rust_highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 25, 2020
@bors
Copy link
Contributor

bors commented Jul 28, 2020

☔ The latest upstream changes (presumably #73265) made this pull request unmergeable. Please resolve the merge conflicts.

@ssomers ssomers force-pushed the btree_no_root_in_remove_kv_tracking branch 2 times, most recently from 059e9ca to b51be5f Compare August 1, 2020 11:40
@ssomers ssomers force-pushed the btree_no_root_in_remove_kv_tracking branch from b51be5f to 99398dd Compare August 1, 2020 22:51
@Mark-Simulacrum
Copy link
Member

I am still not certain that the UB is there, but it seems like the related code cleanups in this PR are good regardless.

@bors r+

@bors
Copy link
Contributor

bors commented Aug 2, 2020

📌 Commit 99398dd has been approved by Mark-Simulacrum

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 2, 2020
@ssomers
Copy link
Contributor Author

ssomers commented Aug 2, 2020

You're probably right. I assumed that pop_level would deference the new root node (in addition to the Root owner of that node) but it seems like it doesn't. It all works on raw pointers and NonNull. There's also the NodeRef::root raw pointer that stays around in the iterator's copy of the handle, that I think we couldn't safely deference a second time, but that never happened anyway because it's only needed to pop_level the empty root and that can only happen once in a drain filter's lifetime.

Note that the earlier commit simplified code a little more, but recent benchmarking said consistently this costs 5 to 10%, so I passed the emptied_internal_root information around instead. Not sure what I benchmarked earlier.

bors added a commit to rust-lang-ci/rust that referenced this pull request Aug 2, 2020
Rollup of 10 pull requests

Successful merges:

 - rust-lang#74686 (BTreeMap: remove into_slices and its unsafe block)
 - rust-lang#74762 (BTreeMap::drain_filter should not touch the root during iteration)
 - rust-lang#74781 (Clean up E0733 explanation)
 - rust-lang#74874 (BTreeMap: define forget_type only when relevant)
 - rust-lang#74974 (Make tests faster in Miri)
 - rust-lang#75010 (Update elasticlunr-rs and ammonia transitive deps)
 - rust-lang#75041 (Replaced log with tracing crate)
 - rust-lang#75044 (Clean up E0744 explanation)
 - rust-lang#75054 (Rename rustc_middle::cstore::DepKind to CrateDepKind)
 - rust-lang#75057 (Avoid dumping rustc invocations to stdout)

Failed merges:

 - rust-lang#74827 (Move bulk of BTreeMap::insert method down to new method on handle)

r? @ghost
@bors bors merged commit 814b31e into rust-lang:master Aug 2, 2020
@ssomers ssomers deleted the btree_no_root_in_remove_kv_tracking branch August 2, 2020 19:28
ssomers added a commit to ssomers/rust that referenced this pull request Aug 5, 2020
ssomers added a commit to ssomers/rust that referenced this pull request Aug 6, 2020
bors added a commit to rust-lang-ci/rust that referenced this pull request Aug 8, 2020
…ulacrum

BTreeMap: better way to postpone root access in DrainFilter

A slightly more elegant (in my opinion) adaptation of rust-lang#74762. Benchmarks seem irrationally pleased to:
```
benchcmp old new --threshold 5
 name                                           old ns/iter  new ns/iter  diff ns/iter   diff %  speedup
 btree::map::clone_fat_100_and_remove_all       215,182      185,052           -30,130  -14.00%   x 1.16
 btree::map::clone_fat_100_and_remove_half      139,667      127,945           -11,722   -8.39%   x 1.09
 btree::map::clone_fat_val_100_and_remove_all   96,755       81,279            -15,476  -16.00%   x 1.19
 btree::map::clone_fat_val_100_and_remove_half  64,678       56,911             -7,767  -12.01%   x 1.14
 btree::map::find_rand_100                      18           17                     -1   -5.56%   x 1.06
 btree::map::first_and_last_0                   33           35                      2    6.06%   x 0.94
 btree::map::first_and_last_100                 40           54                     14   35.00%   x 0.74
 btree::map::insert_rand_100                    45           42                     -3   -6.67%   x 1.07
 btree::map::insert_rand_10_000                 45           41                     -4   -8.89%   x 1.10
 btree::map::iter_0                             2,010        1,759                -251  -12.49%   x 1.14
 btree::map::iter_100                           3,514        2,764                -750  -21.34%   x 1.27
 btree::map::iter_10k                           4,018        3,768                -250   -6.22%   x 1.07
 btree::map::range_unbounded_unbounded          37,269       28,929             -8,340  -22.38%   x 1.29
 btree::map::range_unbounded_vs_iter            31,518       28,814             -2,704   -8.58%   x 1.09
```

r? @Mark-Simulacrum
@ssomers
Copy link
Contributor Author

ssomers commented Aug 13, 2020

Illustration of the alleged undefined behaviour under stacked borrows rules:

Say we have a 2-level map with 11 keys -5..=5. The root has key 0, a left child with keys -5..=-1, a right child with keys 1..=5. You won't get this 2nd level inserting 11 elements from scratch, but you can get there removing an element from a larger map.

We start drain_filter, leave -5 to -2 there, and drain -1. Removing key -1 comes down to simply decrementing the length of the left child. We see the left child is underfull, we see there is no left sibling so we turn to the right sibling, we see we can merge with the right sibling so we do that (appending the 0 and 1..=5 keys/values onto the left child, discarding the right child, shortening the parent). On the road to checking whether that has made the parent underfull instead, we see that the parent has become empty. So we pop the empty root off the tree: parent.into_root_mut().pop_level(). This dereferences the map's &'a mut Root<K, V> (which is unique), gets and later deallocates the raw Root::node of type BoxedNode AKA Unique (fine), creates a NodeRef around the same raw pointer, descends to the only remaining child (that descend uses and obtains only raw pointers) and writes its raw pointer to Root::node. Then it creates another NodeRef around the Unique to the new child, traverses that, and erases the former child's parent pointer.

(*self.as_mut().as_leaf_mut()).parent = ptr::null();

or virtually:

let x: *mut LeafNode<K, V> = …;
(*x).parent = ptr::null();

That's a raw pointer access (unless there's something like #73987 going on).

If we turn it into a "real" &mut LeafNode, Miri still does not complain, while I think the whole LeafNode would be invalidated, and that includes the raw pointer in the NodeRef that is returned and assigned to DrainFilterInner::cur_leaf_edgeand later used in the next iterator step. But I'm not sure, and in addition, the latter access is also done using raw pointers, despite the fact that the iterator has unique access, because code is shared with double-ended iterators that do not have unique access.

ssomers added a commit to ssomers/rust that referenced this pull request Aug 13, 2020
bors added a commit to rust-lang-ci/rust that referenced this pull request Aug 15, 2020
…mulacrum

Revert the fundamental changes in rust-lang#74762 and rust-lang#75257

Before possibly going over to rust-lang#75487. Also contains some added and fixed comments.

r? @Mark-Simulacrum
@cuviper cuviper added this to the 1.47.0 milestone May 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants