Fix UB in Iterator::map_windows clone on element clone panic#156517
Fix UB in Iterator::map_windows clone on element clone panic#156517HaseebKhalid1507 wants to merge 2 commits into
Conversation
`Buffer<T, N>::clone` constructed the destination `Buffer` (with `Drop`
armed via `start = self.start`) *before* cloning the active window into
its `MaybeUninit` storage. If `<[T; N] as Clone>::clone` panicked
partway through, unwinding dropped the destination `Buffer`, whose
`Drop` impl then called `ptr::drop_in_place` over `[start..start + N]`
uninitialized slots — UB reachable from safe `#![feature(iter_map_windows)]`
code.
Clone the window into a stack-local `[T; N]` first. `<[T; N] as Clone>`
is itself panic-safe: a clone failure on the k-th element drops the k-1
already-cloned predecessors. Only after the array clone succeeds do we
construct the destination `Buffer` and move the array into its storage.
`MaybeUninit::write` cannot panic, so the `Buffer::drop` invariant
("`[start..start + N]` initialized") holds for the entire lifetime of
the new `Buffer`.
|
r? @nia-e rustbot has assigned @nia-e. Use Why was this reviewer chosen?The reviewer was selected based on:
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a soundness issue in the unstable Iterator::map_windows implementation where Buffer<T, N>::clone could drop uninitialized memory if an element’s Clone::clone panicked during cloning, making UB reachable from safe Rust. The fix makes Buffer::clone panic-safe by cloning into a temporary [T; N] first and only constructing/writing into the destination Buffer after cloning succeeds, and adds a regression test to cover the panic-unwind path.
Changes:
- Make
Buffer<T, N>::clonepanic-safe by cloning the active window into a temporary[T; N]before constructing the destinationBuffer. - Add a regression test that forces panics at each position within the cloned window to ensure no uninitialized slots are dropped during unwinding.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| library/core/src/iter/adapters/map_windows.rs | Reworks Buffer::clone to avoid creating a drop-armed Buffer before the potentially-panicking array clone completes. |
| library/coretests/tests/iter/adapters/map_windows.rs | Adds a regression test that triggers panics during iter.clone() to detect uninitialized-drop UB and double-drop/leak issues. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { | ||
| let _clone = iter.clone(); | ||
| })); |
| // Only after the clone succeeds do we construct the destination | ||
| // `Buffer` and move the array into its `MaybeUninit` storage. | ||
| // `MaybeUninit::write` cannot panic, so the invariant | ||
| // `self.buffer[self.start..self.start + N]` is initialized holds | ||
| // for the entire lifetime of `buffer`. | ||
| let mut buffer = Buffer { | ||
| buffer: [[const { MaybeUninit::uninit() }; N], [const { MaybeUninit::uninit() }; N]], | ||
| start: self.start, | ||
| }; |
- Hoist `ClonePanicker` and `check_clone_panic` helpers to module scope inside `drop_checks`, matching the `DropCheck` / `check` / `check_drops` pattern of the surrounding tests. - Rename the regression test to `panic_in_clone`, parallel to the existing `panic_in_first_batch` / `panic_in_middle` naming. - Assert that `catch_unwind` returns `Err`. Without this, a future regression that silently stops panicking in `Buffer::clone` would let the test pass without exercising the panic path. - Add `start == N` test coverage. The buggy path was triggerable from two physically distinct destination regions inside `Buffer`'s backing storage depending on `self.start`; the previous test only covered `start == 0`. - Use the shorter `"intended panic in clone"` message, matching the existing `"intended panic"` convention in this file. - Tighten the comments on `Buffer::clone` and clarify that the `Buffer::drop` invariant holds only *after* the `MaybeUninit::write` call, not over the construction-to-write window. - Rename the `cloned` local to `array`.
|
Uncertain if this is an agent pushing commits to this PR or a human actually pushing the commits, but if someone is pushing these commits to this PR, you may want to read up the proposed guidelines for AI usage. |
|
Hi, human here, not a bot. Sorry for the missing disclosure, this PR was AI assisted. The fix and tests were written by AI, but I reproduced the bug before patching, and I believe I understand what the code is doing. |
| }; | ||
| buffer.as_uninit_array_mut().write(self.as_array_ref().clone()); | ||
| buffer.as_uninit_array_mut().write(array); | ||
| buffer |
There was a problem hiding this comment.
Another option would be to wrap Buffer in a ManuallyDrop and extract it out of the ManuallyDrop once the cloned values have been written. This would reduce stack memory usage, though it would leak memory on panic.
There was a problem hiding this comment.
i dont like this style of initializing an invalid Buffer, so i made another go at implementing a fix for this by abstracting the storage: #156533
There was a problem hiding this comment.
I think @bjorn3's suggestion is correct here, no need to reinvent the wheel. It's also the usual approach we use for this
|
Hi, Just letting an LLM generate a solution for an issue for you is not acceptable. While we do not have any policy accepted yet, all three pending policies forbid this:
We are a community of contributors, not just a code repository. Thus we focus on contributors who desire to stay around and put in the work to produce high quality contributions or learn to do so. We are thus issueing you a warning as per our policies (1) and contribution standards (2): Do not generate solutions to issues and just review the output. You can contact the moderation team to discuss and possibly reconsider your warning. Thanks for understanding Oli in the name of the mod team |
@HaseebKhalid1507 In addition to the previous comment, please do not trigger unprompted Copilot reviews without our consent |
Fixes #156501.
Buffer<T, N>::cloneinlibrary/core/src/iter/adapters/map_windows.rsconstructs a
Bufferwithstart = self.start(which arms theDropimpl over
start..start + N) before calling<[T; N] as Clone>::cloneto populate that range. If the array-clone panics, unwinding drops the
half-built
BufferandBuffer::dropcallsptr::drop_in_placeonuninitialized memory — UB reachable from safe Rust under
#![feature(iter_map_windows)].The fix clones the window into a stack-local
[T; N]first. The arrayCloneimpl is itself panic-safe (it drops already-cloned predecessorson failure), so a mid-clone panic unwinds before any
Bufferexists.Only after the array clone succeeds is the destination
Bufferconstructed and the array moved into its
MaybeUninitstorage viaMaybeUninit::write, which is infallible.The regression test lives in the existing
coretests::iter::adapters::map_windows::drop_checksmodule and reusesits
DropInfoharness. It uses aClonePanickerelement that panicson its k-th
Clone::clonecall, parameterized over both the windowsize
Nand the panic position within the window. Without the fix,this test fails under Miri with the same uninitialized-read diagnostic
as the issue's PoC.
Validated locally:
Buffer+ fixedclone→ Miri cleancore→ Miri UB./x test tidy→ passesrustfmt --edition 2024 --config-path rustfmt.toml→ no diff@rustbotlabel +I-unsound +T-libs