-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Audit liballoc for leaks in Drop
impls when user destructor panics
#67290
Conversation
This comment has been minimized.
This comment has been minimized.
Drop
implsDrop
impls when user destructor panics
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this @jonas-schievink! I need to spend more time digging through the changes, especially to Vec
. In general, I think we need comments on all of these drop guards that document what they're doing.
cc @rust-lang/libs do you have any thoughts on this? This PR makes collections try a bit harder to drop their contents in the presence of panics, so they behave in the same as ptr::drop_in_place
and multiple locals when one of them panics during drop.
We've already done this for LinkedList
and VecDeque
. This PR includes:
BinaryHeap::drain_sorted
(which is unstable)BTreeMap::into_iter
(used byBTreeMap
during drop)LinkedList::drain_filter
VecDeque::truncate
VecDeque::drain
Vec::into_iter
Vec::drain
I've historically hoped that it would never come to this myself, maintaining this sort of code is really hard and provides very little benefit in practice. In some sense though this was inevitable, so beyond auditing for correctness I don't think there's much we can do but merge this. |
I would personally like to go the way C++ did and always abort on any unwind from a destructor (even if we weren't already unwinding). However I'm not quite sure whether that is a breaking change. |
I haven't reviewed the implementation but if it does what it says (matching the behavior of ptr::drop_in_place in the case of panics) then I am on board with accepting this. |
What does |
Double panics always abort |
This comment has been minimized.
This comment has been minimized.
It sounds like we need to come to consensus about whether to accept this patch, or instead aim to opt for the behavior C++ has, i.e., aborting on panic in destructors unconditionally. I think we should explore making destructors abort on panic, but in a separate PR (I would be interested in code size and possible performance benefits). For now I would land this PR -- it seems to be consistent with what we have been doing elsewhere (I have not verified the implementation in too much detail, but it seems to be correct -- I think there are tests for every new added impl, but they appear to be in different files so it's a bit hard to be certain without comparing one by one). In terms of the PR itself, it seems like it would be nice to have some primitive for what amounts to |
☔ The latest upstream changes (presumably #67758) made this pull request unmergeable. Please resolve the merge conflicts. |
The job Click to expand the log.
I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact |
Thanks for your patience @jonas-schievink! It looks like we're generally on-board with doing this, so I've got some time this week to give it a thorough 👀 |
I agree with #67290 (comment) -- let's land this change once the implementation is reviewed, and then look into what it would take to make all panic inside Drop behave like a double panic. I think it should be doable. We'd be turning currently panicking programs into aborting programs which is no different from what this PR does. |
Am I right that in the case of
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a safety perspective this looks valid to me. We're mostly just shifting existing logic into guards, and I can't spot any invalid behavior when those guards are triggered partway through a dropping operation, since they remain internally consistent up to that point.
@KodrAus what's blocking this from getting merged? |
@Dylan-DPC I wanted to look at @ssomers comment about
@bors r+ |
📌 Commit e5987a0 has been approved by |
My comments were really confined to But unlike
cannot leave any element behind because that would be as bad as a leak, while it's normal to see elements left behind after:
And if the guarantee is considered important enough to provide code for, it should be important enough to document (for vec and linked_list and probably more alike). Right now, |
☀️ Test successful - checks-azure |
drop(pair); | ||
mem::forget(guard); | ||
} | ||
|
||
unsafe { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the stuff in this unsafe
block also run when unwinding from user drop? It looks like it does more deallocation.
FWIW, Miri can also detect memory leaks and I am running it on the liballoc test suite. It seems you are covered in terms of tests here, but if you think Miri could be helpful, please let me know. Also, the latest run of Miri actually failed with a memory leak -- something landed during the last 9 days that made that happen. I'll have some fun figuring out the details.^^ |
} | ||
|
||
let mut map = BTreeMap::new(); | ||
map.insert("a", D); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to Miri, an allocation made during this call leaks:
note: created allocation with id 143756
--> /home/r/src/rust/rustc/src/liballoc/alloc.rs:81:47
|
81 | __rust_alloc(layout.size(), layout.align())
| ^ created allocation with id 143756
|
= note: inside call to `std::alloc::alloc` at /home/r/src/rust/rustc/src/liballoc/alloc.rs:169:22
= note: inside call to `<std::alloc::Global as std::alloc::AllocRef>::alloc` at /home/r/src/rust/rustc/src/liballoc/alloc.rs:203:15
= note: inside call to `alloc::alloc::exchange_malloc` at /home/r/src/rust/rustc/src/liballoc/boxed.rs:175:9
= note: inside call to `std::boxed::Box::<alloc::collections::btree::node::LeafNode<&str, btree::map::test_into_iter_drop_leak::D>>::new` at /home/r/src/rust/rustc/src/liballoc/collections/btree/node.rs:211:43
= note: inside call to `alloc::collections::btree::node::Root::<&str, btree::map::test_into_iter_drop_leak::D>::new_leaf` at /home/r/src/rust/rustc/src/liballoc/collections/btree/map.rs:1333:25
= note: inside call to `std::collections::BTreeMap::<&str, btree::map::test_into_iter_drop_leak::D>::ensure_root_is_owned` at /home/r/src/rust/rustc/src/liballoc/collections/btree/map.rs:1067:9
= note: inside call to `std::collections::BTreeMap::<&str, btree::map::test_into_iter_drop_leak::D>::entry` at /home/r/src/rust/rustc/src/liballoc/collections/btree/map.rs:838:15
note: inside call to `std::collections::BTreeMap::<&str, btree::map::test_into_iter_drop_leak::D>::insert` at alloc_miri_test/../liballoc/tests/btree/map.rs:1038:5
--> alloc_miri_test/../liballoc/tests/btree/map.rs:1038:5
|
1038 | map.insert("a", D);
| ^^^^^^^^^^^^^^^^^^
### LEAK REPORT ###
Alloc 143756: 00 00 00 00 00 00 00 00 __ __ 05 00 __ __ __ __ 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ (192 bytes, alignment 8) (Rust)
└──────(143732)───────┘ └──────(143850)───────┘ └──────(143963)───────┘ └──────(144092)───────┘ └──────(144237)───────┘
Alloc 143732: 61 (1 bytes, alignment 1) (Static)
Alloc 143850: 62 (1 bytes, alignment 1) (Static)
Alloc 143963: 63 (1 bytes, alignment 1) (Static)
Alloc 144092: 64 (1 bytes, alignment 1) (Static)
Alloc 144237: 65 (1 bytes, alignment 1) (Static)
Is that expected, or should all memory be properly freed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The root node of the tree in this simple test case is not freed. If the intention is to free all BTreeMap nodes as well, then the DropGuard in IntoIter::drop should include the rest of the normal body that frees the last pile of nodes of the tree (but doesn't need the is_shared_root test because the shared root doesn't have any elements to drop).
Most nodes of the tree have already been freed rather secretly in next
calls, and as far as I see, these nodes have already been freed before the last key-value pair in them is dropped (which sounds peculiar, but the key and value dropped are in fact last minute copies). But to be sure of this, some test should cover trees with more than just a root node.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll open an issue around this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issue is at #69769
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah looks like this is what @programmerjake pointed out above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean this comment, I presume? Yeah that seems related.
} | ||
} | ||
|
||
let v = vec![D(false), D(true), D(false)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's another memory leak in this test: the Vec
backing store does not get deallocated. See #69770.
Inspired by #67243 and #67235, this audits and hopefully fixes the remaining
Drop
impls in liballoc for resource leaks in the presence of panics in destructors called by the affectedDrop
impl.This does not touch
Hash{Map,Set}
since they live in hashbrown. They have similar issues though.r? @KodrAus