Repository navigation
VecDeque: collecting an exhausted vec::IntoIter yields head == capacity, later failing wrap_index's debug assertion #162452
Description
Activity
- addedneeds-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triagingThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
on Sep 8, 2026 Thanks for finding this!
This is a violation of the internal invariants of
VecDeque.I'm not sure it actually leads to memory corruption right now, but it is unsound in the sense that new code could.It does, see comment below.Here's a fun way to see the fields without building the standard library.
use std::collections::VecDeque; fn main() { let print_parts = |d: &_| { // don't do this in real code, this is library UB ^^ let parts = unsafe { const PARTS: usize = size_of::<VecDeque<i32>>() / size_of::<usize>(); std::mem::transmute_copy::<_, [usize; PARTS]>(d) }; eprintln!("{:?}", parts) }; 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 print_parts(&d); // [3, 102488265907664, 3, 0] d.push_back(1); d.push_back(2); d.push_back(3); print_parts(&d); // [3, 102488265907664, 3, 3] eprintln!("{d:?}"); }
@rustbot label A-collections I-unsound T-libs C-bug
- addedA-collectionsArea: `std::collections`Area: `std::collections`I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessT-libsRelevant to the library team, which will review and decide on the PR/issue.Relevant to the library team, which will review and decide on the PR/issue.I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}Issue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
on Sep 8, 2026 Here's an example that segfaults:
use std::collections::VecDeque; fn main() { for n in 1..200 { let v = vec![vec![vec![1]]; n]; let mut it = v.into_iter(); for _ in &mut it {} let mut d: VecDeque<_> = it.collect(); // head = n, cap = n, len = 0 d.push_back(vec![]); // head = n, cap = n, len = 1 // Reads from one-past-the-end of buffer (run with miri to see this) let wat = d.pop_front(); eprintln!("{n}: {wat:?}"); } }
@rustbot claim
- added a commit that references this issue
on Sep 8, 2026 Thanks for the quick triage and the segfaulting example. For what it's worth,
pop_frontis the direct culprit on the read side: it doesbuffer_read(old_head)with the unwrappedself.head, sohead == capacityreads slotcapacity. On the verification side (model-checking#681)from_contiguous_raw_parts_innow carries the preconditioninitialized.start < capacity || initialized.start == 0(and ensureshead < capacityunless the capacity is 0), which is the conditioninto_vecdequeneeds to satisfy; normalizing an empty range to0..0in either function satisfies it.Yeah, the
VecDequecode is as much of a mess as I remember it. It has gotten slightly better with the introduction ofWrappedIndex, except that the safety invariants are still not documented and handled as strictly as they probably should be.I'll probably add safety requirements to
from_contiguous_raw_parts_inand refactorinto_vecdeque- removedneeds-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triagingThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
on Sep 8, 2026 - removedI-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}Issue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
on Sep 8, 2026 Assigning P-high (discussion on Zulip).
- addedP-highHigh priorityHigh priorityC-bugCategory: This is a bug.Category: This is a bug.
on Sep 8, 2026 - added a commit that references this issue
on Sep 8, 2026 - added a commit that references this issue
on Sep 8, 2026
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.