Skip to content

Ruling needed: what satisfies "must hold for generic type T (no monomorphization)" #734

Open

Description

@feliperodri

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 T and instantiated over a set of element types that covers every property of T the code observes, with a stated coverage argument.
  • Option B: the proof must be polymorphic in T at 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 a T whose Drop panics 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 of iter_mut, IterMut::next and IterMut::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

  1. A, amended: the instantiation set must reach every branch on size_of/align_of, including in callees (for Vec/VecDeque, a type larger than 1 KiB), and reviewers check those branches against the source.
  2. B: VeriFast (or another tool-level polymorphic proof) only. Kani submissions to these six challenges become ineligible, and the challenge pages should say so.
  3. 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 abstract T with symbolic size, alignment and trait behaviour. That would give Kani a real "for all T" 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

  1. added
    QuestionUse for any clarification or general question about the contest
    on Oct 21, 2026
  2. added theissue type on Oct 21, 2026
  3. DiuDiu777 commented on Oct 22, 2026

    @DiuDiu777

    Hi, @feliperodri.

    I’d recommend A “representative types plus a clear argument for why the results generalize.“ Covering every size_of / align_of branch 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. RawVec already puts much of its logic in RawVecInner, which takes a Layout parameter. A harness could start with:

    let size: usize = kani::any();
    let align: usize = kani::any();
    
    let Ok(layout) = Layout::from_size_align(size, align) else {
        return;
    };
    kani::assume(size % align == 0);
    
    // Pass layout and a symbolic capacity to the actual
    // implementation, then check the target properties.

    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)

  4. patricklam commented on Oct 22, 2026

    @patricklam

    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.

  5. feliperodri commented on Oct 22, 2026

    @feliperodri
    MemberAuthor

    @patricklam I tried a small experiment for a 3rd route: prove the property once for every T with 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> (two Option<T> slots and a len: u8) with new, push and pop, and the type invariant "len counts the occupied slots, and b is used only if a is".

    • Charon translates it without monomorphizing, and Aeneas turns it into a Lean model that is generic in T. In Lean, new establishes the invariant and push/pop preserve it and never overflow len, for every T. The proofs use only Lean's standard axioms.
    • Kani checks the same invariant for u8, u64, char, (u16, bool), [i32; 3], Option<u32> and String; all pass.
    • Breaking push (incrementing len when 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.

    • VeriFast: separation-logic annotations in comments in the Rust source, checked by symbolic execution one function at a time. It targets unsafe code, and for non-unsafe functions it generates the spec from the types (RustBelt's semantic well-typedness), which is why it fits RawVec and LinkedList. (@btj can correct me if I got this wrong)
    • Verus: specs and proof code in Rust syntax next to the code, discharged by Z3. It covers a subset of Rust; raw pointers are partially supported through vstd::raw_ptr, where each access needs a ghost PointsTo permission. Others stdlib functions are trusted through assume_specification. (@elanortang can correct me here if I got this wrong)
    • Charon → Aeneas → Lean: the Rust code carries no annotations, and the spec and proof sit in a separate Lean file, proved with tactics. Safe Rust only. What the other two don't have is the Kani half, which checks the same invariant on the code rustc compiles for concrete types.

    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 every T, 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 unsafe code over raw pointers (RawVec, Vec, VecDeque, LinkedList), so this route doesn't reach them as written. For VecDeque, the index arithmetic would fit, but it doesn't depend on T; the parts that do are the unsafe ones. With trait bounds, a proof for every T is 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionUse for any clarification or general question about the contest

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions