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.
Collecting an exhausted
vec::IntoIterinto aVecDequegoes throughvec::IntoIter::into_vecdeque, which passesinitialized = ptr.offset_from_unsigned(buf)..end.offset_from_unsigned(buf)toVecDeque::from_contiguous_raw_parts_in. When the originalVechadcapacity() == len()(e.g.vec![1, 2, 3]) and every element was consumed, that range iscapacity..capacity, so the resulting deque hashead == capacity(withlen == 0). The field documentation onVecDequesaysso this is a state outside the documented invariant. It is mostly harmless because
wrap_indexmapshead + iwithhead == capacityontoi, but once the deque is filled back up tolen == capacitywithout reallocating, any path that computesto_physical_idx(len)evaluateswrap_index(2 * capacity, capacity), whosedebug_assert!requireslogical_index - capacity < capacity. With a standard library built with debug assertions this panics; in release buildswrap_indexreturnscapacityand the callers I looked at handle it (e.g.write_iter_wrappinggetshead_room == 0and writes nothing).Reproducer
Run with a debug-assertions std, e.g.
Expected: no panic (the deque is simply empty at that point and the
extendadds nothing).Meta
rustc 1.93.0-nightly (01867557c 2025-11-12)onaarch64-apple-darwin; the relevant code is unchanged on currentmaster(into_iter.rsinto_vecdeque,vec_deque/mod.rswrap_index/from_contiguous_raw_parts_in).Possible fix
Normalize the head in
from_contiguous_raw_parts_inwhen the initialized range is empty (or specifically wheninitialized.start == capacity), e.g.head: if initialized.start == initialized.end { 0 } else { initialized.start }, which restores the documented invariant; alternatively teachinto_vecdequeto pass0..0for an exhausted iterator (it already does so for ZSTs).Found while writing Kani proofs for
VecDequein model-checking#681.