Skip to content

Challenge 24: verify Vec IntoIter and spec_* function safety with Kani - #689

Open
kasimte wants to merge 3 commits into
model-checking:mainfrom
kasimte:24-build
Open

kasimte wants to merge 3 commits into
model-checking:mainfrom
kasimte:24-build

Conversation

@kasimte

@kasimte kasimte commented Sep 18, 2026 •

Copy link
Copy Markdown

Towards #285. Kani harnesses for Challenge 24: Verify the safety of Vec functions part 2.

47 harnesses cover all 22 listed functions — Vec::IntoIter (14) and the spec_* helpers it routes through (spec_extend, spec_from_iter/_nested, spec_from_elem, from_elem, extract_if). Each runs the real shipped body (no #[cfg(kani)] rewrite, no assume(false) on a real branch), at symbolic length where tractable, and asserts the observable effect, not just absence of UB. All pass via scripts/run-kani.sh at Kani 6ce55b9 / CBMC 6.11, with 29 kani::cover witnesses. Additive, +976/−0 across 7 files.

Spec requirement Status
22 functions — no UB on the real body all 22; functional postconditions asserted (e.g. advance_by(k) → Ok iff k <= len; fold visits exactly len)
__iterator_get_unchecked safety contract real proof_for_contract at u8/Global and [u8; 3]/Global (Upstream below)
Drop / early-exit paths hit, not assumed — try_fold short-circuits in a Drop variant (a double-drop would be caught); extract_if::next writes through the real pointer
Unbounded length not met — length-bounded (64, smaller where noted); the loop-contract route is tool-walled (CBMC does not terminate on the same_allocation invariant the havoced heap-pointer loops need)
Generic T representative, not parametric; open committee question (#734) — see below

Generic T (#734). On the size/align-branch axis the ruling asks about: IntoIter's 14 functions consume an already-built Vec and have no size_of/align_of-gated branch (nothing to reach). The one such branch in the spec_* callees (min_non_zero_cap) is reached at the size == 1 arm (Vec<u8>); its larger arms are reached only by growing through extend_desugared — a Challenge 23 function, not one of this challenge's 22, and object-bits-walled on its realloc path besides — disclosed in-code. Because element access is typed pointer arithmetic, representative types span the layout properties it depends on: u8, () (ZST), #[repr(align(16))], [u8; 3] (odd stride), and a Drop token.

Upstream Kani. __iterator_get_unchecked lives in a generic trait impl; naming the allocator explicitly resolves it as a proof_for_contract target via our merged fix kani#4865 (instantiate path generic arguments during type resolution). The harnesses verify its shipped #[requires(i < self.len())] + kani::modifies(self) contract directly.

Beyond fresh iterators, 13 harnesses re-check the surface from arbitrary reachable states — a symbolic number of elements consumed from each end via advance_by/advance_back_by, results checked against a pre-conversion snapshot.

Residual bounds, each noted in-code: extract_if::next and the default from_iter exceed --object-bits 12 at larger sizes; the Drop-token harnesses fix size 4 (per-element-identical obligations); element-by-element growth routes through Challenge 23's extend_desugared/extend_with, not claimed here.

Verify with scripts/run-kani.sh (standard -Z flags, --object-bits 12), scoped to the vec::{into_iter,extract_if,spec_*}::verify harnesses; 47 verified, 0 failures.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

@kasimte
kasimte requested a review from a team as a code owner September 18, 2026 17:45
@feliperodri feliperodri added the Challenge Used to tag a challenge label Sep 20, 2026
kasimte added a commit to kasimte/kani that referenced this pull request Sep 30, 2026
…king#4865)

Contracts on a trait method whose type carries a concrete generic
argument — `<CStr as Index<RangeFrom<usize>>>::index`, or a concrete
instantiation of a generic self type — failed to resolve with
`MissingTraitImpl`. `resolve_ty` returned the definition's identity type
(`RangeFrom<usize>` came back as `RangeFrom<Idx>`), so the arguments
written in the path were parsed but never applied.

This PR applies them, in `resolve_ty`'s `Type::Path` arm, via the new
`instantiate_path_args`.

## Verification

The tests live in the standard `tests/kani` and `tests/expected` suites
(CI runs them); each has a definite outcome:

| test (`-Zfunction-contracts`) | expect |
|---|---|
| `tests/kani/FunctionContracts/generic_argument_instantiation.rs` | **6
targets verify.** A primitive argument resolved before (regression
guard); the five generic shapes resolve *only* with this change — a
generic argument, a generic self type (associated-type return,
`where`-bounded method), a concrete DST self, a generic `impl` at a
concrete instantiation, and a type with a lifetime parameter beside a
type parameter (lifetime erased). |
| `tests/expected/function-contract/generic_arg_unimplemented.rs` |
**still fails `MissingTraitImpl`** *(expected-output test — this
diagnostic is the pass)* — `<S as Generic<Wrap<u16>>>::generic` is
genuinely not implemented, so valid targets resolve without
over-resolving invalid ones. |
| `tests/expected/function-contract/generic_method_unimplemented.rs` |
**still fails `MissingTraitImpl`** *(expected-output test — this
diagnostic is the pass)* — a trait method with its own generic parameter
(`fn compute<T>`) is out of scope (see below). |

Confirmed on nightly-2026-09-22 (Kani 0.68.0, CBMC 6.11.0): all six
verify; with the fix reverted, the generic targets fail to resolve while
the primitive resolves. The `cross_module_multiple_impls` (7/7),
`multiple_inherent_impls` (3/3), and resolver unit (41/41) suites are
unchanged.

Mechanism: this is the "trait functions with generic parameters"
limitation described in
[model-checking#1997](model-checking#1997 (comment)).
The trait's arguments already reach trait-impl resolution, but each came
back uninstantiated from `resolve_ty`, and full `Instance::resolve`
cannot match a concrete impl from a free parameter. Making the arguments
concrete fixes that; any shape that cannot be resolved (const-generic
arguments, omitted defaulted parameters) keeps the old uninstantiated
type, so nothing that resolved before changes. No partial-resolution
machinery is needed — with concrete arguments, the existing full
resolution matches.

These are not hypothetical. An iterator adapter's
`__iterator_get_unchecked` (a generic self type) is another std-library
method of this shape: the Challenge 16 and Challenge 24 submissions
([model-checking/verify-rust-std#549](model-checking/verify-rust-std#549),
[model-checking/verify-rust-std#689](model-checking/verify-rust-std#689))
disclose it and fall back to `kani::assume` mirror harnesses. What this
change reaches there: closure-free adapters resolve (`Cloned`, `Zip`);
`Map`/`Filter` do not, since `fn`/closure types aren't supported by
`resolve_ty`. Challenge 24's `<vec::IntoIter<u8> as
Iterator>::__iterator_get_unchecked` resolves only with the allocator
written out (`IntoIter<u8, std::alloc::Global>`, which needs
`allocator_api`): omitted trailing parameters with declared defaults are
not filled and keep the uninstantiated type.

Related to model-checking#1997 — this handles a type carrying concrete generic
arguments: a generic trait argument, or a generic self type instantiated
to concrete types, against a concrete or generic `impl`. A trait method
with its own generic parameter still fails to resolve — that parameter
is not part of the path, so nothing here binds it.

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 and MIT licenses.
wodex1nhaoIeng pushed a commit to wodex1nhaoIeng/kani that referenced this pull request Oct 2, 2026
…el-checking#4915)

A `proof_for_contract` target that omits a trailing generic parameter
with a declared default failed to resolve with `MissingTraitImpl`. For
example `<Vec<u8> as Trait>::m`, or the natural spelling of
`<vec::IntoIter<u8> as Iterator>::__iterator_get_unchecked`, a target in
the Challenge 24 submission (model-checking/verify-rust-std#689); both
omit the allocator parameter. The parameter-count check kept the
uninstantiated type instead of filling the default.

This PR fills an omitted trailing parameter from its declared default,
instantiated with the arguments so far, matching what rustc does for
omitted arguments. The logic lives in `default_type_arg`, called from
`instantiate_path_args`'s substitution loop.

A path written with no generic arguments at all (a bare `Wrapper` for
`struct Wrapper<T = u8>`) is treated as an empty argument list, so an
all-defaulted type resolves too. Parenthesized arguments keep the
uninstantiated type.

## Verification

| test (`-Zfunction-contracts`) | expect |
|---|---|
| `tests/kani/FunctionContracts/generic_default_argument_fill.rs` | **5
targets verify.** A std container default (`Vec<u8>` fills `A =
Global`); a sibling impl with a distinct postcondition (`Vec<u16>`), so
each harness only verifies if resolution picks its own impl; a default
referencing an earlier parameter (`Pair<T, U = T>` at `Pair<u8>` fills
`U = u8`); the explicit spelling (`Vec<u16, std::alloc::Global>`)
unchanged; and an all-defaulted type written with no argument list
(`AllDefault` for `struct AllDefault<T = u8>`) fills `T = u8`. |
| `tests/expected/function-contract/generic_default_missing_required.rs`
| **still fails `MissingTraitImpl`** *(expected-output test; this
diagnostic is the pass)*. `NoDefault<u8>` omits a parameter with no
default, so only declared defaults are filled. |
| `tests/expected/function-contract/generic_default_missing_bare.rs` |
**still fails `MissingTraitImpl`** *(expected-output test; this
diagnostic is the pass)*. `NoDefaultBare` written with no argument list
omits a parameter with no default, so the no-arguments path still keeps
the uninstantiated type. |
| `tests/expected/function-contract/generic_default_const_param.rs` |
**still fails `MissingTraitImpl`** *(expected-output test; this
diagnostic is the pass)*. `WithConst<u8>` omits a defaulted **const**
parameter; only defaulted type parameters are filled, so the const
default is not filled and the target keeps the uninstantiated type. |

The four original targets were confirmed locally when this PR was first
submitted (`generic_default_argument_fill` 4/4, the two negatives
failing `MissingTraitImpl`, resolver suites unchanged at
`generic_argument_instantiation` 6, `cross_module_multiple_impls` 7,
`multiple_inherent_impls` 3). This revision adds the no-arguments case
(`check_all_parameters_defaulted`) and the
`generic_default_missing_bare` negative; CI runs the full suite on the
pinned toolchain.

Scope: type parameters only. A const parameter (with or without a
default), and any argument that cannot be resolved, keep the
uninstantiated type, as before. Defaults are instantiated structurally
(no normalization), matching how `resolve_ty` already hands
`tcx.type_of` results forward.

Resolves model-checking#4914.

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 and MIT licenses.

@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.

Challenge 24 requires: "The verification must be unbounded---it must hold for slices of arbitrary length." That requirement is final. A Kani solution will only be accepted if it is unbounded in the input size for every required function. This PR still verifies at least part of the challenge with bounded harnesses (kani::unwind bounds and/or fixed-size inputs), so I'm requesting changes until every required function is verified without a bound on the input size.

This branch has not been deployed

No deployments
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.

2 participants