Verify iter adapter unsafe methods + safe abstractions (challenge #16) - #602
Verify iter adapter unsafe methods + safe abstractions (challenge #16)#602MavenRain wants to merge 5 commits into
Conversation
…del-checking#16) Kani harnesses for core::iter::adapters: - Unsafe (__iterator_get_unchecked / next_unchecked / get_unchecked): cloned, copied, enumerate, fuse, map, skip, zip. - Safe abstractions with internal unsafe: array_chunks::next_back_remainder, copied::spec_next_chunk, map_windows Buffer::{as_array_ref, as_uninit_array_mut, push, drop}, step_by::original_step, zip::{next, next_back}. Each harness takes a symbolic-length sub-slice of a fixed bounded array and is instantiated per representative element type ((), u8, char, (char, u8)) via macros. The generic unsafe trait methods use plain #[kani::proof] with the #[requires] precondition established by construction, since Kani cannot put contracts on generic trait methods (kani#1997). Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
…ing#16) Adds Kani harnesses for the iterating / chunk-building iterator-adapter methods deferred from the initial model-checking#16 set for tractability: array_chunks::fold filter::next_chunk_dropless filter_map::next_chunk take::spec_fold / spec_for_each zip::nth / fold / spec_fold Each is instantiated over (), u8, char and a composite tuple type (32 harnesses total); all verify with 0 failures. These methods loop over a symbolic-length slice. Drafted without an explicit unwind bound, CBMC over-unwound the loop and timed out; adding assumption) plus a tightened MAX_LEN makes each verify in under a second.
upstream_test runs ./x fmt --check with rust-lang/rust's rustfmt.toml (style_edition 2024, use_small_heuristics = Max). Reflow the check_zip_safe! invocations and the Filter::new call one argument per line. Formatting only. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
|
Status update for reviewers: this PR is green on every check and mergeable It covers the complete unsafe surface of the iter adapters: all The one design decision worth a maintainer's eye is that kani#1997 workaround: The iterating adapters listed as deferred are already verified on a follow-up |
There was a problem hiding this comment.
Pull request overview
Adds Kani-gated verification harnesses for Challenge 16’s iterator adapters. However, generic and unbounded verification requirements remain unmet, with additional uncovered edge cases.
Changes:
- Adds proofs for unsafe iterator accessors.
- Verifies safe adapter abstractions and buffer operations.
- Uses symbolic slices, representative types, and bounded loop unwinding.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
array_chunks.rs |
Verifies remainder and fold operations. |
cloned.rs |
Covers unchecked cloning accessors. |
copied.rs |
Covers unchecked access and chunk copying. |
enumerate.rs |
Verifies unchecked enumerated access. |
filter.rs |
Checks dropless chunk construction. |
filter_map.rs |
Checks mapped chunk construction. |
fuse.rs |
Covers unchecked fused access. |
map.rs |
Covers unchecked mapped access. |
map_windows.rs |
Verifies internal buffer operations. |
skip.rs |
Covers unchecked skipped access. |
step_by.rs |
Verifies step reconstruction. |
take.rs |
Checks specialized fold operations. |
zip.rs |
Verifies unchecked and safe zip operations. |
Suppressed comments (1)
library/core/src/iter/adapters/enumerate.rs:365
- Replacing contract proofs with concrete
#[kani::proof]monomorphizations does not meet Challenge 16's explicit requirement that the result hold for genericTwith no monomorphization. Representative size/alignment classes cannot cover type-dependent behavior such as drop glue and validity niches. This needs an accepted generic encoding or an explicit committee change to the challenge criteria before these harnesses can count as a solution.
// NOTE: `__iterator_get_unchecked` is a trait method on the *generic* impl
// `impl<I> Iterator for Enumerate<I>`, and Kani cannot attach a
// `proof_for_contract` to a generic trait method (kani#1997). So instead of
// the contract machinery we use a plain `#[kani::proof]` that establishes the
// method's precondition by construction (`idx < self.iter.size_hint().0`) and
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| #[kani::unwind(7)] | ||
| fn $harness() { | ||
| const MAX_LEN: usize = 6; | ||
| const N: usize = 4; |
| #[kani::unwind(6)] | ||
| fn $harness() { | ||
| const MAX_LEN: usize = 5; | ||
| const N: usize = 3; |
| let mut it = FilterMap::new( | ||
| any_slice(&array).iter(), | ||
| maybe_map::<$elem_ty> as fn(&$elem_ty) -> Option<usize>, | ||
| ); |
| fn any_zip_iter<'a, T, U>( | ||
| orig_slice_a: &'a [T], | ||
| orig_slice_b: &'a [U], | ||
| ) -> Zip<crate::slice::Iter<'a, T>, crate::slice::Iter<'a, U>> { | ||
| Zip::new(any_slice(orig_slice_a).iter(), any_slice(orig_slice_b).iter()) |
| check_buffer!(verify_map_windows_unit, (), 3); | ||
| check_buffer!(verify_map_windows_u8, u8, 3); | ||
| check_buffer!(verify_map_windows_char, char, 2); | ||
| check_buffer!(verify_map_windows_tup, (char, u8), 2); |
| let idx = kani::any_where(|i: &usize| *i < crate::iter::Iterator::size_hint(&it).0); | ||
| let _ = unsafe { it.__iterator_get_unchecked(idx) }; |
| // Safe `Zip` methods on `TrustedRandomAccess` sources drive the same | ||
| // `get_unchecked`-based machinery as `__iterator_get_unchecked`; these prove | ||
| // `next` / `nth` / `next_back` / `fold` / `spec_fold` keep their internal | ||
| // indexes in bounds. Bounded `MAX_LEN` because the methods iterate. |
| fn any_skip_iter<'a, T>(orig_slice: &'a [T]) -> Skip<crate::slice::Iter<'a, T>> { | ||
| let slice = any_slice(orig_slice); | ||
| let n = kani::any_where(|offset: &usize| *offset <= slice.len()); | ||
| Skip::new(slice.iter(), n) |
| /// One `proof_for_contract` harness per concrete element type; the contract | ||
| /// itself stays generic. `slice::Iter<T>` is `TrustedRandomAccessNoCoerce` | ||
| /// for every `T`, satisfying the method's `Self: TrustedRandomAccessNoCoerce`. |
feliperodri
left a comment
There was a problem hiding this comment.
Review of PR #602 — Challenge 16 (iter adapters)
This is careful, honest, non-vacuous work. There is no fatal soundness problem: no cfg-swap vacuity (all verify modules are #[cfg(kani)]), no loop_invariant(true), and the unsafe-accessor harnesses use the correct pattern of assuming the documented precondition rather than the conclusion. Unfortunately it does not meet the two hard success criteria Challenge 16 states verbatim, so I cannot approve as-is.
Blocker 1 — "The verification must be unbounded—it must hold for slices of arbitrary length."
Every harness that actually iterates is bounded by a small fixed backing array plus #[kani::unwind(N)], so it proves nothing beyond that bound:
array_chunks.rsfold—MAX_LEN4/6,#[kani::unwind(5/7)](diff macro lines 52–69).filter.rsnext_chunk_dropless—MAX_LEN = 6,#[kani::unwind(7)](lines 319–338).filter_map.rsnext_chunk—MAX_LEN = 5,#[kani::unwind(6)](lines 377–396).take.rsspec_fold/spec_for_each—MAX_LEN = 5,#[kani::unwind(6)](lines 730–757).zip.rsnext/nth/next_back/fold/spec_fold—MAX_LEN16/5,#[kani::unwind(6)](lines 816–899).copied.rsspec_next_chunk—MAX_LEN = 16(lines 191–207).
The authors are honest about this ("Bounded MAX_LEN because the methods iterate", diff line ~815; take.rs line ~729), which is appreciated — but honesty does not satisfy the criterion. This is the standard loop_contracts/kani::unwind-with-termination gap; the challenge explicitly requires arbitrary length, so these need loop contracts (or another accepted unbounded encoding), not finite unrolling. (Copilot raised the same point at zip.rs:744.)
Note also the enumerate.rs doc comment (diff lines ~227–230) claiming any_slice "is what makes the proof unbounded". That is inaccurate — the slice length is symbolic but capped at MAX_LEN. For the non-iterating ZST/u8 accessor proofs with MAX_LEN = isize::MAX/u32::MAX this is arguably effectively unbounded, but the char/tuple variants at MAX_LEN = 50 are plainly bounded. Please soften/correct that comment.
Blocker 2 — "The verification must hold for generic type T (no monomorphization)."
Every harness is monomorphized over a fixed menu of representative types ((), u8, char, (char, u8), and paired types for zip). No harness is generic. I recognize Kani fundamentally requires concrete entry types, so full genericity may be infeasible, and representative-type coverage is the usual pragmatic compromise. But the criterion is stated explicitly, so this should at minimum be called out and justified in the PR, and the type menu is thin for some obligations (see below).
Non-blocking soundness/coverage notes
get_unchecked(zip.rs) is only reached transitively. The zip harnesses call__iterator_get_unchecked, which reachesZipImpl::get_uncheckedonly on a freshly builtZipatindex == 0, so theself.index + idxaccumulated-state path is never exercised. The challenge listsget_uncheckedas a separate target; add a direct proof over arbitrary validZipstate (index > 0). (Copilot zip.rs:730.)MAY_HAVE_SIDE_EFFECT = falsebranches are compiled out. All zip/skip harnesses wrapslice::Iter, so the specializednth/next_back(zip) and theidx == 0prefix-advance branch (skip.rs:182–185) with side-effecting sources are never verified. Add aTrustedRandomAccesssource withMAY_HAVE_SIDE_EFFECT = true. (Copilot zip.rs:718, skip.rs:318.)- map_windows drop-safety path not exercised.
any_bufferonly instantiatesCopy, trivial-drop types, sopush's "updatestartbeforedrop_in_place" panic-safety reasoning and non-trivial drop glue are never covered. Add a drop-requiring (ideally panicking-drop) type. (Copilot map_windows.rs:368.) filter_mapoutput type pinned tousize.needs_drop::<B>()is always false, so theGuard::droppath the comment claims to cover is compiled out. Parameterize the mapped output type. (Copilot filter_map.rs:254.)N = 0chunk case omitted forfilter/filter_mapnext_chunk*. Worth adding for completeness (Copilot filter.rs:253, filter_map.rs:249).
Bottom line
Sound, non-vacuous, well-documented harnesses covering (nominally) all listed functions — but the challenge's two explicit gate criteria (unbounded + generic) are not met, and get_unchecked/side-effecting/drop paths are under-covered. Requesting changes primarily on the unbounded criterion (achievable via loop contracts) and the missing direct get_unchecked proof; the monomorphization criterion should at least be explicitly justified.
…rect get_unchecked, side-effect and drop-observing sources Review items (feliperodri, 2026-08-16): - F-B1 unbounded: attach Kani loop invariants to the iterating std loops in scope and drop the unwind bounds of the harnesses that drive them. take.rs spec_fold/spec_for_each (`kani::index <= end`), array_chunks.rs fold (`i <= inner_len`, frame pinned with `kani::loop_modifies(&accum, &i)` because the `from_fn` closure borrows `self`), zip.rs TrustedRandomAccess fold (`kani::index <= len`) and nth (`self.index <= end`). All attributes are additive and runtime no-ops (precedent: slice/mod.rs, str/pattern.rs, num/dec2flt/decimal_seq.rs). MAX_LEN follows the accessor menu: isize::MAX for the ZST, u32::MAX for u8, 10 for char and (char, u8) whose symbolic arrays are built element-wise. Harnesses whose loop lives outside the challenge scope (Filter/FilterMap next_chunk through the generic Iterator::try_fold default, the TrustedLen Zip::spec_fold inner loop over generic next(), the next_back trim loops over generic next_back()) stay bounded and say why. - F-doc: enumerate.rs comments no longer claim unbounded proofs. - F-N1: check_zip_get_unchecked_direct exercises ZipImpl::get_unchecked at a symbolic index > 0 on a struct-literal Zip. - F-N2: check_zip_side_effect (nth, next_back) and check_skip_get_unchecked_side_effect use Map-wrapped sources with MAY_HAVE_SIDE_EFFECT = true. - F-N3: map_windows harnesses add a DropToken element type. - F-N4: filter_map harnesses take an output type other than usize (char, (), (char, u8), DropToken). - F-N5: N = 0 stays excluded because Kani confirms an out-of-bounds write in Filter::next_chunk_dropless::<0> and FilterMap::next_chunk::<0> (upstream issue to follow). Kani also found that the debug assertion after the trim loops in the TrustedRandomAccess Zip::next_back fires when exactly one side has side effects and it is the shorter one; the two one-sided next_back harnesses exclude that shape and document it (upstream issue to follow). All 52 harnesses in take.rs, array_chunks.rs and zip.rs verify on fresh builds with the CI flags; rustfmt (nightly-2025-10-09, upstream config) is clean on the nine files. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
Challenge 16: Verify the safety of
core::iteradaptersTowards challenge #16 (tracking issue #280). Adds Kani harnesses for the unsafe
methods and safe abstractions in
library/core/src/iter/adapters/.This revision answers the 2026-08-15 review (feliperodri: CHANGES_REQUESTED,
blockers F-B1 unbounded and F-B2 generic
T, plus notes F-N1..F-N5; Copilot:the same points inline, plus the enumerate.rs doc comment). Section "Response
to review" below goes item by item. Section "std code changes" lists every
line changed outside the
#[cfg(kani)] mod verifymodules. Section "Harnessinventory" lists every harness with its bound and element types. Section
"Known upstream findings" lists the two defects the harnesses found in the
current upstream code and how the harness domains exclude them.
Unsafe methods (proven free of UB)
cloned__iterator_get_unchecked,next_uncheckedcopied__iterator_get_uncheckedenumerate__iterator_get_uncheckedfuse__iterator_get_uncheckedmap__iterator_get_unchecked,next_uncheckedskip__iterator_get_unchecked(plain source, and aMAY_HAVE_SIDE_EFFECT = truesource)zip__iterator_get_unchecked, andZipImpl::get_uncheckeddirectly over arbitrary validZipstateThe
__iterator_get_uncheckedharnesses read at a symbolic in-bounds index(
kani::any_where(|i| i < size_hint().0)) over akani::any()backing array;the
next_uncheckedharnesses establish the non-empty precondition byconstruction.
Safe abstractions (proven free of UB)
array_chunks::next_back_remainderandarray_chunks::fold,copied::spec_next_chunk,filter::next_chunk_dropless,filter_map::next_chunk, themap_windowsBufferoperations (as_array_ref,as_uninit_array_mut,push,drop),step_by::original_step,take::spec_foldandtake::spec_for_each, andzip::next/next_back/nth/fold/spec_fold.Approach
#[cfg(kani)] mod verifyat the end of each adapter file.proof_for_contractto a generic trait method(kani#1997), each generic method is instantiated at representative concrete
element types via macros, with the caller precondition established by
construction rather than as a
#[requires].u8(1-byte),char(4-byte,validity-constrained), compound tuples with padding such as
(char, u8)and(u32, i16), and, where a code path is gated onneeds_drop, aDropTokentype with a real destructor;
zipharnesses pair two of them.#[safety::loop_invariant(...)](the repo's loop-contract spelling, alreadyused on plain
forloops inslice/mod.rs,str/pattern.rs,num/dec2flt/decimal_seq.rs), so the iterating harnesses have no#[kani::unwind]for those loops and the slice length is symbolic instead ofunrolled. The invariants bound only the loop counter, because the loop
frame Kani infers (or, for
array_chunks, the frame stated withkani::loop_modifies) writes only the counter and the accumulator, so everysize relation the body needs survives as an entry value. Where a loop
cannot take a contract (it calls
nexton a generic inner iterator, or itlives in the generic
Iterator::try_folddefault), the harness staysbounded and the code comment says why.
added
#[cfg(kani)]import, or an added comment.Response to review
F-B1: unbounded verification
Status: the production loops that the harnesses drive now carry
#[safety::loop_invariant], and the corresponding harnesses have no#[kani::unwind]for those loops (an unwind attribute is kept only where asibling construct with a length-independent trip count needs one, stated per
row). 23 loop-contract harnesses (
take8,array_chunks4,zip11:foldx4,nthx4,nthside-effect x3) run with a symbolic slice lengthand no unrolling depth on the contracted loop.
Two adapters (
filter,filter_map) and four narrowarray_chunks/zip/skippaths stay bounded, for the reasons below. Localrun status is in "Verification status".
Now verified by a loop invariant on the production loop:
take.rsSpecTake::spec_fold(TrustedRandomAccess impl),for i in 0..endkani::index <= endcheck_take_spec_fold_unit,check_take_spec_fold_u8,check_take_spec_fold_char,check_take_spec_fold_tup(none)take.rsSpecTake::spec_for_each(TrustedRandomAccess impl),for i in 0..endkani::index <= endcheck_take_spec_for_each_unit,check_take_spec_for_each_u8,check_take_spec_for_each_char,check_take_spec_for_each_tup(none)array_chunks.rsSpecFold::fold(TrustedRandomAccessNoCoerce impl),while inner_len - i >= Ni <= inner_len, with#[cfg_attr(kani, kani::loop_modifies(&accum, &i))]check_array_chunks_fold_unit,check_array_chunks_fold_u8,check_array_chunks_fold_char,check_array_chunks_fold_tup(none)zip.rsZipImpl::fold(TrustedRandomAccessNoCoerce impl),for i in 0..lenkani::index <= lencheck_zip_fold_unit,check_zip_fold_u8,check_zip_fold_char_u8,check_zip_fold_tup(none)zip.rsZipImpl::nth(TrustedRandomAccessNoCoerce impl),while self.index < endself.index <= endcheck_zip_nth_unit,check_zip_nth_u8(unwind(3)),check_zip_nth_char_u8,check_zip_nth_tup(unwind(12)),check_zip_nth_side_effect_a,check_zip_nth_side_effect_b,check_zip_nth_side_effect_both(unwind(3)). The unwind attribute covers onlysuper_nth, which runs at most one iteration after the contracted loop has consumedmin(n, len - index)items, and (forcharand the tuple) the element-wisekani::anyconstruction of theMAX_LENbacking arrays; neither count depends on the slice length.Why the invariants only bound the counter. The loop frame that Kani's loop
contract machinery infers for these loops is
{acc, i}(or{accum, i}, or{self.index}forzip::nth):end,len,self.iter,self.a,self.bare not havoced, so
end <= self.iter.size()andend <= self.len <= min(a.size(), b.size())stay available to the body asentry values and do not need restating. A method call on the captured
selfinside the invariant (for example
self.iter.size() == ...) is not somethingthe current Kani/CBMC pipeline evaluates (it becomes a nondeterministic
value), so the counter-only form is both sufficient and the only form that
proves. For
array_chunks::foldthe frame is stated explicitly withkani::loop_modifies(&accum, &i)(precedent:slice/mod.rs), because thefrom_fnclosure borrowsselfmutably and the inferred frame wouldotherwise havoc the whole adapter,
remainderincluded. Each productioncomment says this.
What "unbounded" means here, stated precisely: the loop is verified by
invariant (CBMC havocs the loop frame and checks the invariant inductively),
so no unrolling depth truncates the number of iterations. The slice length is
still a symbolic value in
0..=MAX_LENwhereMAX_LENis the size of thekani::any()backing array. For the loop-contract harnessesMAX_LENisisize::MAXfor the ZST (every length a Rust slice can have; 50 for thezippair, whose two arrays are separate allocations),u32::MAXforu8,and 10 for
charand the padded tuples. The 10 is a solver-time choice, nota proof-structure one: the loop is the same monomorphic code at every
MAX_LEN, and the cost that scales withMAX_LENfor the wide types is theelement-wise
kani::anyconstruction of the backing array under theloop-contract (DFCC) transformation, not the loop. The ZST and
u8instantiations run the same contracted loop at
isize::MAX/u32::MAX, sothe loop contract is exercised at full length there. If the committee wants
the
char/tuple caps raised, that is a one-number change per macroinstantiation, at the price of longer runs.
Still bounded, and why:
filter.rsnext_chunk_dropless(8 harnesses,MAX_LEN = 6,unwind(7))and
filter_map.rsnext_chunk(12 harnesses,MAX_LEN = 5,unwind(6)).Both fill the chunk through the generic default
Iterator::try_foldiniterator.rs(lines 2424-2435, awhile let Some(x) = self.next()loop thatcalls a generic
FnMut), so the adapter has no loop of its own to annotate,and the tree has no loop contract on the generic default; annotating
iterator.rsis out of this challenge's scope. The fixedMAX_LENis acomplete cover of the reachable state space rather than a truncation: the
only loop-carried state is
initializedin0..=N, so anyMAX_LEN >= N + 2reaches every configuration (empty source, saturation before exhaustion,
exhaustion before saturation). The code comment in each file says this.
array_chunks.rsnext_back_remainder(4 harnesses,MAX_LEN8/8/9/8, nounwind attribute). The fill runs
len % N < Nsteps whatever the slicelength is, so
MAX_LEN >= Nplus the symbolic length covers every reachableremainder.
zip.rsSpecFold::spec_fold(TrustedLen impl):check_zip_spec_fold_unit,_u8,_char_u8,_tup(MAX_LEN = 5,unwind(7)). The innerfor _ in 0..uppercallsnexton the generic inner iterators, which movestheir private pointer state; no public interface lets a loop invariant pin
that state to its allocation, so a loop contract would havoc it into an
unverifiable read. The outer
loopruns once perusize::MAXchunk, soexactly once for these sources. The code comments at both loops say this.
zip.rsspecializednext_backlength-adjust loops with side-effectingsources:
check_zip_next_back_side_effect_a,_b,_both(MAX_LEN = 5,unwind(7)). Same blocking construct:next_backon the generic inneriterators. With plain
slice::Itersources (check_zip_next_back_unit,_u8,_char_u8,_tup) these loops are compiled out(
MAY_HAVE_SIDE_EFFECT = false), so those four harnesses have no unwindattribute; they share the
check_zip_safe!instantiation with theloop-contracted
nth/foldsiblings, soMAX_LENis 50 /u32::MAX/ 10 /10 (the same values as those siblings, not the accessor menu).
skip.rscheck_skip_get_unchecked_side_effect(MAX_LEN = 5,unwind(7)): theidx == 0prefix-drop loopfor skipped_idx in 0..self.nruns
self.ntimes throughtry_get_uncheckedon the innerMap, andself.nis symbolic up to the slice length, so this harness is bounded byMAX_LEN.zip.rssuper_nth(while let Some(x) = Iterator::next(self)) and thegeneric (non-
TrustedLen)SpecFold::spec_fold: no size relation isavailable to an invariant for a general
Zip.super_nthis only reachedafter the contracted
nthloop and runs at most one iteration (covered bythe
nthharnesses' unwind attribute); the genericspec_foldis notselected for any harnessed source (all are
TrustedLen). Both carry acomment saying so.
The remaining harnesses (
__iterator_get_unchecked/next_uncheckedaccessors,
copied::spec_next_chunkwhich is a single bulk copy,step_by::original_step, themap_windowsBufferoperations) have noper-element loop; they were never unwind-bounded and are unchanged except for
copied::spec_next_chunk, whoseMAX_LENis raised from 16 to the accessormenu.
F-B2: generic
TKani monomorphizes: every harness is a concrete entry point, and CBMC verifies
GOTO for concrete types, so a Kani proof "over
T" is always a proof over theset of instantiations the harnesses request. What the harnesses can do is pick
that set so that it spans every property of
Tthe unsafe code can observe.The code under proof reads
Tonly throughsize_of,align_of, validity ofthe bit pattern, and
needs_drop; it never branches onT's identity. Themenu:
(): ZST. Size 0 and align 1, so every pointer arithmetic degenerates andthe "no allocation" edge is exercised.
u8: the trivial 1-byte, no-niche, no-padding case.char: 4-byte, aligned, with invalid bit patterns.kani::any::<char>()is a filtered map over
u32(it assumes the scalar is a validchar), so aread that lands on padding, uninitialized memory, or a foreign slot surfaces
as a validity failure instead of passing silently.
(char, u8)(and(u32, i16)on the secondzipside): compound withinterior padding, so byte-wise copies and
MaybeUninitslots carryuninitialized bytes.
DropToken(au8newtype with aDropimpl that reads its payload) on thepaths gated by
needs_drop:map_windows::Buffer(push'sdrop_in_placeand the
Dropimpl) andfilter_map::next_chunk(theGuard), so real dropglue is compiled in and every dropped slot must be live and in bounds.
This is the same menu the accessor harnesses in
enumerate.rsandcopied.rsof this PR already used, and it is applied uniformly to theloop-contract harnesses now. A universally quantified proof over
Tis notexpressible in Kani (kani#1997 for the contract side; CBMC's model on the
solver side), so this is representative-instantiation coverage; the PR states
that plainly rather than claiming otherwise, and the "Honest caveats" note
about monomorphization is kept below.
F-doc: enumerate.rs doc comments
Both comments Copilot flagged (and the one feliperodri flagged) are corrected:
the
any_slicedoc no longer says the sub-slice "makes the proof unbounded";it now says the length is symbolic in
0..=MAX_LENand that the proof isstill bounded by
MAX_LEN(with theu32::MAX/isize::MAXvs 50 splitspelled out). The macro doc no longer says "
proof_for_contract... thecontract itself stays generic"; it says one plain
#[kani::proof]perconcrete type and points at the NOTE that explains kani#1997. The
"Bounded
MAX_LENbecause the methods iterate" comments intake.rs,array_chunks.rsandzip.rsare replaced by the loop-contract wording.F-N1: direct
ZipImpl::get_uncheckedproofNew macro
check_zip_get_unchecked_direct(5 harnesses:check_zip_get_unchecked_direct_unit_unit,_u8_u8,_char_u8,_u8_char,_tup_tup). It buildsZip { a, b, index, len }directly with symboliclen <= min(a.len(), b.len())and symbolicindex <= len(every statereachable from the
TrustedRandomAccessconstructor, sincenextonlyincrements
indexandnext_backonly decrementslen), picksidx < len - index(the method's caller contract,idx < size_hint().0), andcalls
ZipImpl::get_unchecked(&mut it, idx). This provesself.index + idxstays in bounds of both sources and cannot overflow, on
index > 0states aswell. The transitive
__iterator_get_uncheckedharnesses stay. All five areSUCCESSFUL locally.
F-N2:
MAY_HAVE_SIDE_EFFECT = truesourcesmap.rspinsMAY_HAVE_SIDE_EFFECT = truefor everyMap<I, F>: TrustedRandomAccessNoCoerce, soMap<slice::Iter<u8>, fn(&u8) -> u8>is a side-effecting
TrustedRandomAccesssource without any test-only model.skip.rs:check_skip_get_unchecked_side_effectwraps thatMapinSkipwith symbolicn <= slice.len()and symbolicidx, so theif Self::MAY_HAVE_SIDE_EFFECT && idx == 0branch is compiled in and coveredboth taken (
idx == 0, dropping thenskipped items) and not taken.zip.rs:check_zip_side_effect!instantiated for theMapon sidea,side
b, and both (check_zip_nth_side_effect_a/_b/_both,check_zip_next_back_side_effect_a/_b/_both), so theA::MAY_HAVE_SIDE_EFFECT/B::MAY_HAVE_SIDE_EFFECTbranches of thespecialized
nthandnext_back(includingnext_back's length-adjustloops) are compiled in. The two
any_slicelengths are independent, so thesz_a != sz_badjust path is reachable.next_backside-effect harnesses, and theexclusion is a finding rather than a gap: with exactly one side-effecting
side, and that side shorter than the plain side, upstream's
debug_assert_eq!(self.a.size(), self.b.size())after the trim step fires(the trim loops only run for the
MAY_HAVE_SIDE_EFFECTside, so the plainlonger side is never trimmed). Kani reports it as a failed assertion in a
debug build. The macro takes a
$back_shapepredicate(
len_a >= len_bfor_a,len_b >= len_afor_b,truefor_both),the code comment says why, and the defect is reported upstream (see "Known
upstream findings"). The
nthside-effect harnesses have no suchexclusion.
F-N3:
map_windowsdrop-safety pathverify_map_windows_dropinstantiatescheck_buffer!atDropToken(
needs_droptrue), sopush'sdrop_in_placeand theBufferDropimplexecute real drop glue whose destructor reads the payload; every dropped slot
must be an in-bounds, live slot. Caveat, also in the code comment: Kani
models a panic as a verification failure and has no unwinding, so the
panic-during-drop ordering argument ("update
startbeforedrop_in_place")is not expressible as a passing harness; the coverage this adds is the
non-trivial drop-glue path.
F-N4:
filter_mapoutput typesmaybe_mapis replaced bymaybe_map_to<T, B: kani::Arbitrary>and the macrotakes the output type. The 8 existing harnesses keep
B = usize; four newharnesses cover
B = char,(),(char, u8), andDropToken(
check_filter_map_next_chunk_out_char/_out_unit/_out_tup/_out_drop). WithDropToken,needs_drop::<B>()is true, so theGuard::droppath iscompiled in as the comment claims.
F-N5:
N = 0N = 0is excluded on purpose, and the reason is now a finding rather than anomission. Kani confirms an out-of-bounds write in the current upstream code:
Filter::next_chunk_dropless::<0>writes througharray.get_unchecked_mut(idx)before it comparesinitialized < N, andFilterMap::next_chunk::<0>does a one-elementcopy_nonoverlappingintoguard.arrayatidxbefore it comparesguard.initialized < N; on anysource that yields at least one element, both write into a zero-capacity
array (Kani 0.65.0: 6 of 272 checks failed for
filter, 1 of 294 forfilter_map).doc/src/general-rules.mddoes not permit a local change toruntime logic, so the fix must land upstream: rust-lang/rust issue
<>. The harnesses cover
N >= 1;N = 1harnesses(
*_n1, the smallest valid capacity) are added next to theN = 4(filter)and
N = 3(filter_map) ones. Once the upstream fix lands and is mergedhere,
N = 0instantiations are a one-line addition per macro.std code changes (outside
mod verify)Every production line touched, with the reason. All are runtime no-ops:
#[safety::loop_invariant]expands to nothing outside Kani(
library/contracts/safety/src/runtime.rsreturns the statement unchanged),#[cfg_attr(kani, ...)]and#[cfg(kani)]items are compiled out, and therest are comments. No loop changed shape. Precedent for a
loop_invarianton a plainforloop withkani::index:slice/mod.rs:1014-1019(also theloop_modifiesprecedent),str/pattern.rs:1974,num/dec2flt/decimal_seq.rs:108.array_chunks.rs:6-7#[cfg(kani)] use crate::kani;kani::loop_modifiesinto scopearray_chunks.rs:235-242#[safety::loop_invariant(i <= inner_len)],#[cfg_attr(kani, kani::loop_modifies(&accum, &i))]on the existingwhile inner_len - i >= Nloop inSpecFold::foldfoldharnesses; the explicit frame keeps thefrom_fnclosure's&mut selfborrow from havocingremaindertake.rs:4-5#[cfg(kani)] use crate::kani;kani::indexinto scopetake.rs:304-308#[safety::loop_invariant(kani::index <= end)]on the existingfor i in 0..endinSpecTake::spec_fold(TrustedRandomAccess impl)spec_foldharnessestake.rs:320-321#[safety::loop_invariant(kani::index <= end)]on the existingfor i in 0..endinSpecTake::spec_for_eachspec_for_eachharnesseszip.rs:31-36super_nth: why this loop stays boundedzip.rs:294-298#[safety::loop_invariant(kani::index <= len)]on the existingfor i in 0..lenin the TrustedRandomAccessNoCoerceZipImpl::foldfoldharnesseszip.rs:348-352#[safety::loop_invariant(self.index <= end)]on the existingwhile self.index < endin the TrustedRandomAccessNoCoerceZipImpl::nthnthharnesseszip.rs:405-411next_back: why the two adjust loops stay boundedzip.rs:681-684SpecFold::spec_fold: why thewhile letstays boundedzip.rs:699-702loopof the TrustedLenSpecFold::spec_foldzip.rs:711-716for _ in 0..upperof the TrustedLenSpecFold::spec_fold: why it stays boundedHarness inventory
Bound column: "invariant" means the production loop carries a
#[safety::loop_invariant]and the harness has no unwind attribute (or onethat only covers a length-independent sibling construct, stated); "no loop"
means the harness drives no per-element loop and has no unwind attribute;
"unwind(N)" means bounded finite unrolling with the stated
MAX_LEN.MAX_LENmenu "accessor" =isize::MAXfor(),u32::MAXforu8, 50 forcharand(char, u8); "loop-contract" =isize::MAXfor(),u32::MAXfor
u8, 10 forcharand(char, u8).zippairs use 50 for the ZSTpair.
cloned.rscheck_cloned_get_unchecked_unit/_u8/_char/_tupMAX_LEN(),u8,char,(char, u8)cloned.rscheck_cloned_next_unchecked_unit/_u8/_char/_tupMAX_LENcopied.rscheck_copied_get_unchecked_unit/_u8/_char/_tupMAX_LENcopied.rscheck_copied_spec_next_chunk_unit/_u8/_char/_tup(N= 2/3/2/2)MAX_LEN(raised from 16)enumerate.rscheck_enumerate_get_unchecked_unit/_u8/_char/_tupMAX_LENfuse.rscheck_fuse_get_unchecked_unit/_u8/_char/_tupMAX_LENmap.rscheck_map_get_unchecked_unit/_u8/_char/_tupMAX_LENmap.rscheck_map_next_unchecked_unit/_u8/_char/_tupMAX_LENskip.rscheck_skip_get_unchecked_unit/_u8/_char/_tupMAX_LENslice::Iter)skip.rscheck_skip_get_unchecked_side_effectMAX_LEN = 5(prefix-drop loop runs symbolicntimes)u8viaMap<slice::Iter<u8>, fn>step_by.rscheck_step_by_original_step_unit/_u8/_char/_tupMAX_LEN = 16(proof is over symbolicstep)(),u8,char,(char, u8)map_windows.rsverify_map_windows_unit/_u8/_char/_tup/_drop::{check_as_array_ref, check_as_uninit_array_mut, check_push, check_drop}(N= 3/3/2/2/2)Nis the const generic, no unwind attribute(),u8,char,(char, u8),DropTokenarray_chunks.rscheck_array_chunks_next_back_remainder_unit/_u8/_char/_tup(N= 2/2/3/2)MAX_LEN8/8/9/8, no unwind attribute (fill runslen % N < Nsteps)(),u8,char,(char, u8)array_chunks.rscheck_array_chunks_fold_unit/_u8/_char/_tup(N= 2/2/3/2)loop_modifiesframe), loop-contractMAX_LENtake.rscheck_take_spec_fold_unit/_u8/_char/_tupMAX_LENtake.rscheck_take_spec_for_each_unit/_u8/_char/_tupMAX_LENfilter.rscheck_filter_next_chunk_dropless_unit/_u8/_char/_tup(N = 4) and..._unit_n1/_u8_n1/_char_n1/_tup_n1(N = 1)MAX_LEN = 6(defaulttry_fold; complete state cover)filter_map.rscheck_filter_map_next_chunk_unit/_u8/_char/_tup(N = 3, outusize),..._unit_n1/_u8_n1/_char_n1/_tup_n1(N = 1, outusize),check_filter_map_next_chunk_out_char/_out_unit/_out_tup/_out_drop(inu8,N = 3)MAX_LEN = 5(defaulttry_fold; complete state cover)(),u8,char,(char, u8); out:usize,char,(),(char, u8),DropTokenzip.rscheck_zip_get_unchecked_unit_unit/_u8_u8/_char_u8/_u8_char/_tup_tupMAX_LEN50 /u32::MAX/ 50 / 50 / 50((),()),(u8,u8),(char,u8),(u8,char),((char,u8),(u32,i16))zip.rscheck_zip_get_unchecked_direct_unit_unit/_u8_u8/_char_u8/_u8_char/_tup_tupMAX_LENmenu; symbolicindex <= len <= minzip.rscheck_zip_next_unit/_u8/_char_u8/_tupMAX_LEN50 /u32::MAX/ 10 / 10((),()),(u8,u8),(char,u8),((char,u8),(u32,i16))zip.rscheck_zip_nth_unit/_u8/_char_u8/_tupnth; unwind(3)/(3)/(12)/(12) forsuper_nthand the wide-typekani::anyarray construction only;MAX_LEN50 /u32::MAX/ 10 / 10zip.rscheck_zip_next_back_unit/_u8/_char_u8/_tupslice::Iter);MAX_LEN50 /u32::MAX/ 10 / 10zip.rscheck_zip_fold_unit/_u8/_char_u8/_tupZipImpl::fold;MAX_LEN50 /u32::MAX/ 10 / 10zip.rscheck_zip_spec_fold_unit/_u8/_char_u8/_tupMAX_LEN = 5(inner loop calls genericnext; outerloopruns once)zip.rscheck_zip_nth_side_effect_a/_b/_bothnth; unwind(3) forsuper_nthonly;MAX_LEN = u32::MAXu8/u8, one or both sides throughMap<slice::Iter<u8>, fn>zip.rscheck_zip_next_back_side_effect_a/_b/_bothMAX_LEN = 5(adjust loops call genericnext_back);$back_shapeexcludes the debug-assert shape (see F-N2)Verification status
CI runs every harness through
scripts/run-kani.sh(which passes-Z loop-contractsalongside the other unstable flags). Local runs below useKani 0.65.0 with the same flag set (
verify-std -Z unstable-options ./library -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),one harness per invocation, each on a fresh per-harness build of the current
tree.
Local, SUCCESSFUL, 2026-08-17 (loop-contract and
zipsweep, 42 harnesses,19-46 s wall each):
take.rs:check_take_spec_fold_unit/_u8/_char/_tupandcheck_take_spec_for_each_unit/_u8/_char/_tup(8 of 8).array_chunks.rs:check_array_chunks_fold_unit/_u8/_char/_tupandcheck_array_chunks_next_back_remainder_unit/_u8/_char/_tup(8 of 8).zip.rs:check_zip_fold_unit/_u8/_char_u8/_tup,check_zip_nth_unit/_u8/_char_u8/_tup,check_zip_spec_fold_unit/_u8/_char_u8/_tup,check_zip_next_u8,check_zip_next_back_unit/_u8,check_zip_get_unchecked_direct_unit_unit/_u8_u8/_u8_char/_char_u8/_tup_tup,check_zip_nth_side_effect_a/_b/_both,check_zip_next_back_side_effect_a/_b/_both(26 of 26).Local, SUCCESSFUL, 2026-08-17 (the remaining
zip.rsharnesses, no loopcontract involved, re-run after the
MAX_LENmenu change; 10 of 10, 20-36 swall each):
check_zip_next_unit/_char_u8/_tup,check_zip_next_back_char_u8/_tup,check_zip_get_unchecked_unit_unit/_u8_u8/_u8_char/_char_u8/_tup_tup.Every harness in
zip.rs(36) is therefore green locally on the currenttree.
Local, SUCCESSFUL, 2026-08-16 (files not touched since):
filter.rs: all 8check_filter_next_chunk_dropless*harnesses (0 failedeach; 271 to 563 checks per harness).
filter_map.rs: all 12check_filter_map_next_chunk*harnesses (0 failedeach; 295 to 587 checks per harness).
copied.rs: all 4check_copied_spec_next_chunk*harnesses at the raisedMAX_LEN(0 failed each).skip.rs:check_skip_get_unchecked_side_effect(0 of 222 failed).map_windows.rs:verify_map_windows_drop::{check_drop, check_push, check_as_uninit_array_mut, check_as_array_ref}(0 failed each).enumerate.rs:check_enumerate_get_unchecked_u8after the comment edit(0 failed).
The accessor harnesses in
cloned.rs,copied.rs(get_unchecked),enumerate.rs(the other three),fuse.rs,map.rs,skip.rs(plain),step_by.rs, andmap_windows.rs(unit/u8/char/tup) are unchanged fromthe previous revision, which CI verified; CI re-verifies them here.
Known upstream findings (excluded from the harness domains, issues to be filed)
Both are defects in the current upstream
corecode that Kani surfaced whilebuilding these harnesses. Neither is fixed in this PR
(
doc/src/general-rules.md: no local change to std runtime logic); each isexcluded from the harness domain with a code comment that names the
exclusion, and reported upstream.
Filter::next_chunk_dropless::<0>andFilterMap::next_chunk::<0>writeone element into a zero-capacity array before the
initialized < Ncheck (out-of-bounds write on any non-empty source). Excluded by
instantiating
N >= 1only. Issue: <>.Zip::next_back:debug_assert_eq!(self.a.size(), self.b.size())after the trim step fireswhen exactly one side has
MAY_HAVE_SIDE_EFFECT = trueand that side isthe shorter one (the plain longer side is never trimmed). Debug-only
false assertion, no UB in release. Excluded by the
$back_shapeassumption in
check_zip_side_effect!(_a:len_a >= len_b;_b:len_b >= len_a;_both: no exclusion). Issue:<>.
Honest caveats (please review)
universal-over-
Tproof; the F-B2 answer above says why and whichproperties of
Teach instantiation pins. Glad to switch encodings if thecommittee prefers a different form.
except the ones listed under "Still bounded, and why" (
filter,filter_map,array_chunks::next_back_remainder, the TrustedLenzip::spec_fold, the side-effectingzipnext_backandskipprefix-drop paths). For those, the code comment states the bound and the
state-space argument, and the bound is a property of the generic default
try_fold/ private inner-iterator state, not of the adapter code.MAX_LENfor wide element types. 10 forcharand the tuples in theloop-contract harnesses (50 in the accessor harnesses) is a
kani::any()array size, not an unrolling depth; the
u8/ZST instantiations of the sameloops run to
u32::MAX/isize::MAX.N = 0forfilter/filter_map. Excluded because upstream iscurrently unsound there (see F-N5); tracked upstream at
<>.
zip::next_backwith one side-effecting shorter side. Excludedbecause upstream's debug assertion is false there (see F-N2); tracked
upstream at <>.