Challenge 25: Verify the safety of VecDeque (unbounded, symbolic ring layouts, contracts on all 13 unsafe fns) - #681
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
`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
feliperodri
left a comment
There was a problem hiding this comment.
Thanks @jrey8343 — and apologies this was missed in the first Challenge 25 round (created after our triage snapshot). Reviewed now with our vacuity tooling + local Kani (pinned 0.67.0 / CBMC 6.8.0).
This is by far the strongest Challenge 25 submission and a genuine breakthrough: the first TRULY UNBOUNDED VecDeque solution. any_deque allocates a symbolic cap constrained only by Layout::array::<T>(cap).is_ok() + the CBMC object-model bound — no kani::unwind, no MAX_LEN, no length constant anywhere, both contiguous and wrapped ring layouts covered. A real advance over #564/#605 (both bounded). 13/13 unsafe fns with genuine #[requires]/#[ensures] + #[kani::proof_for_contract] and 30/30 safe abstractions. It even surfaced a real upstream unsoundness (rust-lang#162452). Clean soundness elsewhere: substantive ring invariant_holds() (not true), real loop invariants, no orphan contracts, no kani::assume(false), and — unlike #605 — no out-of-scope core:: edits.
Local Kani (CBMC 6.8.0): check_push_unchecked_u8 (unbounded proof_for_contract) and check_write_iter_u8 (the loop-contract harness) both VERIFICATION SUCCESSFUL. The unbounded harnesses are CBMC-heavy (wrap_copy didn't finish in ~10 min locally) — relevant to the "completes within CI resource limits" bar; please confirm the full suite is green in CI.
Requesting changes on the items to resolve before acceptance:
- Generic-
T. Bodies are generic (fn check_x<T: kani::Arbitrary>()) but instantiated over representative types (u8/u64/[u8;3]/()). Kani requires monomorphized harnesses, so literal "no monomorphization" is unachievable in-tool — the same open question across Ch16/17/18/23/24. Needs a committee ruling on whether representative-type coverage satisfies the clause; until then I can't mark it as literally meeting the criterion (for consistency with those challenges). write_itercfg(kani) loop-paraphrase (T1).#[cfg(not(kani))]ships the realfor_each;#[cfg(kani)]runswrite_iter_loop. Runtime is unchanged and the paraphrase is a faithful transcription (justified by kani#4591/rust-lang#3682/rust-lang#4749) and verified locally — but the proof runs the paraphrase, not the shipped body. Move to thestub_verifiedpath once the pin includes rust-lang#4749, and flag for committee sign-off.- Three hand-written stubs (
model_write_iter,stub_write_iter_loop(_drop),stub_ptr_rotate) are modular over-approximations whose soundness rests on argument — principled and documented, but reviewer trust points.
This is the prioritized Ch25 solution and the closest any unbounded-clause challenge has come to acceptance. If the committee accepts representative-mono generic-T and signs off on the write_iter paraphrase + stubs, this is approvable. Outstanding work.
… checks Sync with main (toolchain nightly-2026-02-05, Kani 152c6a8, CBMC 6.10.0). At this pin Kani compares a stub's signature with the original's by the position of their own generic parameters, which rejects a free function as the stub of a method; the contract replacement for `write_iter_loop` is therefore a `#[cfg(kani)]` sibling method now. Generic `T`: add the shapes `bool` (validity invariant; generators now write a valid bit pattern of the shape), `Al16` (`repr(align(16))`) and `WithDrop` (`needs_drop`, non-`Copy`, destructor counter) on the targets that exercise them; the slice drop glue is checked bounded for `WithDrop` (`truncate`, `drain`, `retain_mut`, `Drop for VecDeque`); alias drops of bitwise copies in harnesses become `mem::forget`; the module doc states the shape matrix and the parametricity argument. `write_iter` transcription: the shipped `for_each` statement is compiled under Kani byte for byte as `write_iter_for_each` and checked against `write_iter`'s contract with a bounded iterator length; the docs derive why no unbounded proof of the shipped text exists at any current pin and why `stub_verified` (with or without kani#4749) would not discharge the wrapping branch. Stubs: `stub_write_iter_loop`/`model_write_iter` become `VecDeque::write_iter_contract_replacement`, which asserts the contract's own predicate (`write_iter_precondition`) and havocs the `modifies` expression; its one step beyond the contract (consuming the iterator by `hi`) and `stub_ptr_rotate`'s abstraction get bounded checks in `verify::bounded_evidence`, the only place using `kani::unwind`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XjuqtSUTkxA5kjmq32PEoJ
|
Thanks for the review. The branch is synced to
Bounded harnesses live in CI: the previous run (at the old merge ref) was green on all four verification partitions, metrics and autoharness; this push runs at the new pin and I will follow up here if anything fails. Local: 172/172 harnesses verified at the new pin, all 918 covers satisfied; 42.5 min of CBMC time sequentially (32.2 min for the 125 reviewed harnesses, 10.3 min for the 47 added ones, heaviest |
Resolves #286 (Challenge 25). All changes are in
library/alloc/src/collections/vec_deque/mod.rs(plus#![feature(proc_macro_hygiene)]inlibrary/alloc/src/lib.rs, which is what lets#[safety::loop_invariant]sit on a statement, as incore/src/lib.rs; no runtime effect).What is verified
safety::{requires, ensures}+kani::modifies), each with#[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 safe list) 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.Unbounded. Every harness starts from
any_deque(): a real allocation of symbolic capacity, a symbolicheadover every valid physical index and a symboliclen(0..=cap), with exactly the logical range initialized. There is nokani::unwindand no length constant in any proof; the only assumption on sizes isLayout::array::<T>(cap).is_ok()(RawVec's own precondition) andcap * size_of::<T>() <= 2^48(CBMC's object model under--object-bits 12, seeMAX_ALLOCATION_BYTES). Loops are discharged with loop contracts (retain_mut's two shipped loops, andwrite_iter's iteration — see item 2 below).Wrapped states.
any_dequecovers bothhead + len <= capandhead + len > cap; every harness 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 its four element-moving cases,make_contiguousfor all four (incl. both rotations),drainfor head/tail/interior/all.Changes since the 2026-09-13 review
The three review items, in the reviewer's order. The 125 reviewed harnesses keep their bodies except for the plumbing listed under "Diff to the reviewed revision" (the two the reviewer reproduced locally,
check_push_unchecked_u8andcheck_write_iter_u8, are unchanged).1. Generic
T— representative shapes, with the propertiesVecDequeobserves coveredKani rejects
#[kani::proof]on a generic function (attributes.rs: "the#[kani::proof]attribute cannot be applied to generic functions"), so each harness body is written once for arbitraryTand instantiated over element shapes.VecDeque<T>is parametric inTexcept throughsize_of/align_of(allocation and pointer arithmetic),T::IS_ZST(the branches that skip the buffer), moves of opaque values, and the drop glueptr::drop_in_place::<[T]>intruncate,drainandDrop for VecDeque; it never branches onneeds_dropor on element values (the only value-dependent code is the user closures, which the harnesses make nondeterministic). The shapes now cover every one of those properties:needs_drop/ non-Copy()u8u64[u8; 3]bool(new)0/1)push_unchecked,buffer_read,write_iter,get,pop_front,push_backAl16(new,#[repr(align(16))])get,pop_front,push_back,shrink_to,split_offWithDrop(new,impl Dropwith a destructor counter)push_unchecked,buffer_read,buffer_write,write_iter,write_iter_wrapping,from_contiguous_raw_parts_in,get,get_mut,swap,pop_front,pop_back,push_front,push_back,remove,split_off,appendThe generators write pre-existing elements with one symbolic byte per region — now a valid bit pattern of the shape (
Shape::any_fill, whichbooloverrides), so they are sound for types with a validity invariant; elements that enter through the API are per-elementkani::any(). What cannot be unbounded for aneeds_dropshape is the slice drop glue:drop_in_place::<[T]>is a compiler-generated loop that cannot carry a loop contract, sotruncate,drain,retain_mut(which truncates) andDrop for VecDequeare checked forWithDropbounded (len <= 4) inverify::bounded_evidence, asserting through the counter that exactly the removed elements' destructors run, and every element's exactly once when the deque is dropped. Every other path is unbounded for all shapes.This is the in-tool maximum; whether representative shapes satisfy the literal "no monomorphization" clause is the committee question the review names (see "Items requiring committee sign-off").
2.
write_iter's cfg(kani) transcription — kept (it is the minimum at any current pin), what it hides now machine-checkedwrite_itershipsiter.enumerate().for_each(|(i, element)| unsafe { self.buffer_write(dst + i, element); *written += 1; })under#[cfg(not(kani))]and runs the explicit loopwrite_iter_loop(loop contract) under#[cfg(kani)]. Three facts, each checked against the pinned Kani152c6a8(the pinmainmoved to on 2026-09-12; both spike errors below were reproduced at it):for_eachtext exists at any current pin. The shipped loop is insideIterator::for_each→Enumerate::fold→I::fold(core,library/core/src/iter/adapters/enumerate.rs,traits/iterator.rs), where no loop contract can be attached. A loop contract inside a harness iterator's ownfoldsits belowEnumerate::fold, which moves its indexcountinto a closure that the loop either havocs (it is a loop local;loop_modifiesof a reference lowers to__CPROVER_object_whole) or writes in violation of the assigns clause — and no invariant can name a closure field. StubbingEnumerate::fold(Kani Handle divide by zero in constant evaluator rust-lang/rust#4587, trait-method stubbing, is in the pin) is rejected becausefoldhas method-level generics that the resolver does not pass;for_eachis a non-overridden default (Kani doc: Document mutable function arguments rust-lang/rust#4588). Kani's ownfor-loop support is itself a rewrite to an indexedwhile(kani_macros/src/sysroot/loop_contracts/mod.rs, "types that cannot be havocked effectively … private fields").stub_verified(write_iter)is not available at the pin, and would not be enough. At152c6a8,stub_verifiedon a contract whosemodifiesnames a slice still ICEs (kani-compiler/src/kani_middle/reachability.rs:420: "Failed to resolvestd::iter::IntoIterator::into_iterwith&mut [[u8]]", Kani String concatenation with + only type-checks if the first operand is an owned pointer rust-lang/rust#3682; the fix auto: vim syntax highlighting improvements rust-lang/rust#4749 is 92 commits after the pin), andkani::stubof a contracted function still fails ("Failed to find contract closure__kani_recursion_check_abs_val", Kani Rewrite the coercion code to be more readable, more sound, and to reborrow when needed. rust-lang/rust#4591). Even with auto: vim syntax highlighting improvements rust-lang/rust#4749, a pure contract replacement would not provewrite_iter_wrapping's wrapping branch: the generated replacement drops the by-valueByRefSized(&mut iter).take(head_room)unconsumed, so the secondwrite_iter(0, iter, ..)sees the whole iterator again, havocsslots(0, n)outsidewrite_iter_wrapping'smodifiesregion and can pushwrittenpastlen; anditeris moved, so noensuresonwrite_itercan observe its post-state. The earlier PR text's "once the pin includes auto: vim syntax highlighting improvements rust-lang/rust#4749 this can become a plainstub_verified(write_iter)" was wrong and is withdrawn.write_iter_for_each("must stay byte-identical; diff it"), andverify::bounded_evidence::bounded_write_iter_shipped_text_{u8,u64,u8x3,unit,withdrop}areproof_for_contract(write_iter)harnesses that substitute it forwrite_iter_loop— the shipped text checked against the same contract, the deque fully symbolic, the iterator length bounded by 4 (#[kani::unwind(6)]). The unbounded proof stays on the transcription (check_write_iter_*), whose doc now states what it relies on (Iterator's documented equivalence offoldand repeatednext()), that loop-contract proofs are partial-correctness proofs at this pin (termination follows from the exactsize_hintofTrustedLeniterators;loop_decreasescannot be combined with an explicitloop_modifiesat this pin), and the future condition under which the transcription can move out of alloc (Kani gaining stubbing of generic trait methods, so that core'sEnumerate::foldcan be replaced).Flagged for committee sign-off below, as requested.
3. The stubs — argument turned into checks; each stub's evidence in one table
write_iter_loop(cfg(kani) body ofwrite_iter)for_eachstatementbounded_write_iter_shipped_text_*: the shipped statement against the same contract,n <= 4fold≡ repeatednext()VecDeque::write_iter_contract_replacement(callers ofwrite_iter;_dropvariant forresize_with, whoseTake<RepeatWith<_>>cannot be advanced without a loop)write_iter_loopwrite_iter's precondition — through the same predicate the contract uses,write_iter_precondition, so the two cannot driftmodifiesregionslots(dst, hi)gets an arbitrary valid fill;written += hi; iterator advanced byhibounded_write_iter_wrapping_shipped_text_u8(the wrapping branch with the shipped statement iterating on the realTake<ByRefSized<&mut _>>stack:written == n) andbounded_replacement_advance_by_matches_next_u8(advance_by(hi)≡hi×next()on that stack and on the bare iterator)hi=TrustedLenexactness (assertedlo == hi); a symbolicn <= hiwould put the second call's havoc outside the caller'smodifies, the same failure as abovestub_ptr_rotate(check_make_contiguous_*)core::slice::rotate::ptr_rotate(pub(super)in core; no contract exists for it)bounded_rotate_permutes_range_u8: the real<[u8]>::rotate_left/rotate_right(the callsmake_contiguousmakes) on ranges of every length and amount up to eight, symbolic contents, symbolic guard bytes on both sides — the range holds exactly the rotated sequence, the guards are untouched (lengths and amounts enumerated so that only the selected, loop-freeptr_rotate_memmoveis explored; the looping algorithms needmin(left, right) > 256foru8)make_contiguousdoes nothing value-dependent afterwardsThe replacement's doc comment is corrected: it writes exactly
hi(the code always did; the comment said "some n ≤ hi"). Two things were tried and abandoned at the pin, recorded in the doc: running the realwrite_iter_loopinsideresize_withand a direct-branch-onlywrite_iter_wrappingharness (the loop contract is then instrumented for theTake<ByRefSized<&mut _>>instance too; neither finished within 20 minutes), a stub-free unboundedmake_contiguousrestricted to inputs whose rotation is the loop-freeptr_rotate_memmove(3.8 GB and unfinished after 20 minutes), andmake_contiguousitself with the real rotation on an eight-slot buffer with symbolic layout (the unreachable looping algorithms ofptr_rotateare unwound anyway and the harness exceeded 17 GB).bounded_evidenceNone of the 12 harnesses in
verify::bounded_evidenceis the proof of a listed function; each is supplementary evidence for one stub, one transcription or one drop-glue path named in its doc comment, the module is the only place inverifythat uses#[kani::unwind], and every listed function keeps its unbounded harness in the parent module. Thewrite_iteriteration is exercised there through the contract-freewrite_iter_for_each, never through a loop that carries a loop contract (Kani would replace such a loop by its contract regardless of the unwind bound).Items requiring committee sign-off
T— machine-checked: every property the code observes, over the seven shapes above; argued: that these shapes stand for allT(parametricity of the code inT); the slice drop glue is bounded evidence only.write_itertranscription — machine-checked: the transcription unbounded (check_write_iter_*), the shipped text bounded (bounded_write_iter_shipped_text_*); argued:Iterator::fold≡ repeatednext(); why it cannot be otherwise at any current pin is derived above.Diff to the reviewed revision
mainmerged (toolchain nightly-2026-02-05, Kani pin152c6a8, CBMC 6.10.0). At this pin Kani compares a stub's signature with the original's by the position of their own generic parameters, which rejects a free function as the stub of a method ("Cannot stubVecDeque::<T, A>::write_iter_loop… Expected type&mut VecDeque<T, A>for parameter 1, but found&mut VecDeque<T, A>"); the replacement is therefore now a#[cfg(kani)]-only sibling method (VecDeque::write_iter_contract_replacement), which is the only change needed for the reviewed revision to build at the new pin.#[cfg(kani)]items only):write_iter's#[requires]now callswrite_iter_precondition; newwrite_iter_precondition,write_iter_for_each; doc rewrites onwrite_iter/write_iter_loop.mod verify: module documentation (method, shape matrix, evidence table, pinned-Kani limitations);Shapetrait with the fill hook, the three new shapes,finish()(leaks the deque forneeds_dropshapes, drops it otherwise); the eightlet _ = ptr::read(..)alias-drop sites becamemem::forget(ptr::read(..))(a bitwise copy of a live element must not run a destructor);finish(deque)at the end of the 16 harnesses instantiated withWithDrop;T: Shapebounds; the replacement renamed and moved as described; new instantiation lines;bounded_evidence.Local results
Pinned Kani
152c6a8(the pin CI uses since #613), CBMC 6.10.0, CI's flags (-Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi -Z loop-contracts -Z quantifiers -Z stubbing --no-assert-contracts --cbmc-args --object-bits 12): 172 of 172 harnesses verified, 0 failures, all 918kani::coverwitnesses SATISFIED. Sequential run (one harness at a time, full output): 53 min 35 s wall including the library build, 42.5 min of CBMC time in total — 32.2 min for the 125 reviewed harnesses (heaviest:check_wrap_copy_u64107 s,check_copy_u8x3101 s,check_wrap_copy_u899 s,check_insert_u892 s), 10.3 min for the 47 added ones (heaviest:bounded_rotate_permutes_range_u8180 s,bounded_write_iter_shipped_text_u8x364 s; the 34 new shape instantiations average 6 s). Parallel run (-j, 12 cores): 12 min 12 s wall. Reports: the loop-invariant base/step checks appear incheck_write_iter_*andcheck_retain_mut_*only; among the bounded harnesses onlybounded_retain_mut_drop_glue_withdropcarries loop-contract instrumentation (itsretain_mutloops, by design); the replacement's checks are visible by name in the reports of its callers (write_iter precondition,write_iter callers pass TrustedLen iterators,write_iter replacement: iterator advanced by hi).🤖 Generated with Claude Code
https://claude.ai/code/session_01XjuqtSUTkxA5kjmq32PEoJ