Skip to content

Fix double free when a comparator panics mid-split_off#158710

Open
chatman-media wants to merge 1 commit into
rust-lang:mainfrom
chatman-media:fix-btree-split-off-panic-safety
Open

Fix double free when a comparator panics mid-split_off#158710
chatman-media wants to merge 1 commit into
rust-lang:mainfrom
chatman-media:fix-btree-split-off-panic-safety

Conversation

@chatman-media

@chatman-media chatman-media commented Jul 3, 2026

Copy link
Copy Markdown

Root::split_off's loop calls search_node (which invokes the caller's Ord/Borrow impl and can panic) interleaved with move_suffix, which physically relocates key-value pairs out of the left tree and into the new right-hand tree one level at a time. If the comparator panics on a level after the first move_suffix has already run, the unwind leaves self with its old, too-large length but a tree that's missing whatever already got moved into right_root — and right_root itself gets dropped along with the panic, freeing those values. Iterating or dropping the "recovered" map afterwards walks past the border into node slots that no longer own what they claim to, and double frees. Reproducer from #158165 (comparator that panics on cmp(6, 0), caught with catch_unwind, then the map is dropped) crashes with malloc(): double free detected on stable/beta/nightly.

search_node is pure — it's the only thing here that can fail, and nothing gets mutated until move_suffix runs — so a drop guard that only gets armed once the first move actually happens turns the panic into a clean abort instead. This is the same strategy mem::replace already uses a few lines away in this same module for an analogous "can't safely unwind out of this" situation.

Worth flagging for review: my first pass at this used guard.get_or_insert(AbortOnDrop), which takes its argument by value — so on every loop iteration after the first it would construct a fresh AbortOnDrop, find the Option already Some, and immediately drop (and thus abort on) the throwaway one, turning every ordinary multi-level split into a crash. Caught it by testing against the full alloctests suite before deciding this was ready; get_or_insert_with is the actual fix, get_or_insert was silently wrong for stateful guard types like this. Added a regression test for exactly that (multi-level, non-panicking splits) alongside the existing split_off tests.

Verified with -Z build-std against a patched local alloc: the double-free repro now aborts cleanly instead of continuing into the corrupted state, a panic on the very first comparison (before any move) still unwinds normally via catch_unwind as before, and all 334 tests in alloctests (173 of them in btree) pass, including BTreeSet::split_off which goes through the same Root::split_off.

…Map::split_off

split_off's loop interleaves search_node (which calls into user Ord/Borrow
code and can panic) with move_suffix, which physically relocates key-value
pairs from the left tree into the still-being-built right tree one level at
a time. If the comparator panics after the first move_suffix has already
run, the unwind leaves self with its old, too-large length but a tree that's
missing the part that got moved into the right-hand root (which then gets
dropped along with the panic). Iterating or dropping the map afterwards
walks past the border into node slots that no longer own what they claim to,
and can double free.

Since search_node is the only failure point and it's pure (no mutation until
move_suffix runs), a small drop guard that only gets armed once the first
move actually happens turns the panic into a clean abort, which matches how
mem::replace already handles the equivalent situation elsewhere in this
module. Added a regression test for the ordinary multi-level path too, since
get_or_insert vs get_or_insert_with is an easy thing to get backwards here
(get_or_insert takes its arg by value, so it'll construct-and-immediately-
drop a fresh guard on every loop iteration after the first).
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 3, 2026
@rustbot

rustbot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pull request, and welcome! The Rust Project is excited to review your changes, and you should hear from @Darksonn (or someone else) some time within the next two weeks.

Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue
Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: libs
  • libs expanded to 12 candidates
  • Random selection from 6 candidates

@Darksonn Darksonn added A-collections Area: `std::collections` A-panic Area: Panicking machinery labels Jul 3, 2026
let mut guard = None;

loop {
let mut split_edge = match left_node.search_node(key) {

@Darksonn Darksonn Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to help me understand: this call to search_node is the only line that actually calls into code that can panic, right? So for this to result in an abort, it has to fail on at least the second iteration and it has to fail here?

View changes since the review

// unaffected; the abort itself can't be observed from within a single
// process. See the reproducer attached to the issue for the double free.
#[test]
fn test_split_off_multi_level_unaffected_by_panic_guard() {

@Darksonn Darksonn Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add the actual failing code from #158165 as a regression test.

View changes since the review

@Darksonn

Darksonn commented Jul 3, 2026

Copy link
Copy Markdown
Member

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 3, 2026
@rustbot

rustbot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-collections Area: `std::collections` A-panic Area: Panicking machinery S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants