Skip to content

Abort instead of unwinding out of an inconsistent BTreeMap::split_off - #161784

Closed
matthew-demidoff wants to merge 1 commit into
rust-lang:mainfrom
matthew-demidoff:btree-split-off-panic-safety
Closed

Abort instead of unwinding out of an inconsistent BTreeMap::split_off#161784
matthew-demidoff wants to merge 1 commit into
rust-lang:mainfrom
matthew-demidoff:btree-split-off-panic-safety

Conversation

@matthew-demidoff

@matthew-demidoff matthew-demidoff commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #158165.

BTreeMap::split_off runs the caller's Ord/Borrow impl via search_node inside Root::split_off's descent. After the first move_suffix, the two roots alias the same values through structurally invalid trees until the borders are fixed; a comparator panic there unwinds with a stale length and double-frees on later iteration or drop. It is reachable from #![forbid(unsafe_code)] on stable (reproducer on the issue).

Fix: guard the descent loop with an abort-on-panic PanicGuard, as btree::mem::replace already does. Verified with Miri: the reproducer goes from a double-free to a clean abort, and normal multi-level splits are unaffected. Adds a happy-path regression test.

Revives the approach from #158710 (closed for inactivity); credit to @ostrowr for the report.

@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 Aug 25, 2026
@rustbot

rustbot commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

r? @nia-e

rustbot has assigned @nia-e.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

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 JohnTitor, Mark-Simulacrum, clarfonthey, nia-e

@rustbot

This comment has been minimized.

@matthew-demidoff
matthew-demidoff force-pushed the btree-split-off-panic-safety branch from de7b14a to 05c9135 Compare August 25, 2026 21:12

@nia-e nia-e left a comment

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.

left some comments, but the overall approach seems good.

@rustbot author

View changes since this review

// unwinding through it double-frees the shared values (#158165). Abort
// instead, matching the panic-safety strategy used elsewhere in this
// module (see `mem::replace`).
struct PanicGuard;

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.

Why not just use either a bare catch_unwind or a DropGuard?

// inconsistent. `get_or_insert_with` (rather than `get_or_insert`)
// avoids constructing a fresh `PanicGuard` on later iterations,
// which would drop immediately and abort.
guard.get_or_insert_with(|| PanicGuard);

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.

we're rearming the guard every loop iteration? hm. why not just arm it once before the loop and disarm after, or just envelop the entire loop in a catch_unwind?

@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 Aug 28, 2026
@rustbot

rustbot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

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

After the first move_suffix in Root::split_off, the source and result
trees share values through two temporarily invalid structures. If a
later key comparison panics, unwinding leaves the map with a stale
length over a partially detached tree, and consuming iteration then
double-frees the shared values, reachable from safe code.

Guard the descent loop with an abort-on-panic PanicGuard, armed before
the first move_suffix and forgotten once the borders are fixed, matching
the strategy already used in btree::mem::replace.
@matthew-demidoff
matthew-demidoff force-pushed the btree-split-off-panic-safety branch from 05c9135 to dc0341a Compare August 29, 2026 12:54
@rustbot rustbot removed the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Aug 29, 2026
@matthew-demidoff

Copy link
Copy Markdown
Contributor Author

Sorry for the noise - I botched a force-push from a shallow clone and this PR's head got detached, so I could not reopen it. Reopened as #161975 with your feedback addressed (guard armed once before the loop; the first search stays outside it).

JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Aug 29, 2026
…anic-safety, r=nia-e

Abort instead of unwinding out of an inconsistent BTreeMap::split_off

Fixes rust-lang#158165. Supersedes rust-lang#161784, which I had to abandon after a bad force-push from a shallow clone left its head detached.

`BTreeMap::split_off` runs the caller's `Ord`/`Borrow` impl via `search_node` inside `Root::split_off`'s descent. After the first `move_suffix`, the two roots alias the same values through structurally invalid trees until the borders are fixed; a comparator panic there unwinds with a stale length and double-frees on later iteration or drop. It is reachable from `#![forbid(unsafe_code)]` on stable (reproducer on the issue).

Guard the descent loop with an abort-on-panic `PanicGuard`, as `btree::mem::replace` already does. `catch_unwind` isnt available in `alloc` (no_std), and I kept an inline guard rather than the unstable `DropGuard`. It is armed once before the loop; the first `search_node` stays outside it, since a panic there can still unwind safely (nothing has moved yet).

Verified with Miri: the reproducer goes from a double-free to a clean abort, and normal multi-level splits are unaffected. Adds a happy-path regression test.

Credit to @ostrowr for the report and to rust-lang#158710 for the original approach.

r? @nia-e
GuillaumeGomez added a commit to GuillaumeGomez/rust that referenced this pull request Aug 29, 2026
…anic-safety, r=nia-e

Abort instead of unwinding out of an inconsistent BTreeMap::split_off

Fixes rust-lang#158165. Supersedes rust-lang#161784, which I had to abandon after a bad force-push from a shallow clone left its head detached.

`BTreeMap::split_off` runs the caller's `Ord`/`Borrow` impl via `search_node` inside `Root::split_off`'s descent. After the first `move_suffix`, the two roots alias the same values through structurally invalid trees until the borders are fixed; a comparator panic there unwinds with a stale length and double-frees on later iteration or drop. It is reachable from `#![forbid(unsafe_code)]` on stable (reproducer on the issue).

Guard the descent loop with an abort-on-panic `PanicGuard`, as `btree::mem::replace` already does. `catch_unwind` isnt available in `alloc` (no_std), and I kept an inline guard rather than the unstable `DropGuard`. It is armed once before the loop; the first `search_node` stays outside it, since a panic there can still unwind safely (nothing has moved yet).

Verified with Miri: the reproducer goes from a double-free to a clean abort, and normal multi-level splits are unaffected. Adds a happy-path regression test.

Credit to @ostrowr for the report and to rust-lang#158710 for the original approach.

r? @nia-e
rust-bors Bot pushed a commit that referenced this pull request Aug 30, 2026
Rollup merge of #161975 - matthew-demidoff:btree-split-off-panic-safety, r=nia-e

Abort instead of unwinding out of an inconsistent BTreeMap::split_off

Fixes #158165. Supersedes #161784, which I had to abandon after a bad force-push from a shallow clone left its head detached.

`BTreeMap::split_off` runs the caller's `Ord`/`Borrow` impl via `search_node` inside `Root::split_off`'s descent. After the first `move_suffix`, the two roots alias the same values through structurally invalid trees until the borders are fixed; a comparator panic there unwinds with a stale length and double-frees on later iteration or drop. It is reachable from `#![forbid(unsafe_code)]` on stable (reproducer on the issue).

Guard the descent loop with an abort-on-panic `PanicGuard`, as `btree::mem::replace` already does. `catch_unwind` isnt available in `alloc` (no_std), and I kept an inline guard rather than the unstable `DropGuard`. It is armed once before the loop; the first `search_node` stays outside it, since a panic there can still unwind safely (nothing has moved yet).

Verified with Miri: the reproducer goes from a double-free to a clean abort, and normal multi-level splits are unaffected. Adds a happy-path regression test.

Credit to @ostrowr for the report and to #158710 for the original approach.

r? @nia-e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

BTreeMap::split_off is not panic-safe leading to a potential double-free

3 participants