Skip to content

Verify iter adapter unsafe methods + safe abstractions (challenge #16) - #602

Open
MavenRain wants to merge 4 commits into
model-checking:mainfrom
MavenRain:16-iter-adapters
Open

Verify iter adapter unsafe methods + safe abstractions (challenge #16)#602
MavenRain wants to merge 4 commits into
model-checking:mainfrom
MavenRain:16-iter-adapters

Conversation

@MavenRain

@MavenRain MavenRain commented Jun 17, 2026

Copy link
Copy Markdown

Challenge 16: Verify the safety of core::iter adapters

Towards challenge #16 (tracking issue #280). Adds Kani harnesses for the unsafe
methods and safe abstractions in library/core/src/iter/adapters/.

Unsafe methods (proven free of UB)

adapter unsafe methods
cloned __iterator_get_unchecked, next_unchecked
copied __iterator_get_unchecked
enumerate __iterator_get_unchecked
fuse __iterator_get_unchecked
map __iterator_get_unchecked, next_unchecked
skip __iterator_get_unchecked
zip __iterator_get_unchecked

The __iterator_get_unchecked harnesses read at a symbolic in-bounds index
(kani::any_where(|i| i < size_hint().0)) over a kani::any() backing array;
the next_unchecked harnesses establish the non-empty precondition by
construction.

Safe abstractions (proven free of UB)

array_chunks::next_back_remainder and array_chunks::fold,
copied::spec_next_chunk, filter::next_chunk_dropless,
filter_map::next_chunk, the map_windows Buffer operations (as_array_ref,
as_uninit_array_mut, push, drop), step_by::original_step,
take::spec_fold and take::spec_for_each, and zip::next / next_back /
nth / fold / spec_fold.

Approach

  • Harnesses live in a #[cfg(kani)] mod verify at the end of each adapter file.
  • Because Kani cannot attach a proof_for_contract to 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].
  • The representative element types are unit (ZST), u8 (1-byte), char (4-byte,
    validity-constrained), and compound tuples with padding such as (char, u8)
    and (u32, i16); zip harnesses pair two of them. These exercise the distinct
    memory-layout classes, since the unsafe operations depend on
    size_of::<T>()
    and align_of::<T>() rather than on T's identity.

Honest caveats (please review)

Two points where this encoding does not literally match the challenge's success
criteria, written out so the committee can weigh them:

  1. Monomorphization. The criteria ask for a proof that holds for generic T.
    These harnesses instead prove the property at the representative concrete
    instantiations above. The soundness argument is the size-and-align one
    described (the unsafe accesses do not branch on T's identity, and the
    representative types cover ZST, single-byte, 4-byte validity-constrained,
    and
    padded-compound layouts), but this is representative-type coverage rather
    than
    a single universal-over-T proof. The reason is architectural: CBMC
    monomorphizes and Kani has no generic contracts (kani#1997). Glad to switch
    encodings if the committee prefers a different form.
  2. Boundedness. For the single-source accessor harnesses (cloned,
    copied,
    enumerate, fuse, map, skip), the __iterator_get_unchecked proof is
    effectively unbounded in the ZST and u8 cases: the backing length ranges up
    to the type's representable maximum (isize::MAX / u32::MAX elements) with a
    symbolic index. The char and padded-tuple cases of those harnesses use a
    fixed backing length of 50, and zip's __iterator_get_unchecked uses a fixed
    length of 50 for every element-type pair except u8 / u8 (which is
    u32::MAX). The iterating abstractions (zip next/nth/fold/spec_fold,
    take spec_fold/spec_for_each, filter, filter_map,
    array_chunks::fold, step_by) use a bounded length (5 to 16) with an
    explicit #[kani::unwind], because CBMC must unroll the loop. The per-element
    safety obligation is structurally identical at every position, so a modest
    bound exercises it, but coverage there is bounded rather than arbitrary-length.

AI-assisted (Claude); disclosed per repo policy.

…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>
@MavenRain
MavenRain requested a review from a team as a code owner June 17, 2026 00:53
…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>
@MavenRain

Copy link
Copy Markdown
Author

Status update for reviewers: this PR is green on every check and mergeable
against current main.

It covers the complete unsafe surface of the iter adapters: all
__iterator_get_unchecked, next_unchecked, and get_unchecked methods
across cloned, copied, enumerate, fuse, map, skip, and zip, plus the safe abstractions
listed in the description. Each generic trait method is verified at representative
concrete element types via macro instantiation, with the caller precondition
established by construction, since Kani cannot place contracts on generic trait
methods (kani#1997).

The one design decision worth a maintainer's eye is that kani#1997 workaround:
these use plain #[kani::proof] plus precondition-by-construction rather than
proof_for_contract, which proves the same no-UB property the contract would
express. Glad to switch encodings if you would prefer a different form.

The iterating adapters listed as deferred are already verified on a follow-up
branch; I can fold them into this PR or send them separately, whichever you
prefer. Thanks for taking a look whenever you have a chance.

@feliperodri feliperodri self-assigned this Aug 15, 2026
@feliperodri feliperodri added the Challenge Used to tag a challenge label Aug 15, 2026
@feliperodri
feliperodri requested a balanced review from Copilot August 15, 2026 20:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 generic T with 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;
Comment on lines +251 to +254
let mut it = FilterMap::new(
any_slice(&array).iter(),
maybe_map::<$elem_ty> as fn(&$elem_ty) -> Option<usize>,
);
Comment on lines +714 to +718
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())
Comment on lines +365 to +368
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);
Comment on lines +729 to +730
let idx = kani::any_where(|i: &usize| *i < crate::iter::Iterator::size_hint(&it).0);
let _ = unsafe { it.__iterator_get_unchecked(idx) };
Comment on lines +741 to +744
// 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.
Comment on lines +315 to +318
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)
Comment on lines +358 to +360
/// 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 feliperodri assigned MavenRain and unassigned feliperodri Aug 16, 2026

@feliperodri feliperodri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.rs foldMAX_LEN 4/6, #[kani::unwind(5/7)] (diff macro lines 52–69).
  • filter.rs next_chunk_droplessMAX_LEN = 6, #[kani::unwind(7)] (lines 319–338).
  • filter_map.rs next_chunkMAX_LEN = 5, #[kani::unwind(6)] (lines 377–396).
  • take.rs spec_fold / spec_for_eachMAX_LEN = 5, #[kani::unwind(6)] (lines 730–757).
  • zip.rs next / nth / next_back / fold / spec_foldMAX_LEN 16/5, #[kani::unwind(6)] (lines 816–899).
  • copied.rs spec_next_chunkMAX_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

  1. get_unchecked (zip.rs) is only reached transitively. The zip harnesses call __iterator_get_unchecked, which reaches ZipImpl::get_unchecked only on a freshly built Zip at index == 0, so the self.index + idx accumulated-state path is never exercised. The challenge lists get_unchecked as a separate target; add a direct proof over arbitrary valid Zip state (index > 0). (Copilot zip.rs:730.)
  2. MAY_HAVE_SIDE_EFFECT = false branches are compiled out. All zip/skip harnesses wrap slice::Iter, so the specialized nth/next_back (zip) and the idx == 0 prefix-advance branch (skip.rs:182–185) with side-effecting sources are never verified. Add a TrustedRandomAccess source with MAY_HAVE_SIDE_EFFECT = true. (Copilot zip.rs:718, skip.rs:318.)
  3. map_windows drop-safety path not exercised. any_buffer only instantiates Copy, trivial-drop types, so push's "update start before drop_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.)
  4. filter_map output type pinned to usize. needs_drop::<B>() is always false, so the Guard::drop path the comment claims to cover is compiled out. Parameterize the mapped output type. (Copilot filter_map.rs:254.)
  5. N = 0 chunk case omitted for filter/filter_map next_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Challenge Used to tag a challenge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants