Repository navigation
Ruling needed: what satisfies "must hold for generic type T (no monomorphization)" #734
Description
Challenges 16, 17, 18, 19, 23, 24 and 25 say the verification "must hold for generic type T (no monomorphization)". Kani can't express that: every harness is a concrete instantiation. Six of these challenges have Kani submissions blocked on what the clause means (#549, #602, #632, #603, #651, #564, #681). The question, from #681 (comment #681 (comment)):
- Option A: a harness written once for arbitrary
Tand instantiated over a set of element types that covers every property ofTthe code observes, with a stated coverage argument. - Option B: the proof must be polymorphic in
Tat the tool level, which today means VeriFast.
What each approach misses
A 3-line generic function that reads past its buffer only when T is larger than 1 KiB, shaped like RawVec's min_non_zero_cap (which branches on element size 1, up to 1 KiB, or larger):
| Run | Result |
|---|---|
Kani, instantiated over #681's seven shapes ((), u8, bool, [u8; 3], u64, Al16, WithDrop) |
all 7 pass |
| Kani, plus a 2 KiB type | fails (pointer outside object bounds) |
| VeriFast, one proof of the generic function | fails (Pointer may be out of bounds) |
This isn't hypothetical for #681: its largest shape is 16 bytes and check_grow runs only for u8, so RawVec's large-element branch is never checked. The VeriFast RawVec proof covers all three branches.
VeriFast's guarantee has its own limits, unrelated to generics:
- Panics are allowed, and the proofs here run with
-ignore_unwind_paths, so panic safety (for example aTwhoseDroppanics mid-operation) isn't checked. Kani fails on any panic but doesn't model unwinding either. - 130 std functions are trusted through hand-written specs in VeriFast's
lib.rsspec. - The LinkedList and RawVec proofs contain 10
assume(false). Three of them replace the whole safety proof ofiter_mut,IterMut::nextandIterMut::next_back; the rest are subtyping lemmas ([Rust] Check struct interpretation variance verifast/verifast#610).
Other approved tools: goto-transcoder/ESBMC uses Kani's per-type output; RAPx analyzes generic MIR but only for its fixed tag set; Flux checks generic code once but can't track values written through pointers; KMIR doesn't support Vec or heap types yet.
Precedent
Ch19 is the only resolved challenge with the clause, solved with VeriFast. Per-type Kani instantiation was accepted where the clause is absent (Ch6 NonNull).
Options for the ruling
- A, amended: the instantiation set must reach every branch on
size_of/align_of, including in callees (forVec/VecDeque, a type larger than 1 KiB), and reviewers check those branches against the source. - B: VeriFast (or another tool-level polymorphic proof) only. Kani submissions to these six challenges become ineligible, and the challenge pages should say so.
- Later, a third route: Verify generic code once for an abstract
T(symbolic size, alignment and trait behaviour) kani#5041 proposes verifying generic code once for an abstractTwith symbolic size, alignment and trait behaviour. That would give Kani a real "for allT" proof, but it doesn't exist yet.
Whichever is chosen, it should be written into the challenge book so it applies to all seven challenges.
Activity
feliperodri commentedon Oct 21, 2026
any thoughts? @btj @patricklam @tautschnig @rajath-mk @HuStmpHrrr @celinval @remi-delmas-3000 @rahulku @rafaelsamenezes @lucasccordeiro @dkcumming @DiuDiu777
DiuDiu777 commentedon Oct 22, 2026
Hi, @feliperodri.
I’d recommend A “representative types plus a clear argument for why the results generalize.“ Covering every
size_of / align_ofbranch is a useful baseline, but we still need to explain why other types can’t introduce new safety issues. Capacity arithmetic, pointer offsets, and trait behavior can vary even within the same branch.For code that mainly depends on type layout, one approach is to make size and alignment symbolic.
RawVecalready puts much of its logic inRawVecInner, which takes a Layout parameter. A harness could start with:This would need two pieces: Kani checks the internal implementation for all allowed layouts and states, and a separate argument shows that the generic wrapper passes the right layout and preserves the required invariants for any T.
Drop,Clone, and value validity would still need their own contracts and reasoning in other ways (maybe with explicit assumptions)patricklam commentedon Oct 22, 2026
This is kind of a placeholder comment, but I am actually interested into looking into this in more depth and figuring out what would be needed to have Kani support this. (In particular I am looking for projects for graduate students). I see that @DiuDiu777 touched on what is needed in the above comment.
feliperodri commentedon Oct 22, 2026
@patricklam I tried a small experiment for a 3rd route: prove the property once for every
Twith Charon → Aeneas → Lean, and check the same property on concrete types with Kani (see this https://github.com/feliperodri/kani/blob/charon-type-invariant/experiments/charon-type-invariant/WALKTHROUGH.md).The example is a generic
Slots<T>(twoOption<T>slots and alen: u8) withnew,pushandpop, and the type invariant "lencounts the occupied slots, andbis used only ifais".T. In Lean,newestablishes the invariant andpush/poppreserve it and never overflowlen, for everyT. The proofs use only Lean's standard axioms.u8,u64,char,(u16, bool),[i32; 3],Option<u32>andString; all pass.push(incrementinglenwhen both slots are full) fails both the Lean proof and the Kani harnesses.Compared with Verus and VeriFast: all three verify a generic function once rather than per instance. They differ in where the spec lives and what code they reach.
unsafecode, and for non-unsafefunctions it generates the spec from the types (RustBelt's semantic well-typedness), which is why it fitsRawVecandLinkedList. (@btj can correct me if I got this wrong)vstd::raw_ptr, where each access needs a ghostPointsTopermission. Others stdlib functions are trusted throughassume_specification. (@elanortang can correct me here if I got this wrong)The experiment shows only that the two halves can share one invariant and that both reject the same mutant. Each covers a gap in the other. Kani runs the compiled code for a concrete
T, checks for undefined behaviour and layout-dependent bugs, and returns a concrete counterexample without anyone writing a proof. The Lean proof covers everyT, which no list of instances does (the 2 KiB case in the issue description). I haven't tried it, but the two could feed each other in an agent loop: propose an invariant, let Kani find a counterexample on some instance, refine, and attempt the Lean proof once the instances pass; or use an unsolved Lean goal to choose the next instance to check. I don't know whether that converges faster than either tool alone, and I'd like to hear from anyone who has tried something similar with Verus or VeriFast. @patricklam perhps this could be the direction for a graduate student to explore.What it doesn't settle for this issue: Aeneas works on safe Rust, and the challenges here are mostly
unsafecode over raw pointers (RawVec,Vec,VecDeque,LinkedList), so this route doesn't reach them as written. ForVecDeque, the index arithmetic would fit, but it doesn't depend onT; the parts that do are the unsafe ones. With trait bounds, a proof for everyTis also only as good as the trait specs each concrete impl has to meet... So for these challenges I still think something like @DiuDiu777's split is the practical answer (check the layout-dependent core for all layouts, and argue separately about the wrapper), and this route is for safe generic code.docs(ch8): fix working requirements interpretation from organizer sta…