Repository navigation
Verify the safety of core::slice functions with VeriFast (Challenge 17) - #742
Open
paulocsanz wants to merge 15 commits into
Open
paulocsanz wants to merge 15 commits into
paulocsanz wants to merge 15 commits into
Conversation
…safe fns and safety proofs for the 27 safe abstractions
- verifast-proofs/core/slice/mod.rs: proof of core/src/slice/{mod,raw,rotate}.rs
- generic in T at the tool level, unbounded in slice length
- check-verifast-proofs.sh / patch-verifast-proofs.sh: add core/slice entry
…ast's own toolchain rust-src VeriFast 26.10 targets rustc nightly-2026-09-25, which cannot compile the repository's library/core subtree (nightly-2026-10-12: new abort_immediate / type_id_* intrinsics, optimize attribute gate dropped). Build libcore_noslice.rlib from that toolchain's rust-src component instead, so the gate passes on a clean checkout (CI) and no longer depends on subtree bumps. The slice sources under verification remain the repository's, byte-for-byte.
paulocsanz
marked this pull request as draft
October 27, 2026 14:29
- Copy: drop copy_trait_bound (asserted is_Copy for every T); is_Copy is now established only by copy_witness__VeriFast_wrapper<T: Copy>, so rustc rejects any use for a non-Copy type. - as_simd/as_simd_mut: no inline assume in safe code; the wrappers are unsafe with is_array_of / valid_slice_transmute as explicit preconditions. - align_to/align_to_mut: share_transmute_prefix / full_borrow_transmute_prefix require the run to be rest's prefix, byte-exact coverage and alignment; the split (array_share_split / new proved slice_full_borrow_split), the coverage equation (gcd divisibility proved, mul_assoc arithmetic axiom) and the alignment (ptr::align_offset spec) are proved at the call sites. - get_disjoint_*: replace the opaque disjoint_mut_loop_* lemmas (one of which duplicated a full borrow) and the opaque result predicate by a region model of the sealed GetDisjointMutIndex impls (6 closed axioms); carving, conversion to one lifetime-k borrow, the strong-close restore proof, the output-array bookkeeping and disjoint_mut_borrows_peel are proved. - rotate: rawarray lemmas concrete in the buffer type, bound in bytes. - README: caveats rewritten to list every remaining axiom by name.
paulocsanz
marked this pull request as ready for review
October 27, 2026 21:37
Like linked_list.rs-negative: a copy of the proof with a deliberately wrong body (swap_unchecked touching one element past the contract's bound), on which both verifast and the refinement checker must fail. Wired into check-verifast-proofs-negative.sh (the verifast-negative.yml job) and patch-verifast-proofs.sh.
…ions
The VeriFast proof verifies the 20 const-generic functions through runtime-n
transcriptions (VeriFast 26.10 cannot attach specifications to <const N>
functions). This adds, on the unchanged shipped bodies:
- #[requires]/#[ensures] contracts on as_chunks_unchecked,
as_chunks_unchecked_mut (N != 0 and len % N == 0) and
get_disjoint_unchecked_mut (get_disjoint_check_valid(&indices, len).is_ok(),
i.e. exactly what get_disjoint_mut checks), with proof_for_contract
harnesses over u8/u32/() and N in {1, 2, 3, 4}, usize and Range indices;
- bounded harnesses (slice length <= 12, any_slice_of_array) for the 17 safe
const-generic functions, asserting the returned shapes (lengths, pointers,
Ok/Err <=> get_disjoint_check_valid).
The contract macros are no-ops outside cfg(kani_host) and mod verify is
cfg(kani), so the VeriFast proof is unaffected; its original/ and verified/
copies are re-synced via patch-verifast-proofs.sh (same 1818 statements).
This was referenced Oct 28, 2026
…er to its VeriFast issue Each assume(false) lemma that stands in for a missing prelude rule or prover fact, and each assume_correct wrapper that exists because of the -extern_spec header restriction or a missing std specification, now carries the URL of the VeriFast issue that reports the gap with a minimal reproducer (verifast/verifast#1049-rust-lang#1056); the chunk reinterpretation lemmas point to the [T; N] ownership proposed in verifast/verifast#1048. The README caveats carry the same links. No proof change: 0 errors found (1818 statements verified), refinement checker clean, negative test still fails as intended.
…opies Two lines in the new `mod verify` harnesses exceeded rustfmt's width (upstream_test format check). Formatting only; the original/ and verified/ copies of the proof (and of the negative test) are re-synced with update.sh, and the gate is unchanged: 0 errors found (1818 statements verified), refinement checker clean, negative test still rejected.
…pping, sysroot edits as a patch file, precise flag caveats - core.rsspec: `ptr::swap` (with the `x == y` case made explicit by `opt_pointsto`) and `ptr::swap_nonoverlapping` are now extern specifications, like `ptr::metadata`; the two `assume_correct` forwarding functions in `verified/lib.rs` are gone (proposed upstream for `std/lib.rsspec`). `ptr::replace` cannot move: `dst.replace(src)` is the `<*mut T>::replace` method, which `.rsspec` files cannot specify. - verify.sh: the four sysroot `core` edits are `core-sysroot.patch` (a `diff -ru`, applied with `patch -p1`) instead of an inline python block, in both the proof and the negative test. - README: `-allow_dead_code` suppresses exactly the four compiled-out `debug_assert!` bodies of `revswap`; the 48 `ignore_ref_creation` sites are all slice/array references created from raw pointers, which the 26.10 aliasing model (`Sized` pointee only) cannot express. Gate from a clean sysroot: 0 errors found (1816 statements verified), refinement checker clean, negative test still rejected.
This was referenced Oct 28, 2026
… -> 26 axioms) The separation-logic bookkeeping of get_disjoint_mut is now proved instead of assumed: `rest_elems` represents the element array minus the regions of an index list element by element (`in_some_region`), and the two lemmas are proved by induction over the index list, carving or restoring one region at a time (`carve_one`, `uncarve_one`), using only the two interval axioms on the sealed index types. `slice_rest_owned` is now a defined predicate that also records the in-bounds and pairwise-disjointness facts, so the specifications of both lemmas are unchanged. Gate: 0 errors found (1909 statements verified), refinement checker clean, negative test still rejected.
…(honest const-generic wording, verified usize index impls, sound element-access specs, abstract is_array_of, Range bounds instance) - README / coverage: state that VeriFast does not verify the 20 const-generic bodies (specless, skipped; body/transcription link is by inspection; Kani verifies the bodies bounded), and that only the usize GetDisjointMutIndex impls are verified. - GetDisjointMutIndex for usize: is_in_bounds / is_overlapping now carry a specification (with a safety_proof opening the usize frac borrow) and are verified against the region model; the model is defined at usize and Range<usize> (usize_region_model, Range_region_model, empty regions normalized to [0, 0)) and the two generic interval axioms are proved at those types. The Range<usize> impls cannot take a spec (field reads through &Range need std::ops::Range_share, absent in 26.10); RangeInclusive and core::range::* cannot be named in ghost code. - slice_index_shared__VeriFast_wrapper: pointer arithmetic only (the old spec fabricated an element permission on top of the array fraction); callers use the proved array_extract_elem_frac / array_insert_elem_frac. - write_disjoint_mut_elem__VeriFast_wrapper: requires GetDisjointMutIndex_in_bounds(idx, len); callers use all_in_bounds_nth. - is_array_of is abstract (Chunk is [T; n] or has its layout and validity invariant), with the layout facts as the axiom is_array_of_layout. - Range_SliceIndex_in_bounds: the get_unchecked bounds precondition at Range<usize>. assume(false): 26 -> 30 (four explicit statements replacing a false, unsound or unusable one). Gate: 0 errors found (1964 statements verified), refinement checker clean, negative test still rejected; four new mutation controls (<= in is_in_bounds, is_overlapping always false, missing in-bounds fact, chunk lemma without is_array_of) all rejected.
…ectives, prove the full align_to_offsets postcondition in the wrapper, correct the README - Only 12 of the 48 //~ignore_ref_creation directives suppress anything (each of the other 36 was removed individually and the gate still passes); keep those 12 and describe them precisely in the README (pointees: [T; N], I::Output, Ordering, [_; 0], I). - align_to_offsets_proof__VeriFast_wrapper now takes the hypotheses of the method's trusted (assume_correct) specification and proves all of its conclusions, including us_len * size_of::<U>() <= isize::MAX and the end of the middle run being within limits. - README: as_rchunks_mut is not verified (removed from the list); the Option<&[T; N]> functions do not depend on verifast/verifast#1054 (close_ref_own exists in the prelude); state what is trusted and what is proved for align_to_offsets. - Negative copy re-synced.
…appers; create the &I references of get_disjoint_check_valid under the aliasing protocol - revswap_proof__VeriFast_wrapper omits revswap's two compiled-out debug_assert!s (the only dead-code reports) and swaps through element pointers, so slice_index_mut__VeriFast_wrapper and the -allow_dead_code flag go. - get_disjoint_check_valid_proof__VeriFast_wrapper obtains &I from indices.as_ptr().add(i) with precreate_ref / init_ref_readonly / end_ref_readonly instead of a trusted indexing wrapper; slice_index_shared__VeriFast_wrapper goes. 10 ignore_ref_creation directives remain, the README says why each of them cannot be replaced in VeriFast 26.10 (type_interp for a type-parameter pointee; temporaries in directly verified bodies). - clone_disjoint_index: document why clone is the identity for the sealed trait. - Negative copy re-synced.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Towards #281. Solves Challenge 17 with a VeriFast 26.10 proof of
library/core/src/slice/{mod,raw,rotate}.rs, following the structure of the accepted VeriFast solutions for challenges 5 (linked_list) and 19 (RawVec):verifast-proofs/core/slice/mod.rs/{original,verified}with a three-step gate (verifast→refinement-checker→ byte-diff againstlibrary/), wired intocheck-verifast-proofs.sh(runs in the existingverifast.ymlCI job). A negative counterpart,verifast-proofs/core/slice/mod.rs-negative(the same proof with a deliberately wrongswap_unchecked), is wired intocheck-verifast-proofs-negative.sh(theverifast-negative.ymljob) and must be rejected by bothverifastand the refinement checker.Success criteria
get_unchecked,get_unchecked_mut,swap_unchecked,split_at_unchecked,split_at_mut_unchecked,align_to,align_to_mut); 3 const-generic ones (as_chunks_unchecked,as_chunks_unchecked_mut,get_disjoint_unchecked_mut) on a runtime-ntranscription of the body, see caveat 1T(no monomorphization)T(and inn) at the tool levelThe two criteria that Kani submissions to this challenge have marked "not literally met" (unbounded; generic T without monomorphization) are satisfied here by construction, so this submission does not depend on the pending committee ruling on the generic-T clause.
Kani harnesses for the const-generic functions
For the 20 const-generic functions, which VeriFast 26.10 can only verify through transcriptions (caveat 1), this PR also adds Kani verification of the exact shipped bodies, bounded and monomorphized, in
library/core/src/slice/mod.rs:#[requires]/#[ensures]contracts on the three unsafe ones (as_chunks_unchecked,as_chunks_unchecked_mut:N != 0 && self.len().is_multiple_of(N);get_disjoint_unchecked_mut:get_disjoint_check_valid(&indices, self.len()).is_ok(), the checkget_disjoint_mutperforms) with#[kani::proof_for_contract]harnesses overu8/u32/()andNin 1..=4 (usizeandRange<usize>indices forget_disjoint_unchecked_mut), and bounded harnesses (slice length ≤ 12,any_slice_of_array) for the 17 safe ones, asserting the shapes of the results. The two tools are complementary here: VeriFast gives the unbounded, generic-Targument; Kani runs the real bodies. The contract macros are no-ops outsidecfg(kani_host)and the harness module iscfg(kani), so the VeriFast gate is unchanged (patch-verifast-proofs.shre-syncs the proof copies). With the repository's pinned Kani (tool_config/kani-version.toml),kani verify-stdon the 22 harnesses reportsComplete - 22 successfully verified harnesses, 0 failures(the CI partitions of this PR ran all 22).Coverage (all 37 functions)
get_unchecked,get_unchecked_mut,swap_unchecked,split_at_unchecked,split_at_mut_unchecked,align_to,align_to_mutnwrappers — see caveats):as_chunks_unchecked,as_chunks_unchecked_mut,get_disjoint_unchecked_mutreverse,split_at_checked,split_at_mut_checked,binary_search_by,partition_dedup_by,rotate_left,rotate_right,copy_from_slice,copy_within,swap_with_sliceGetDisjointMutIndex::{is_in_bounds, is_overlapping}: verified against the region model forusizeonly (2 of the 10 impl methods). TheRange<usize>impls read fields through&Range<usize>, which VeriFast 26.10 routes through ashareinterpretation ofRangethat itsstdspecs do not define;RangeInclusive<usize>andcore::range::*cannot be named in ghost code. Those 8 methods have no specification and are skipped, so the model is not checked against themfirst_chunk,first_chunk_mut,split_first_chunk,split_first_chunk_mut,split_last_chunk,split_last_chunk_mut,last_chunk,last_chunk_mut,as_chunks,as_chunks_mut,as_rchunks,as_simd,as_simd_mut,as_flattened,as_flattened_mut,get_disjoint_mut,get_disjoint_check_valid. Theas_simd/as_simd_mutwrappers additionally takevalid_slice_transmute::<T, Simd<T, N>>()(the# Safetycondition ofalign_to, justified bySimd's#[repr(simd)]layout) as an explicit, unchecked preconditionThe tour of the proof, the ownership discipline (RustBelt-style
share/full_borrowencoding), and every caveat are documented inverifast-proofs/core/slice/mod.rs/README.md.Caveats (summary; full list in the proof README)
<const N: usize>functions (error: Todo: ConstKind::Param, vf_mir_translator.ml:1787), and passingNfrom the const fn into a wrapper aborts the refinement checker (Clause kind not supported, refinement_checker.ml:2338,ClauseKind::ConstArgHasType). Each const-generic function therefore has a*_proof__VeriFast_wrappertranscription that is generic inChunkand takesn: usizeat runtime, with a concrete layout predicatefix is_array_of<T, Chunk>(n: usize) -> bool { 0 <= n && size_of::<Chunk>() == n * size_of::<T>() && align_of::<Chunk>() == align_of::<T>() }as the layout bridge (used to prove e.g.flatten_non_zst_no_overflowmechanically). One proof covers everyN, unbounded. To be explicit: VeriFast does not verify these 20 bodies. They carry no specification and-skip_specless_fnsskips them; the refinement checker only confirms that they are identical to the original, which says nothing about the transcriptions. The correspondence between each body and its wrapper is by inspection only. The Kani harnesses below verify the bodies themselves, bounded and monomorphized. A VeriFast change that lifts this limitation is proposed in [Rust] Support const generic parameters in specified functions verifast/verifast#1048. This submission does not depend on it, but we are prepared to replace the 20 wrappers by direct specifications on the unchanged bodies as soon as a VeriFast release supports that.assume(false)lemmas), all with closed specifications. Contiguous borrow splitting (split_at_uncheckedby induction onarray_share,split_at_mut_uncheckedand the reusableslice_full_borrow_splitviaopen_full_borrow_strong_+full_borrow_split),slice_full_borrow_inv,flatten_non_zst_no_overflow,gcd(including divisibility of its result),align_to_offsets(including byte-exact coverage of the middle slice; proved on its*_proof__VeriFast_wrappertranscription, since the method'sconst { gcd(..) }block and nestedfn gcdare not translated, and the method itself carries the proved postcondition as a trustedassume_correctspec, see the README) and the wholeget_disjoint_*bookkeeping (region carving and borrow conversion) are proved mechanically. What remains axiomatized is listed by name in the README: tuple/Rangeownclosing,[T]↔[T; N]-chunk and stack-buffer reinterpretation under an abstract "Chunkis[T; n]" predicate (with its layout facts as an axiom), RustBelt'sCopyrules (gated by aT: Copywitness function, so rustc rejects any use for a non-Copytype), the[T]→[U]reinterpretation of an aligned, byte-exactly covering prefix inalign_to(alignment comes fromptr::align_offset's spec, coverage and the split are proved), the region model of the five sealedGetDisjointMutIndeximpls (4 generic lemmas: the interval meaning ofis_in_bounds/is_overlappingand the ownership meaning ofget_unchecked_mut's result; plus the model's definition atusizeandRange<usize>, against which the two interval lemmas are proved and theusizeimpls are verified; the carving of the regions out of the array and their restoration are proved by induction), theRange<usize>instance ofget_unchecked's bounds precondition, and associativity of multiplication. No axiom produces ownership of pointers unrelated to its inputs or duplicates a borrow, and there are no inlineassumestatements. Each axiom that stands in for a missing prelude rule or prover fact is linked, at theassume(false)and in the README, to a VeriFast issue with a minimal reproducer: tuple/Rangeclosing [Rust] No way to close <(T1, T2)>.own / <(T1, T2, T3)>.own / <Range<Idx>>.own from the components verifast/verifast#1049, slice validity facts $PATH for running compiletest.exe is not correct on windows rust-lang/rust#1050,Copyrules Bare functions, typesafe spawn, function subtyping rust-lang/rust#1051,mul_assocRewrite spawn again rust-lang/rust#1052, tuple layout Make a decision about fn@/lambda and fn&/block rust-lang/rust#1053 (and the chunk reinterpretation lemmas to the[T; N]ownership of Non-boxed recursive tag constructor fails to compile with "Out of stack space" rust-lang/rust#1048).assume_correcton external/MIR-unsupported wrappers (slice::range,call_f_binary_search__VeriFast_wrapper,call_same_bucket__VeriFast_wrapper,empty_slice__VeriFast_wrapper, …; the full list is in the README). Two of them sit inside theget_disjoint_unchecked_muttranscription and are its only unsafe operations:clone_disjoint_index__VeriFast_wrapper(indices.get_unchecked(i).clone()) andwrite_disjoint_mut_elem__VeriFast_wrapper(arr_ptr.add(i).write(&mut *slice.get_unchecked_mut(idx)), whose trusted spec is the raw-pointer semantics of the sealedSliceIndex::get_unchecked_mutimpls expressed in the region model, not theSliceIndexcontract ofcore.rsspec), so what VeriFast proves for that function is the ownership bookkeeping over that model. external functions referencingOption,Range, orMaybeUninitcannot be placed incore.rsspecbecausevf_mir_translator.ml:418restricts-extern_specheader dependencies toprelude.rsspeconly ([Rust] -extern_spec files can only depend on prelude.rsspec, so they cannot mention std types (Option, Range, MaybeUninit) verifast/verifast#1056). Thestdfunctions whose signatures allow it (ptr::metadata,ptr::swap,ptr::swap_nonoverlapping) are specified incore.rsspecas extern specifications, like VeriFast's ownstd/lib.rsspec, and proposed upstream in [Rust] Specs for ptr::replace, ptr::swap, ptr::swap_nonoverlapping, ptr::slice_from_raw_parts; tuple_2/tuple_3 own lemmas verifast/verifast#1057;align_offset(a raw-pointer method) andslice_from_raw_parts{,_mut}/ptr::copy(whose specs use crate-local definitions) remain local forwarding functions inverified/lib.rs. Note that[T]::lenis verified directly againstptr::metadataincore.rsspec, andT::IS_ZSTbranches are discharged mechanically via VeriFast's built-instd::mem::IS_ZSTtranslation (vf_mir_translator.ml:2220) andstd::alloc::is_valid_layout_size_of_align_ofwithout inlineassumestatements.vf_revswap!inreverse(the verified copy omitsrevswap's two compiled-outdebug_assert!s and indexes through element pointers; details in the README):vf_mir_exporter(lib.rs:434,940) ignores inline specs on nested functions inside a statement block, andrust_parser.ml:1036rejectsimpl<T> [T]in.rsspecfiles; thereforerevswapis verified as the top-level functionrevswap_proof__VeriFast_wrapperand invoked viavf_revswap!. Nested-fn specifications are proposed in [Rust] Support specifications on fn items nested inside a function body verifast/verifast#1058.-skip_specless_fns -ignore_unwind_paths -allow_assume -allow_ignore_ref_creation. The 10ignore_ref_creationdirectives (each checked to be necessary) suppress the aliasing-rule check at 10 sites: four&[T; N]created from raw pointers in the chunk wrappers (a shared reference to a type-parameter pointee fromshareownership:init_ref_shareneedstype_interp, which 26.10 only provides inside lemma-pointer bodies), one&I::Outputinget_unchecked, three&Orderingauto-references inbinary_search_byand two&[_; 0]literals inalign_to(temporaries inside expressions of directly verified bodies, where the protocol's ghost statements cannot be placed); the&Ireferences of theget_disjoint_check_validwrapper are created under the protocol; slice references created from raw pointers need no directive in 26.10 because the translator does not apply the protocol to them ([Rust] Aliasing model: reference-creation lemmas require a Sized pointee, so a &[T] created from a raw pointer cannot be verified verifast/verifast#1059); results are per target architecture (LP64); VeriFast is not foundational and has known unsoundnesses (pointer-aliasing rules, variance) as documented at https://github.com/verifast/verifast/issues?q=is%3Aissue+is%3Aopen+label%3Aunsoundnesscore. Sincecore::slicelives incore,verify.shbuilds alibcore_noslice.rlib(acorewithout the slice inherent impls under test). It is built from therust-srcof the toolchain VeriFast 26.10 targets (nightly-2026-09-25), because VeriFast's rustc frontend cannot compile the repository's newerlibrary/coresubtree (nightly-2026-10-12addsabort_immediate/type_id_*intrinsics and drops theoptimize_attributegate). The slice sources under verification are the repository's, byte-for-byte (enforced by thediffstep); only the surroundingcorecomes from VeriFast's toolchain, as thelinked_listandraw_vecproofs already do with VeriFast's ownstdspecs. This also decouples the proof from subtree bumps such as Merge subtree update for toolchain nightly-2026-10-22 #737. The four visibility/attribute edits applied to thatcoreare a reviewablecore-sysroot.patch(adiff -ru), applied withpatch -p1byverify.sh.Complements (not required by the challenge)
slice/rotate.rs(ptr_rotate_memmove,ptr_rotate_gcd,ptr_rotate_swap) against theptr_rotatecontract used byrotate_left/rotate_right.from_raw_parts/from_raw_parts_mut(slice/raw.rs) are verified.array_share_split,slice_full_borrow_inv, and tuple/range ownership closing lemmas) into the VeriFast Rust prelude in a follow-up, so future proofs don't need to re-declare them; the gaps are filed as [Rust] No way to close <(T1, T2)>.own / <(T1, T2, T3)>.own / <Range<Idx>>.own from the components verifast/verifast#1049–Get rid of iterators rust-lang/rust#1056 and the const-generic support as [Rust] Support const generic parameters in specified functions verifast/verifast#1048.