Skip to content

VecDeque: collecting an exhausted vec::IntoIter yields head == capacity, later failing wrap_index's debug assertion #162452

Description

@jrey8343

Collecting an exhausted vec::IntoIter into a VecDeque goes through vec::IntoIter::into_vecdeque, which passes initialized = ptr.offset_from_unsigned(buf)..end.offset_from_unsigned(buf) to VecDeque::from_contiguous_raw_parts_in. When the original Vec had capacity() == len() (e.g. vec![1, 2, 3]) and every element was consumed, that range is capacity..capacity, so the resulting deque has head == capacity (with len == 0). The field documentation on VecDeque says

// `head < buf.capacity()`, unless `buf.capacity() == 0` when `head == 0`.
head: usize,

so this is a state outside the documented invariant. It is mostly harmless because wrap_index maps head + i with head == capacity onto i, but once the deque is filled back up to len == capacity without reallocating, any path that computes to_physical_idx(len) evaluates wrap_index(2 * capacity, capacity), whose debug_assert! requires logical_index - capacity < capacity. With a standard library built with debug assertions this panics; in release builds wrap_index returns capacity and the callers I looked at handle it (e.g. write_iter_wrapping gets head_room == 0 and writes nothing).

Reproducer

use std::collections::VecDeque;

fn main() {
    let v: Vec<i32> = vec![1, 2, 3]; // capacity == len
    let mut it = v.into_iter();
    it.by_ref().for_each(drop); // exhaust it: ptr == end == buf + capacity
    let mut d: VecDeque<i32> = it.collect(); // head == 3 == capacity, len == 0
    d.push_back(1);
    d.push_back(2);
    d.push_back(3); // full, no reallocation
    d.extend(std::iter::empty::<i32>()); // to_physical_idx(3) == wrap_index(6, 3)
    println!("{d:?}");
}

Run with a debug-assertions std, e.g.

$ cat Cargo.toml
...
[profile.dev]
debug-assertions = true
$ cargo +nightly run -Zbuild-std --target aarch64-apple-darwin
thread 'main' panicked at .../library/alloc/src/collections/vec_deque/mod.rs:3472:5:
assertion failed: (logical_index == 0 && capacity == 0) || logical_index < capacity ||
    (logical_index - capacity) < capacity

Expected: no panic (the deque is simply empty at that point and the extend adds nothing).

Meta

rustc 1.93.0-nightly (01867557c 2025-11-12) on aarch64-apple-darwin; the relevant code is unchanged on current master (into_iter.rs into_vecdeque, vec_deque/mod.rs wrap_index / from_contiguous_raw_parts_in).

Possible fix

Normalize the head in from_contiguous_raw_parts_in when the initialized range is empty (or specifically when initialized.start == capacity), e.g. head: if initialized.start == initialized.end { 0 } else { initialized.start }, which restores the documented invariant; alternatively teach into_vecdeque to pass 0..0 for an exhausted iterator (it already does so for ZSTs).

Found while writing Kani proofs for VecDeque in model-checking#681.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

A-collectionsArea: `std::collections`C-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-highHigh priorityT-libsRelevant to the library team, which will review and decide on the PR/issue.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions