Verify safety of iterator adapter functions (Challenge 16) - #549
Conversation
68e2629 to
c1620b6
Compare
74 harnesses proving safety of all 10 unsafe functions and 17 safe abstractions listed in Challenge 16, across 13 iterator adapter files. Unbounded verification via large symbolic arrays, loop contracts, and inductive decomposition. 4 representative types (u8, (), char, (char,u8)) cover all behavioral axes of the generic code.
There was a problem hiding this comment.
Pull request overview
Adds Kani-based formal verification coverage for iterator adapter implementations in library/core/src/iter/adapters/, including harnesses and contract/invariant annotations to justify the safety of unsafe internals and safe abstractions.
Changes:
- Add
#[cfg(kani)]verification modules with Kani harnesses across multiple iterator adapters. - Add/extend safety contracts (
#[requires(...)]) and Kani loop-invariant annotations (#[cfg_attr(kani, ...)]) to enable (mostly) unbounded verification. - Document verification assumptions/limitations inline (e.g., bounded unwind in some harnesses).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| library/core/src/iter/adapters/zip.rs | Adds contracts/loop-invariant annotations and extensive Kani harnesses for Zip unsafe/specialized paths. |
| library/core/src/iter/adapters/take.rs | Adds loop-invariant annotations and Kani harnesses for Take specialized paths. |
| library/core/src/iter/adapters/step_by.rs | Adds Kani harnesses exercising original_step via multiple entry points. |
| library/core/src/iter/adapters/skip.rs | Adds Kani harnesses for Skip::__iterator_get_unchecked. |
| library/core/src/iter/adapters/map.rs | Adds Kani harnesses for Map unsafe methods. |
| library/core/src/iter/adapters/map_windows.rs | Adds Kani harnesses targeting buffer initialization/wrap, clone paths, and drop safety. |
| library/core/src/iter/adapters/fuse.rs | Adds Kani harnesses for Fuse::__iterator_get_unchecked. |
| library/core/src/iter/adapters/filter.rs | Adds bounded + inductive-step Kani harnesses for next_chunk-related unsafe operations. |
| library/core/src/iter/adapters/filter_map.rs | Adds bounded + inductive-step Kani harnesses for next_chunk-related unsafe operations. |
| library/core/src/iter/adapters/enumerate.rs | Adds Kani harnesses for Enumerate::__iterator_get_unchecked. |
| library/core/src/iter/adapters/copied.rs | Adds Kani harnesses for Copied::__iterator_get_unchecked and next_chunk specialization. |
| library/core/src/iter/adapters/cloned.rs | Adds a safety contract for next_unchecked and Kani harnesses for unsafe methods. |
| library/core/src/iter/adapters/array_chunks.rs | Adds comments about loop reasoning and Kani harnesses for next_back and bounded fold. |
- Remove unused `loop_invariant` import in take.rs and zip.rs (#[cfg_attr(kani, kani::loop_invariant(...))] does not require it) - Rewrite `Zip::get_unchecked` `#[requires(...)]` to avoid `self.index + idx` overflow, using subtraction-based bounds - Clarify "vacuous loop invariant" comments in take.rs and zip.rs — note that `true` is intentional and only enables loop-contract mode - Reword "Loop invariant:" to "Safety argument:" in array_chunks.rs to avoid implying a verified invariant where there is none (bounded harness) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove unused `loop_invariant` import in take.rs and zip.rs (#[cfg_attr(kani, kani::loop_invariant(...))] does not require it) - Rewrite `Zip::get_unchecked` `#[requires(...)]` to avoid `self.index + idx` overflow, using subtraction-based bounds - Clarify "vacuous loop invariant" comments in take.rs and zip.rs — note that `true` is intentional and only enables loop-contract mode - Reword "Loop invariant:" to "Safety argument:" in array_chunks.rs to avoid implying a verified invariant where there is none (bounded harness)
|
Hi @feliperodri — addressed the Copilot review in commit e80e689:
All CI green on the fork test PR kasimte#2 (identical tree). Ready for another look when you have a moment. |
|
Thanks for bringing the branch up to date with |
There was a problem hiding this comment.
@kasimte This is strong, honest work — thank you for the transparent write-up of every limitation. To be clear up front about what's good: the source code is verified directly (no #[cfg(not(kani))] body substitution), and the unsafe trait-method harnesses are sound and non-circular — e.g. check_copied_get_unchecked_u8 does kani::assume(idx < iter.size_hint().0) (the documented precondition, not the safety conclusion), then lets CBMC check the real __iterator_get_unchecked(idx) for UB and asserts result == slice[idx]. That's a legitimate manual stand-in for proof_for_contract, and the #[requires] clauses faithfully match the assumed preconditions.
I'm requesting changes on two points.
1. The 5 loop_invariant(true) annotations (take.rs, zip.rs)
All five added loop invariants are #[cfg_attr(kani, kani::loop_invariant(true))], and the code comments call them "intentionally vacuous ... only to enable loop-contract mode." The concern: these loops perform get_unchecked(i) / __iterator_get_unchecked(i), whose safety depends on the induction variable staying < len. A true invariant does not establish that inductive bound, which leaves two possibilities that need to be resolved:
- If loop-contract mode genuinely abstracts the loop, the havoc'd
iis constrained only bytrue, so the indexed access is exercised at an arbitrary index — the proof should then either fail or be relying on something else to re-derive the bound. Please explain why it's sound. - If instead the loop is effectively still being unwound (bounded by
MAX_LEN), then these functions are bounded, not "unbounded via loop contracts" as the summary states — in which case the claim should be corrected.
Either way, please provide a meaningful loop invariant that captures the bound the get_unchecked relies on (e.g. i <= len / self.index <= self.len), as you did locally for array_chunks::fold, or a concrete justification that true is sufficient and the coverage is genuinely unbounded. (This is the same trivial-invariant issue that #327's loop_invariant(true) and the discussion on #544 ran into.)
2. Decorative safety contracts (9 of 10 #[requires])
9 of the 10 #[requires(...)] (the __iterator_get_unchecked / get_unchecked trait methods) have no #[kani::proof_for_contract], so Kani never checks them as contracts — a plain #[kani::proof] that calls the method ignores its contract. I understand proof_for_contract doesn't support trait-impl methods today, and that you mirror each precondition with a kani::assume in a real harness (which is what actually does the verification). That's reasonable, but as written the annotation and the assumed precondition are independent and can silently drift. Please either:
- add a brief note in the code that these
#[requires]are documentation-only (verification is via the mirroredassumeinmod verify), and/or - keep a single source of truth so the contract and the harness precondition can't diverge.
Minor / for the committee
The disclosed limitations — bounded array_chunks::fold, Range<u8> substitution for next_back_remainder, and 4 concrete types standing in for a generic T — are reasonable and clearly documented; I'll defer to the maintainers on whether they satisfy the "unbounded" and "generic" wording of the challenge.
Overall this is close, and the verification approach is sound where it counts. Resolving the loop-invariant question (item 1) is the main blocker.
|
Thanks for the thorough review, @feliperodri — working through both items now (meaningful invariants + the requires notes); will push updates and a full response shortly. |
Replace the five vacuous #[kani::loop_invariant(true)] annotations with real inductive bounds that capture the indexed-access bound (take spec_fold / spec_for_each: kani::index <= end; zip fold: kani::index <= len; nth: self.index <= end; super_nth: self.index <= self.len). Each verifies with base and step checks. Document each trait-impl #[requires] as precondition documentation, verified via the mirrored kani::assume in mod verify. Add a checked #[ensures] with a proof_for_contract harness for the inherent StepBy::original_step, and rewrite Skip::__iterator_get_unchecked's precondition in overflow-safe subtraction form. Correct the bounded-vs-unbounded wording in the harness comments to match what the harnesses verify.
|
Thanks for the detailed review, @feliperodri. Both items are addressed in the pushed commits; details below. 1. Loop invariants (the 5
|
| Site | Invariant now |
|---|---|
take.rs spec_fold / spec_for_each |
kani::index <= end |
zip.rs TRANC fold |
kani::index <= len |
zip.rs TRA nth |
self.index <= end |
zip.rs super_nth |
self.index <= self.len |
(kani::index is Kani's handle for a for loop's iteration count, per the loop-contracts reference; the loop's own pattern variable is not in scope in the invariant position.)
On your question of why the abstracted loop with true was sound rather than failing at an arbitrary index: the loops are genuinely abstracted, not unwound — the loop_invariant base/step checks appear in the verification output — and Kani retains the loop guard around the abstracted body (loop-contracts reference), so the havoc'd index stays < end and get_unchecked(i) was only ever checked under that bound. The explicit invariants now state that bound directly, so it is visible in the annotation and any drift is checkable.
2. Trait-impl #[requires]
Most of these #[requires] are already upstream (#435); because they sit on trait-impl methods, proof_for_contract cannot check them there, so they remain unchecked in main. This PR's harnesses are what exercise them: each #[requires] now carries a note that it documents the precondition and is verified via the mirrored kani::assume in the named verify:: harness, with the two to be kept in sync. I re-checked each assume against its contract expression and found them consistent.
Where proof_for_contract can resolve the method — the inherent StepBy::original_step — this push adds #[ensures(|result| result.get() - 1 == old(self).step_minus_one)] with a proof_for_contract harness. Skip::__iterator_get_unchecked's precondition is also rewritten in the overflow-safe subtraction form, matching the Zip::get_unchecked fix.
Claim corrections
The PR body and the harness comments now separate the two axes: loop contracts remove the unwinding bounds on the annotated loops, while harness input arrays remain bounded at MAX_LEN (5000/u8, 50/char+tuple; ZST at isize::MAX; the spec_fold inductive step is the arbitrary-length case). "unbounded" now appears only where it is literally true, and the requirements table leaves the criterion judgment to the committee.
Also in this push
- Merged current
main(nightly-2025-11-25 subtree update); no runtime-logic changes to the 27 target functions. - CI on the updated branch: green — all
Verify stdpartitions,autoharness, andupstream_test(rustfmt) passing.
The remaining gaps — input-domain bounds and concrete types for T — are disclosed in the body; glad to track them however the committee prefers.
Resolves #280 (Challenge 16: Verify the safety of Iterator functions)
75 Kani harnesses verifying the safety of all 10 unsafe functions and all 17 safe abstractions across 13 iterator adapter files in
library/core/src/iter/adapters/. Most of the unsafe-method#[requires]preconditions already exist upstream (added in #435);mainhas no harnesses for these adapters, so they are not yet machine-checked. This PR adds that verification layer — the harnesses that exercise each precondition and check the guardedget_unchecked/next_uncheckedfor UB. Loop-based paths carry loop-contract invariants that bound the iteration index, so Kani abstracts those loops instead of unwinding them — removing the per-harness unwind bound on those loops. Harness input arrays are bounded at MAX_LEN (5000 for u8; 50 for char/tuple;isize::MAXfor the ZST); thezip::spec_foldinductive-step harness is the one arbitrary-length case. Per-function status is in the tables below.Requirements checklist
Zip::get_uncheckedexercised transitively vianext/nth/fold). Of the 10#[requires], 7 are upstream (#435); this PR adds 2 and rewritesSkip's in overflow-safe form#[ensures]contract fororiginal_stepviaproof_for_contract)isize::MAX) and thespec_foldinductive step. We defer to the committee on whether this satisfies the criterion.Tu8,(),char,(char,u8)) covering all behavioral axes (size_of,align_of, ZST, validity constraints, padding). We defer to the committee's judgment.#[requires](7 upstream from #435, 2 added here, 1 rewritten overflow-safe) are precondition documentation, each mirrored by akani::assumein the harness that exercises it — sinceproof_for_contractcannot resolve trait-impl methods, that harness is where they first get verified; the inherentoriginal_stepcarries a checked#[ensures]viaproof_for_contractKnown limitations
Input-domain bounds: most harnesses draw symbolic slices from fixed-size arrays (MAX_LEN above), so slice length is symbolic only within that bound. The loop-contract annotations remove the unwinding bounds (Kani abstracts those loops); they do not make the input arrays arbitrary-length.
array_chunks::fold: Bounded harness (MAX_LEN=8, unwind 9), N=2/u8 only. TheTrustedRandomAccessNoCoerce-specialized fold path callsfrom_fn, whose internalMaybeUninitloop conflicts with loop-contract mode, so no source-level loop invariant is applied here. The unsafe operation —__iterator_get_unchecked(i + local)guarded byinner_len - i >= N— is the same indexed-access pattern abstracted via loop contracts intake.rsandzip.rs.next_back_remainder: UsesRange<u8>instead ofslice::Iterbecause CBMC exhausts resources on the pointer-heavy adapter chain. Exercises the sameunwrap_err_uncheckedcode path.Generic type
T: The challenge requires verification without monomorphization. Kani requires concrete types (CBMC operates on concrete GOTO programs), so a single generic proof is not possible with this tool. The unsafe operations depend onsize_of::<T>()andalign_of::<T>(), not type identity; the 4 types cover those axes (integer, ZST, validity-constrained, padded). We defer to the committee on whether this satisfies the requirement.Source code modifications
Five loop-contract invariants via
#[cfg_attr(kani, kani::loop_invariant(...))], all verified inductive (base + step):take.rsspec_foldandspec_for_each:kani::index <= endzip.rsfold:kani::index <= len;nth:self.index <= end;super_nth:self.index <= self.lenZero impact on non-Kani builds. Contract changes vs
main: 7 of the 10#[requires]are upstream (#435) and left unchanged; this PR adds 2 (Cloned::next_unchecked,Zip::get_unchecked), rewritesSkip::__iterator_get_unchecked's precondition in overflow-safe subtraction form, and adds 1 checked#[ensures]on the inherentoriginal_stepviaproof_for_contract. The trait-impl#[requires]are precondition documentation (proof_for_contractcannot resolve trait-impl methods); each is mirrored by akani::assumein theverify::harness that exercises it — which is where the preconditions first get checked, asmaincarries them without a harness.Verification details (click to expand)
Quick verification
Expected:
Complete - 75 successfully verified harnesses, 0 failures, 75 total.Verification techniques
kani::slice::any_slice_of_arraywith up to 5000 elements (u8),isize::MAX(ZST), 50 (char/tuple). Slice length is fully symbolic within [0, MAX_LEN].kani::index <= end/len,self.index <= end) on the annotated loops intake.rsandzip.rs; Kani abstracts these loops, so no per-harness unwind bounds are needed for them.k(unbounded ink), supplemented by bounded end-to-end harnesses. Used forzip::spec_fold,filter::next_chunk_dropless,filter_map::next_chunk.StepBy::original_step(inherent method):#[ensures]+#[kani::proof_for_contract].Safety contracts
__iterator_get_unchecked#[requires(idx < self.it.size_hint().0)]next_unchecked#[requires(self.it.size_hint().0 > 0)]__iterator_get_unchecked#[requires(idx < self.it.size_hint().0)]__iterator_get_unchecked#[requires(idx < self.iter.size_hint().0)]__iterator_get_unchecked#[requires(self.iter.is_some() && idx < self.iter.as_ref().unwrap().size_hint().0)]__iterator_get_unchecked#[requires(idx < self.iter.size_hint().0)]next_unchecked#[requires(self.iter.size_hint().0 > 0)]__iterator_get_unchecked#[requires(self.n <= self.iter.size_hint().0 && idx < self.iter.size_hint().0 - self.n)]__iterator_get_unchecked#[requires(idx < self.size_hint().0)]get_unchecked#[requires(self.index <= self.a.size() && idx < self.a.size() - self.index && self.index <= self.b.size() && idx < self.b.size() - self.index)]original_step#[ensures(|result| result.get() - 1 == old(self).step_minus_one)]— checked viaproof_for_contractUnsafe functions (10/10)
__iterator_get_uncheckednext_unchecked__iterator_get_unchecked__iterator_get_unchecked__iterator_get_unchecked__iterator_get_uncheckednext_unchecked__iterator_get_unchecked__iterator_get_uncheckedget_uncheckednext/nth/fold)Safe abstractions (17/17)
next_back_remainderfoldspec_next_chunknext_chunk_droplessnext_chunkas_array_refas_uninit_array_mutpushdroporiginal_stepproof_for_contract)spec_foldspec_for_eachfoldnextnthnext_backspec_foldTest plan
take,zip,skip,step_by) verified locally, with Kani built at thetool_configpin and the CI flag set.Resolves #280
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.