Verify the safety of slice functions (challenge #17) - #603
Conversation
Safety contracts + Kani proof_for_contract harnesses for split_at_unchecked, split_at_mut_unchecked, and swap_unchecked (12 harnesses, all pass) via the proof_for_contract(<[T]>::method) + kani::slice::any_slice_of_array pattern.
…ns (challenge model-checking#17) Completes the unsafe-function half of challenge model-checking#17: get_unchecked/_mut (#[requires(N != 0 && len % N == 0)] via proof_for_contract per concrete (T,N)), and get_disjoint_unchecked_mut (plain proof + assume: in-bounds + pairwise distinct). 39 harnesses, all pass. With tranche 1 and the existing align_to/ align_to_mut, all 10 unsafe slice functions in the challenge now verify. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
…_chunk/ first_chunk_mut, last_chunk/last_chunk_mut, split_first_chunk/_mut, split_last_chunk/_mut, split_at_checked/split_at_mut_checked. 24 harnesses over representative element types and chunk sizes (incl. the N=0 edge), all pass; each uses a symbolic-length slice so both the None and cast/split branches are covered. No loops, so no unwind bounds. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
… (challenge model-checking#17) No-UB harnesses for the O(1)/log/N-bounded safe abstractions: as_chunks/_mut/ as_rchunks, as_flattened/_mut, as_simd/_mut (replay align_to), binary_search_by (logarithmic loop, unwind 7), get_disjoint_mut + get_disjoint_check_valid (const-N loops). 19 harnesses, all pass. Brings challenge model-checking#17 to 30/37 (all 10 unsafe + 20 of 26 safe). Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
…el-checking#17) -- 37/37 copy_from_slice, copy_within, swap_with_slice, partition_dedup_by (symbolic length, small backing + #[kani::unwind]); rotate_left, rotate_right (concrete (length, amount) configs with symbolic values -- a symbolic rotation amount makes ptr_rotate's symbolic-size memcpy intractable >6GB here, so rotate is proven per-config). 16 harnesses, all pass. Completes all 37 functions in challenge model-checking#17 (10 unsafe + 27 safe abstractions). Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
upstream_test's ./x fmt --check rejected the check_get_unchecked_mut! invocations; one macro argument per line under style_edition 2024. Formatting only. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
|
Status update for reviewers: this PR is green on every check and mergeable It verifies all 37 functions in challenge #17: the 10 unsafe functions with The claim is on the tracking issue (#281) and the challenge is otherwise quiet. |
There was a problem hiding this comment.
Pull request overview
Adds Kani verification for Challenge 17 slice operations.
Changes:
- Adds safety contracts to five unsafe slice methods.
- Adds concrete Kani harnesses for unsafe and safe slice APIs.
- Covers pointer operations, chunking, searching, copying, rotation, and deduplication.
Suppressed comments (2)
library/core/src/slice/mod.rs:5620
get_unchecked_mutis also left with only assumed plain proofs rather than the required safety contract andproof_for_contractverification. Besides failing the unsafe-function criterion, the two selected index shapes do not establish the property for all supportedSliceIndeximplementations. Please provide the generic in-bounds contract and verify the function through it.
#[kani::proof]
library/core/src/slice/mod.rs:5708
- This leaves
get_disjoint_unchecked_mutwithout the contract required by Challenge 17 and proves onlyusizecases. Here the existingGetDisjointMutIndex::{is_in_bounds,is_overlapping}methods (orget_disjoint_check_valid) already expose the exact borrowed predicates needed for a contract, includingRangeandRangeInclusive; attach that precondition to the unsafe function and useproof_for_contractinstead of assuming selectedN/index configurations.
#[kani::proof]
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // so they need no `#[kani::unwind]`; the symbolic-length sub-slice over a fixed | ||
| // backing array is the accepted "unbounded" encoding. |
| check_split_at_unchecked!(check_split_at_unchecked_unit, ()); | ||
| check_split_at_unchecked!(check_split_at_unchecked_u8, u8); | ||
| check_split_at_unchecked!(check_split_at_unchecked_u64, u64); | ||
| check_split_at_unchecked!(check_split_at_unchecked_char, char); |
|
|
||
| macro_rules! check_get_unchecked { | ||
| ($usize_h:ident, $range_h:ident, $ty:ty) => { | ||
| #[kani::proof] |
feliperodri
left a comment
There was a problem hiding this comment.
Challenge 17 review — PR #603
Soundness (no fatal issues found)
This PR is verification-sound. I checked every item on the soundness checklist:
- cfg-swap vacuity: 0. No
cfg-swapped bodies or trick attributes. - Assume-precondition vs assume-conclusion: The
kani::assume(...)calls in the plain proofs assume the documented caller preconditions (e.g.check_get_unchecked_usize_*:assume(idx < slice.len())beforeget_unchecked(idx);check_get_disjoint_unchecked_mut_*:idx < len+ pairwise!=). These are legitimate precondition assumptions, not assuming the conclusion. Sound. - Trivial invariants: none; no
loop_invariant(true). - Contract liveness / faithfulness: The 5 new
#[requires]are faithful to the documented safety preconditions and each is exercised by a real#[kani::proof_for_contract]:slice/mod.rs:948swap_unchecked→#[requires(a < self.len() && b < self.len())]+kani::modifies(self)(proof_for_contract, u8/u16/u64/char).slice/mod.rs:1347/:1508as_chunks_unchecked{,_mut}→#[requires(N != 0 && self.len() % N == 0)](proof_for_contract).slice/mod.rs:2047/:2102split_at{,_mut}_unchecked→#[requires(mid <= self.len())](proof_for_contract).
None are decorative. Theas_simd/as_simd_mutplain proofs correctly replayalign_to's real body (no contract stubbing at the call site), so the transmute is genuinely verified.
- Over-constrained assumes: none; the assumes match the exact documented obligations.
Credit vs the rejected #567
This is substantially better than #567 and does not repeat its main failures:
- It adds 5 real, verified contracts (#567 added zero).
- It uses
kani::slice::any_slice_of_array{,_mut}, i.e. a symbolic length in0..=ARR_SIZEexercising all lengths up to the cap — not #567's single fixedlen == 5. - Safe-abstraction coverage is comprehensive: first/last/split_first/split_last chunk (+mut), as_chunks/as_chunks_mut/as_rchunks, split_at_checked/split_at_mut_checked, binary_search_by (nondet comparator,
unwind(7)for len 32), partition_dedup_by, rotate_left/right, copy_from_slice, copy_within, swap_with_slice, as_simd/as_simd_mut, get_disjoint_mut, get_disjoint_check_valid, as_flattened/as_flattened_mut. All 27 required safe fns are hit (reversepre-exists). - align_to/align_to_mut are already covered on
main(contracts atslice/mod.rs:4070/:4168, proof_for_contract harnesses at:5426–:5504), so unlike #567 this challenge area is not left with them missing.
Blocking issues
1. Three required unsafe functions receive NO contract (hard-criterion miss).
Challenge 17 states verbatim: for each listed unsafe fn, "Write contracts specifying the safety precondition(s)... then Verify that if the caller respects those preconditions, the function does not cause UB." These three get only assume-guarded plain proofs, not contracts:
get_unchecked(slice/mod.rs:~5592)get_unchecked_mut(slice/mod.rs:~5620)get_disjoint_unchecked_mut(slice/mod.rs:~5708)
The PR's justification (a generic SliceIndex/GetDisjointMutIndex precondition can't be written as one fn-level #[requires]) is only partly true. As Copilot notes, GetDisjointMutIndex::{is_in_bounds, is_overlapping} already expose exactly the borrowed predicates needed for a get_disjoint_unchecked_mut contract (covering Range/RangeInclusive, not just usize), and get_unchecked can carry contracts at the concrete SliceIndex impl level. So a contract is achievable and is required here. Additionally, covering only I = usize (and Range for get_unchecked) leaves the other SliceIndex impls unproven. Action: attach real safety contracts to these three and verify via proof_for_contract.
2. Bounded + monomorphized vs the verbatim mandatory criteria.
doc/src/challenges/0017-slice.md states verbatim "The verification must be unbounded---it must hold for slices of arbitrary length" and "must hold for generic type T (no monomorphization)." All harnesses use a fixed backing array (ARR_SIZE 64/100) so length is capped, and every T is a concrete monomorphization (u8/u64/char/unit/u32). The rotate_left/right harnesses are weaker still — six concrete (len, amount) configs, not even symbolic length (honestly disclosed in-comment). This is the criterion #567 was rejected on.
Fairness note for the maintainer: this bounded+concrete pattern is identical to the already-merged align_to work on main (any_slice_of_array + concrete src/dst types), so criterion #2 reflects a repo-wide tension with Kani's limits rather than a regression unique to this PR. I would not block on #2 alone. Issue #1 (missing contracts on 3 of the 10 required unsafe fns) is the concrete, actionable blocker and is why this is REQUEST_CHANGES.
Non-blocking
- No
as_rchunks_mutharness, but it is not on the required list. swap_uncheckedomits the ZST instantiation due to a documented CBMCcar_set_insertlimitation withmodifiesover zero-size regions — reasonable and disclosed; swapping ZSTs moves zero bytes.
Recommend the author add faithful contracts + proof_for_contract for get_unchecked, get_unchecked_mut, and get_disjoint_unchecked_mut, and confirm with maintainers whether the established any_slice_of_array/concrete-type encoding satisfies the unbounded+generic criteria (given it matches merged align_to), before this can be approved.
…unchecked_mut (challenge 17 review) Address the challenge 17 review: the three remaining unsafe functions now carry real fn-level safety contracts verified by proof_for_contract, instead of assume-guarded plain proofs. Kani cannot attach contracts to trait functions (model-checking/kani#1997), so the SliceIndex impls cannot carry them directly. Instead: - New kani-only predicate SliceIndex::kani_in_bounds(&self, len): the documented in-bounds precondition of each impl, overridden by all 13 SliceIndex<[T]> impls. The default is true, so a missing override makes proof_for_contract fail loudly instead of pass vacuously. - <[T]>::get_unchecked and <[T]>::get_unchecked_mut gain #[requires(index.kani_in_bounds(self.len()))]. - <[T]>::get_disjoint_unchecked_mut gains #[requires(get_disjoint_check_valid(&indices, self.len()).is_ok())], the same GetDisjointMutIndex predicate the safe get_disjoint_mut gates on. - The assume-guarded plain proofs are replaced by proof_for_contract harnesses that drive the contracted wrappers through the real body of every SliceIndex<[T]> impl: usize, IndexRange, Range, RangeTo, RangeFrom, RangeFull, RangeInclusive, RangeToInclusive, their core::range counterparts, and (Bound, Bound). RangeInclusive inputs include iteration-exhausted values, so the exhausted arm of its predicate is exercised. get_disjoint_unchecked_mut is verified for usize (N = 2, 3) and all four GetDisjointMutIndex range impls (ops and core::range flavors of Range and RangeInclusive). Local verification with the pinned Kani (415ca503): 46/46 harnesses successful. Contract liveness confirmed by mutation: weakening the usize predicate (< to <=), dropping the end <= len conjunct of the Range predicate, and inverting the disjoint contract each make the matching harness fail. rustfmt is clean under the upstream rust-lang/rust config. Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
|
Thank you for the thorough review. I have pushed adjustments that address issue Kani cannot attach contracts to trait functions at all. Adding
So impl-level contracts (the suggested mechanism for
Verification status: all 46 affected harnesses pass locally with the pinned On issue number 2 (bounded + monomorphized vs the verbatim unbounded/generic-T |
Challenge 17: Verify the safety of
slicefunctionsResolves the verification targets in
doc/src/challenges/0017-slice.md(tracking #281).This adds Kani harnesses for all 37 functions in the challenge: the 10 unsafe
functions (with safety contracts) and the 27 safe abstractions (proven UB-free).
Unsafe functions (contracts +
proof_for_contract)get_unchecked/get_unchecked_mutswap_uncheckeda < len && b < len#[requires]+kani::modifies(self)split_at_unchecked/split_at_mut_uncheckedmid <= len#[requires]as_chunks_unchecked/as_chunks_unchecked_mutN != 0 && len % N == 0#[requires], per concrete<[T]>::…::<N>align_to/align_to_mutmain)get_disjoint_unchecked_mutSafe abstractions (proven free of UB)
first_chunk(_mut),last_chunk(_mut),split_first_chunk(_mut),split_last_chunk(_mut),split_at_checked,split_at_mut_checked,as_chunks,as_chunks_mut,as_rchunks,as_flattened,as_flattened_mut,as_simd,as_simd_mut,binary_search_by,get_disjoint_mut,get_disjoint_check_valid,copy_from_slice,copy_within,swap_with_slice,partition_dedup_by,rotate_left,rotate_right(andreverse,pre-existing on
main).Approach
#[cfg(kani)] mod verifyat the end oflibrary/core/src/slice/mod.rs, following the establishedalign_topattern.#[kani::proof_for_contract(<[T]>::method)](incl. const-generic turbofish<[T]>::as_chunks_unchecked::<N>), spread over representative element types(
(),u8,u64,char, …) and chunk sizes — source-generic, proof-concrete.kani::slice::any_slice_of_array(_mut)helper (the same "unbounded" encoding
align_touses).binary_search_by,rotate_*,copy_*,swap_with_slice,partition_dedup_by) carry an explicit#[kani::unwind(K)]— a verifiedunwinding assertion, not an assumption.
Honest caveats (please review)
align_toharnesses, the symbolicslice length ranges over
[0, ARR_SIZE]for a fixedARR_SIZE. The per-index/-elementsafety obligation is structurally identical at every position, so a modest bound
exercises it, but coverage is bounded rather than literally unbounded.
get_unchecked,get_unchecked_mut,get_disjoint_*). Thesafety precondition is index-type-specific (
idx < lenforusize; range bounds forRange) and cannot be expressed as one#[requires]over the generic indexI(a contract closure only borrows its args, and
SliceIndexexposes no genericin-bounds predicate). These are proven with plain
#[kani::proof]at concrete indextypes, with the documented caller obligation established by
kani::assume. Same propertythe contract would express; happy to adjust if you'd prefer a different encoding.
rotate_left/rotate_rightare per-config, not symbolic-length. A symbolicrotation amount makes
ptr_rotateperform symbolic-sizememcpys at a symbolic splitpoint and explore its block-swap/juggling paths; CBMC exceeded a 6 GB budget (and kept
climbing) even at length 4 on the dev machine. They are instead verified over
representative concrete
(length, amount)configurations with symbolic element values(a single concrete
ptr_rotatepath). A higher-memory environment could attempt thefully symbolic version.
swap_uncheckedomits the ZST instantiation.kani::modifies(self)over azero-size slice region trips a CBMC contracts-library limitation (
car_set_insert);swapping ZSTs moves zero bytes, so it is trivially safe. The non-ZST instantiations
exercise the actual
ptr::swapwrites.Tooling / disclosure
Verified with the repo's pinned Kani (commit
415ca50,nightly-2025-10-09) viaverify-std. Authored with AI assistance (Claude); all proofs were run and confirmedto pass locally (sequential CBMC).