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
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