Skip to content

In-place optimisation in IntoIterator can lead to memory leak #101628

Closed
@Voultapher

Description

@Voultapher

I tried this code:

fn main() {
    #[derive(Debug)]
    struct Old(i32);

    impl Drop for Old {
        fn drop(&mut self) {
            if self.0 == 44 {
                panic!();
            }

            println!("Dropped Old: {}", self.0);
        }
    }

    #[derive(Debug)]
    struct New(i32);

    impl Drop for New {
        fn drop(&mut self) {
            println!("Dropped New: {}", self.0);
        }
    }

    let vec: Vec<Old> = vec![Old(11), Old(22), Old(33), Old(44), Old(55)];
    let vec_ptr = vec.as_ptr();

    let mapped_vec: Vec<New> = vec.into_iter().map(|x| New(x.0)).take(2).collect();
    let mapped_vec_ptr = mapped_vec.as_ptr();

    println!("vec_ptr:    {vec_ptr:?}");
    println!("mapped_vec: {mapped_vec_ptr:?}");
    println!("{mapped_vec:?}");
}

I expected to see this happen:

The drop implementation for New(11) and New(22) is called.

Instead, this happened:

Everything that was mapped and the allocation was leaked.

Meta

rustc --version --verbose:

rustc 1.63.0 (4b91a6ea7 2022-08-08)
binary: rustc
commit-hash: 4b91a6ea7258a947e59c6522cd5898e7c0a6a88f
commit-date: 2022-08-08
host: aarch64-apple-darwin
release: 1.63.0
LLVM version: 14.0.5

I discovered this issue when I learned that IntoIterator re-uses a Vec allocation if possible. Which prompted me to read the code. Running the example with cargo miri run also reports the memory leak. And changing New(i32) to New(i64) no longer displays the memory leak. This is what I think happens:

[ 11| 22| 33| 44| 55] Values
[ 0 | 1 | 2 | 3 | 4 ] Indices

[Old|Old|Old|Old|Old] Start (default fn from_iter)

[New|Old|Old|Old|Old] Map [0] (fn collect_in_place)
[New|New|Old|Old|Old] Map [1] (fn collect_in_place)

[New|New|Drp|Old|Old] Drop [2] (fn forget_allocation_drop_remaining)
[New|New|Drp|Old|Old] Drop [3] (fn forget_allocation_drop_remaining) PANIC

While dropping [3] the ptr::drop_in_place(remaining) call in forget_allocation_drop_remaining panics.

Then [4] is being dropped, but honestly I don't really understand why. My understanding is that ptr::drop_in_place(remaining) should unwind. If anyone can explain this, please do so.

Then as part of unwinding collect_in_place IntoIter is dropped, but it has its fields set to an empty buffer of len 0, so it does nothing. Note, I suspect more optimal code gen could be achieved by using mem::forget to avoid generating code for the IntoIter drop that will never do anything after we cleared the fields.

The two created New will never get dropped, thus leading to the memory leak. Plus the main Vec allocation, previously owned by IntoIterator as stored in dst_buf will be leaked too. Because it was cleared in forget_allocation_drop_remaining and assumed to be taken over by let vec = unsafe { Vec::from_raw_parts(dst_buf, len, cap) }; but we never reached that point.


Proposed solution, a drop guard in collect_in_place that guards the forget_allocation_drop_remaining call and would drop the head in this case 0..2 and the backing allocation if forget_allocation_drop_remaining panics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.P-lowLow priorityT-libsRelevant to the library team, which will review and decide on the PR/issue.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions