Challenge 25: Verify the safety of VecDeque (unbounded, symbolic ring layouts, contracts on all 13 unsafe fns) - #681
Open
jrey8343 wants to merge 2 commits into
Open
Conversation
…bounded proofs for the 30 safe fns
Adds `safety::{requires, ensures}` contracts (with `kani::modifies`) to
`push_unchecked`, `buffer_read`, `buffer_write`, `buffer_range`, `copy`,
`copy_nonoverlapping`, `wrap_copy`, `copy_slice`, `write_iter`,
`write_iter_wrapping`, `handle_capacity_increase`,
`from_contiguous_raw_parts_in`, `abort_shrink` (plus `rotate_left_inner` /
`rotate_right_inner`), each verified by `proof_for_contract` harnesses, and
`kani::proof` harnesses for the 30 safe abstractions listed in the challenge.
All harnesses start from a deque with symbolic capacity, symbolic `head` and
symbolic `len` (`any_deque`), so both contiguous and wrapped ring layouts are
explored; no harness bounds the length or capacity, and no harness uses
`kani::unwind`. Loops are handled with loop contracts: the shipped
`retain_mut` loops get attribute-only invariants, and `write_iter`'s
`for_each` is spelled as an equivalent explicit loop under `cfg(kani)`
(`write_iter_loop`) so that a loop contract can be attached. The `slice::rotate`
call in `make_contiguous` is abstracted by a precondition-checking stub.
Only shipped-code changes: the `cfg(kani)` loop form of `write_iter`, the
loop-contract attributes on `retain_mut`, and `#![feature(proc_macro_hygiene)]`
in alloc's lib.rs (needed for statement attributes, as in core).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hQMihqVQLDerCELXExGHg
This was referenced Sep 8, 2026
`initialized.start == capacity` (an exhausted `vec::IntoIter` whose `Vec` had `capacity == len`) produced a deque with `head == capacity`; `pop_front` reads slot `head` unwrapped, so that state reads past the buffer (rust-lang#162452, I-unsound). The contract now requires `initialized.start < capacity || initialized.start == 0` and ensures the structural invariant, and the harness note no longer calls the state harmless. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hQMihqVQLDerCELXExGHg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #286 (Challenge 25). All changes are in
library/alloc/src/collections/vec_deque/mod.rs(plus one feature gate in
library/alloc/src/lib.rs).What is verified
safety::{requires, ensures}+kani::modifies), eachwith
#[kani::proof_for_contract]harnesses:push_unchecked,buffer_read,buffer_write,buffer_range,copy,copy_nonoverlapping,wrap_copy,copy_slice,write_iter,write_iter_wrapping,handle_capacity_increase,from_contiguous_raw_parts_in,abort_shrink; additionallyrotate_left_inner/rotate_right_inner(unsafe fns on the safelist) carry contracts too.
#[kani::proof]harnesses: get, get_mut, swap, reserve_exact,reserve, try_reserve_exact, try_reserve, shrink_to, truncate, as_slices, as_mut_slices, range,
range_mut, drain, pop_front, pop_back, push_front, push_back, insert, remove, split_off, append,
retain_mut, grow, resize_with, make_contiguous, rotate_left, rotate_right, rotate_left_inner,
rotate_right_inner.
How the two hard requirements are met
Unbounded. Every harness starts from
any_deque(): a real allocation of symbolic capacity,a symbolic
headover every valid physical index and a symboliclen(0..=cap), with exactlythe logical range initialized. There is no
kani::unwindand no length constant anywhere; theonly assumption on sizes is
Layout::array::<T>(cap).is_ok()(RawVec's own precondition) andcap * size_of::<T>() <= 2^48(CBMC's object model under--object-bits 12, see the comment onMAX_ALLOCATION_BYTES). Loops are discharged with loop contracts:retain_mut's two shippedloops get attribute-only invariants, and
write_iterruns its real iteration as an explicit loopunder
cfg(kani)(write_iter_loop, same writes, same counting, nothing forgotten).Wrapped states.
any_dequecovers bothhead + len <= capandhead + len > cap; everyharness reports both witnesses SATISFIED.
wrap_copyhas witnesses for all seven copy branches,handle_capacity_increasefor A/B/C,abort_shrinkfor its three outcomes,shrink_tofor itsfour element-moving cases,
make_contiguousfor all four (incl. both rotations),drainforhead/tail/interior/all.
Generic
T. Harnesses are generic functions instantiated by macro: every contract harnessover
u8,u64and()(the ZST branch,capacity() == usize::MAX), the cheap ones also over[u8; 3](odd stride); every safe-function harness overu8and(), the cheap ones also overu64. As on #605 this is per-type instantiation, which Kani requires; the harness bodiesthemselves are written once for arbitrary
T. 125 harnesses in total.Faithfulness notes (reviewer's list on #564/#605)
buffer_readrequiresub_checks::can_dereference(self.ptr().add(off))(initialized slot);buffer_writerequirescan_write.write_iter/write_iter_wrappingbound the iterator through its uppersize_hint(exact for the
TrustedLeniterators every caller passes), andwrittenoverflow.handle_capacity_increaserequiresself.len <= old_capacity <= self.capacity()andself.head < old_capacity || self.head == 0.wrap_copyuses the documentedmin(|src-dst|, cap-|src-dst|) + len <= cap(which alsoimplies its
debug_assert!).abort_shrink's precondition is the stateshrink_toleaves behind when the allocator unwinds.Kani limitations worked around (all at the pinned
d4df833c)stub_verifiedon any function whosemodifiesnames a slice ICEs (kani-compilertransform/contracts.rsinstantiateswrite_any_slicewith the slice type as the elementtype; Failed to
stub_verifiedcontracts with slices inkani::modifieskani#3682, fixed upstream after the pin by Fix compiler crash on slice-modifies verified stubs (#4748) kani#4749).kani::stubon a contracted function is blocked by#[kani::stub]on contracted functions kani#4591. Hence thewrite_iterloop lives in a contract-freecfg(kani)helper (write_iter_loop) whosecallers are verified with a hand-written contract replacement (
stub_write_iter_loop:asserts
write_iter's precondition, havocs exactly the written slots, advanceswrittenand the iterator). Once the Kani pin includes auto: vim syntax highlighting improvements rust-lang/rust#4749 this can become a plain
stub_verified(write_iter).&mut(
Take<ByRefSized<&mut I>>inwrite_iter_wrapping's wrapping branch); that is why thecallers use the replacement above instead of running the loop.
Take<RepeatWith<_>>(whatresize_withfeeds throughextend) cannot be advanced withouta loop, so the two
resize_withharnesses cover the non-wrapping append (arbitrary wrappeddeque with capacity reserved up front, and
head == 0with growth insideresize_with); thewrapping write path is verified by
check_write_iter_wrapping_*.slice::rotate::ptr_rotate(reached frommake_contiguous) is a safe callee abstracted by aprecondition-checking stub;
make_contiguousperforms no value-dependent step after it.loop_modifiestarget that is a zero-sized closure(CBMC aborts with
l2_rename_rvalues casestruct' not handled` when a zero-sized closure is a loop_modifies target kani#4786); theretain_mutloop contract therefore does not list thepredicate (harness predicates are stateless).
Finding:
from_contiguous_raw_parts_inis reachable withhead == capacity(unsound)Writing the contract for
from_contiguous_raw_parts_inshowed thatvec::IntoIter::into_vecdequepasses
initialized == capacity..capacityfor an exhausted iterator whoseVechadcapacity == len, producing a deque withhead == capacity, outside the field comment'shead < capacity.pop_frontreads slotheadunwrapped, so after onepush_backthis readsone past the end of the buffer (segfault demonstrated by @maxdexh). Reported as
rust-lang#162452, labelled I-unsound and claimed for a fix. The contract here requires
initialized.start < capacity || initialized.start == 0and ensures the structural invariant,which is exactly the precondition
into_vecdequeviolates; the fix upstream will make thatcaller satisfy it.
Local results
125 harnesses with the pinned Kani, all passing, every
kani::coverwitness satisfied. Heaviest:insert(u8) ~3-6 min,wrap_copy(u8/u64) ~2-3 min,rotate_*_inner(u8) ~2 min; total ≈ 45CPU-minutes.
-Z uninit-checkscould not be used as additional evidence: at the pinned Kani itrejects alloc (function-pointer operands, kani#3300).
🤖 Generated with Claude Code
https://claude.ai/code/session_011hQMihqVQLDerCELXExGHg