Skip to content

MapWindows::clone is not panic-safe; panicking T::clone causes uninitialized memory to be dropped #156501

@qwaz

Description

@qwaz

Iterator::map_windows stores its active window in an internal Buffer<T, N>. Buffer<T, N>::clone first constructs a destination Buffer with start = self.start and uninitialized backing storage, then calls buffer.as_uninit_array_mut().write(self.as_array_ref().clone()). If cloning the [T; N] panics before the destination array is written, unwinding drops the destination Buffer.

This matters because Drop for Buffer<T, N> unconditionally drops N elements starting at start. In the panic path above, those slots are uninitialized, so safe Rust can cause ptr::drop_in_place to run on uninitialized memory. The PoC below demonstrates this as an invalid reference dereference during Drop.

Security impact

Expected to be low.

Triggering the bug requires all of the following conditions:

  1. Use of nightly-only unstable #![feature(iter_map_windows)].
  2. A MapWindows iterator has been advanced far enough for inner.buffer to be Some(Buffer<_, N>).
  3. The MapWindows iterator is cloned.
  4. I::Item: Clone, and an item’s Clone::clone panics while cloning the active window.
  5. Panic unwinding is enabled, so the partially constructed internal Buffer is dropped.

Data flow trace

MapWindows::cloneMapWindowsInner::cloneOption<Buffer>::cloneBuffer::cloneT::clone panic → Buffer::drop

  • Buffer: documents that self.buffer[self.start..self.start + N] is initialized.

    // `Buffer` uses two times of space to reduce moves among the iterations.
    // `Buffer<T, N>` is semantically `[MaybeUninit<T>; 2 * N]`. However, due
    // to limitations of const generics, we use this different type. Note that
    // it has the same underlying memory layout.
    struct Buffer<T, const N: usize> {
    // Invariant: `self.buffer[self.start..self.start + N]` is initialized,
    // with all other elements being uninitialized. This also
    // implies that `self.start <= N`.
    buffer: [[MaybeUninit<T>; N]; 2],
    start: usize,
    }

  • Buffer::clone: constructs a destination Buffer with uninitialized storage and an active Drop, then performs panic-capable cloning before the destination slots are initialized.

    impl<T: Clone, const N: usize> Clone for Buffer<T, N> {
    fn clone(&self) -> Self {
    let mut buffer = Buffer {
    buffer: [[const { MaybeUninit::uninit() }; N], [const { MaybeUninit::uninit() }; N]],
    start: self.start,
    };
    buffer.as_uninit_array_mut().write(self.as_array_ref().clone());
    buffer
    }

  • Buffer::drop: drops N slots starting at start.

    impl<T, const N: usize> Drop for Buffer<T, N> {
    fn drop(&mut self) {
    // SAFETY: our invariant guarantees that N elements starting from
    // `self.start` are initialized. We drop them here.
    unsafe {
    let initialized_part: *mut [T] = crate::ptr::slice_from_raw_parts_mut(
    self.buffer_mut_ptr().add(self.start).cast(),
    N,
    );
    ptr::drop_in_place(initialized_part);
    }
    }

Demonstration

This PoC uses only safe Rust. It initializes a map_windows buffer, clones the iterator, panics immediately from CloneBomb::clone, then crashes when Buffer::drop drops uninitialized CloneBomb slots.

Run with:

cargo +nightly run --features unstable-map-windows --bin map_windows_clone_panic_poc
// cargo +nightly run --features unstable-map-windows --bin map_windows_clone_panic_poc
#![feature(iter_map_windows)]
#![deny(unsafe_code)]

const WINDOW: usize = 4;

const STATIC_U8: &u8 = &42;

fn main() {
    let input: [CloneBomb; WINDOW] = std::array::from_fn(|_| CloneBomb::new());
    let mut iter = IntoIterator::into_iter(input).map_windows::<_, _, WINDOW>(|_| ());

    iter.next();
    eprintln!("initialized a {WINDOW}-element map_windows buffer from an array");
    eprintln!("cloning the iterator will panic");

    let _clone = iter.clone();
}

struct CloneBomb {
    payload: &'static u8,
}

impl Clone for CloneBomb {
    fn clone(&self) -> Self {
        panic!("intentional panic from CloneBomb::clone");
    }
}

impl Drop for CloneBomb {
    fn drop(&mut self) {
        // Buffer::drop calls this for an uninitialized slot after clone panics.
        // Printing the raw address first shows the bad reference before the
        // following dereference turns it into a visible abort in a plain run.
        let payload_addr = self.payload as *const u8;
        eprintln!("CloneBomb::drop, payload ref {payload_addr:p}");
        eprintln!("payload byte: {}", *self.payload);
    }
}

impl CloneBomb {
    fn new() -> Self {
        Self { payload: STATIC_U8 }
    }
}

Output

Pointer values and the final crash message may vary by platform.

initialized a 4-element map_windows buffer from an array
cloning the iterator will panic

thread 'main' (1099108) panicked at src/bin/map_windows_clone_panic_poc.rs:26:9:
intentional panic from CloneBomb::clone
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
CloneBomb::drop, payload ref 0x599659d14f28
payload byte: 42
CloneBomb::drop, payload ref 0x599659d14f28
payload byte: 42
CloneBomb::drop, payload ref 0x2828282828282828
payload byte: Segmentation fault         (core dumped) cargo +nightly run --features unstable-map-windows --bin map_windows_clone_panic_poc 2>&1

Environment

$ rustc +nightly --version --verbose
rustc 1.97.0-nightly (4b0c9d76a 2026-05-10)
binary: rustc
commit-hash: 4b0c9d76ae7d387229caea55cfa73c280b08b8a7
commit-date: 2026-05-10
host: x86_64-unknown-linux-gnu
release: 1.97.0-nightly
LLVM version: 22.1.4

$ lsb_release -a
Distributor ID: Ubuntu
Description:    Ubuntu 26.04 LTS
Release:        26.04
Codename:       resolute

The initial discovery was made by AI. The analysis, exploitation, and this report have been reviewed and verified by human experts.

Reporting on behalf of Autonomous Code Security (ACS) team at Microsoft.

Metadata

Metadata

Labels

C-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-lowLow priorityneeds-triageThis issue may need triage. Remove it if it has been sufficiently triaged.requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions