| |

Rust 130 🦀 Formal Verification and Theorem Proving in Rust (Kani / Prusti)

⏱️ Estimated read: 11 min | |

Testing shows the presence of bugs, never their absence. Fuzzing finds panics and crashes, but it cannot prove that a function returns the correct value for every possible input. Formal verification is the discipline of mathematically proving that a program satisfies a specification. For Rust, two tools occupy opposite ends of the verification spectrum. Kani is a bounded model checker: it exhaustively explores the state space of a program up to a configurable depth, catching memory safety violations and assertion failures without requiring annotations. Prusti is a deductive verifier: it uses function contracts and loop invariants to prove properties for all inputs, but requires the programmer to write those specifications. The choice between them is a trade-off between automation and completeness, between finding bugs and proving absence.

Key point: Kani is fully automated and verifies unsafe Rust, but it is bounded. Prusti is unbounded and handles generics, but it cannot reason about unsafe code and requires annotations. A program that is safe and annotated is a candidate for Prusti; a program that is unsafe or annotation-free is a candidate for Kani.


Why formal verification tools exist for Rust

The testing gap. A unit test checks a specific input. A property-based test checks random inputs up to a size limit. Neither can prove that a function is correct for all inputs. Formal verification closes this gap: it either produces a mathematical proof of correctness or a concrete counterexample. In safety-critical domains—cryptography, aerospace, medical devices—this distinction is the difference between “we tested it” and “we proved it.”

The bounded model checking approach. Kani translates Rust’s Mid-level Intermediate Representation (MIR) into a goto-program, which is then verified by CBMC, a bounded model checker . “Bounded” means that loops are unrolled up to a fixed depth, and recursive calls are explored to a finite depth. If no assertion fails within that bound, the program is correct up to that bound—but not beyond it. Kani is fully automated: the programmer writes a harness with #[kani::proof] and assertions, and Kani either says “VERIFICATION SUCCESSFUL” or “FAILED” with a trace .

The deductive verification approach. Prusti uses the Viper verification infrastructure to encode Rust functions and their contracts into an intermediate verification language . The programmer adds #[requires] preconditions and #[ensures] postconditions. Prusti then attempts to prove that the implementation satisfies the contract for all inputs, including unbounded loops and arbitrary sizes. This is stronger than bounded model checking, but it requires specification effort. Prusti also checks for panics by default: if the code might reach an unreachable!() or a division by zero, Prusti reports an error unless it can prove the panic is impossible .

The unsafe boundary. Kani can verify unsafe Rust. It checks memory safety properties automatically and can verify user-specified assertions within unsafe blocks . Prusti cannot reason about unsafe code. Its analysis is sound only for safe Rust. This is the most consequential difference: a crate with unsafe blocks must be verified with Kani (or another tool), not Prusti.

The generic problem. Kani only verifies monomorphic code. It cannot handle generic functions directly; they must be monomorphized before verification . Prusti handles generic code natively, making it suitable for library crates with abstract types. A generic data structure with complex invariants is a candidate for Prusti; a concrete parser with unsafe byte manipulation is a candidate for Kani.

The trade-off. Kani’s automation is its strength, but its bounded nature is a limitation. A bug that requires unrolling a loop 1000 times will not be found if the bound is 100. Prusti’s proofs are stronger, but the annotation burden is real. The ideal workflow uses both: Kani for rapid bug discovery and memory safety, Prusti for proving critical properties once the code is stable.


a. Kani: model checking without annotations

Kani is installed as a Cargo subcommand and invoked with cargo kani. The verification harness is a function annotated with #[kani::proof]. Inside the harness, the programmer can use kani::any() to introduce symbolic values—values that represent any value of the given type. Kani then explores all possible paths through the function.

#[cfg(kani)]
mod verification {
    use super::*;

    #[kani::proof]
    fn check_abs() {
        let x: i32 = kani::any();
        let result = abs(x);
        assert!(result >= 0);
    }
}

When cargo kani runs, it compiles the crate, translates the harness into a verification model, and invokes CBMC. If the assertion result >= 0 holds for all i32 values within the bound, Kani reports success. If it fails, Kani provides a concrete counterexample: a specific value of x that makes the assertion false. This trace is the diagnostic artifact—it is the input that breaks the program .

Kani’s unbounded verification is available for certain cases through the --unwind flag with a large bound, but the fundamental limitation remains: loops and recursion must be bounded. The Kani team has worked on extending this with function contracts and loop invariants, but the tool remains a bounded model checker at its core .


b. Prusti: deductive verification with contracts

Prusti is installed as prusti-cli and runs either from the command line (cargo prusti) or as a VS Code extension that verifies on save . The programmer adds specifications using attributes from the prusti_contracts crate.

use prusti_contracts::*;

#[requires(x > 0)]
#[ensures(result > 0)]
fn increment(x: i32) -> i32 {
    x + 1
}

The precondition #[requires(x > 0)] states the contract that the caller must satisfy. The postcondition #[ensures(result > 0)] states what the function guarantees. Prusti verifies that the implementation satisfies the postcondition for every input that meets the precondition. If the function is called elsewhere in the crate, Prusti also verifies that the call site satisfies the precondition.

Prusti’s default behavior includes checking for panics. A call to unwrap(), an array index, or an arithmetic operation that might overflow is flagged as a potential panic unless Prusti can prove it is unreachable. The CHECK_BINARY_OPERATIONS=true setting in Prusti.toml enables overflow checking, which is essential for proving arithmetic correctness but requires the programmer to add preconditions constraining the input range .

The VS Code extension provides incremental verification: as the programmer types, Prusti runs in the background and displays errors inline. This makes the specification process interactive, with feedback at the speed of a type checker.


c. Choosing the right tool

The decision tree is straightforward. If the code contains unsafe blocks, Kani is the only option among the two. If the code is purely safe and relies on generics, Prusti is the better fit. If the goal is rapid bug discovery without writing specifications, Kani requires only a harness with assertions. If the goal is a mathematical proof of correctness for all inputs, Prusti requires contracts but provides the stronger guarantee.

CriterionKaniPrusti
Unsafe RustYesNo
GenericsNo (monomorphic only)Yes
AutomationFully automated (no annotations)Requires contracts
Verification strengthBounded (loop unrolling limit)Unbounded (deductive)
Panic checkingVia assertions in harnessDefault behavior

A practical workflow uses Kani first: write harnesses with assertions, run cargo kani, and fix the bugs it finds. Once the code is stable, add Prusti contracts to the critical functions—the ones where a bug would be catastrophic—and prove that the contracts hold. The two tools are complementary, not competing.


Complete Example Session

# ============================================
# PART 1: INSTALLING KANI
# ============================================

cargo install --locked kani-verifier
cargo kani setup
# Kani is now available as a cargo subcommand.
# ============================================
# PART 2: KANI HARNESS
# ============================================

#[cfg(kani)]
mod verification {
    use super::*;

    #[kani::proof]
    fn check_abs() {
        let x: i32 = kani::any();
        let result = abs(x);
        assert!(result >= 0);
    }
}
# ============================================
# PART 3: RUN KANI
# ============================================

cargo kani
# If the assertion holds for all i32 values:
# VERIFICATION SUCCESSFUL
# If not:
# VERIFICATION FAILED
# with a concrete counterexample for x.
# ============================================
# PART 4: INSTALLING PRUSTI
# ============================================

cargo install prusti-cli
# Or install the VS Code extension for inline verification.
# ============================================
# PART 5: PRUSTI CONTRACTS
# ============================================

use prusti_contracts::*;

#[requires(x > 0)]
#[ensures(result > x)]
fn increment(x: i32) -> i32 {
    x + 1
}
# ============================================
# PART 6: RUN PRUSTI
# ============================================

cargo prusti
# If the implementation satisfies the contract:
# Verification succeeded
# If not:
# Verification failed with a specific error.
# ============================================
# PART 7: PRUSTI OVERFLOW CHECKING
# ============================================

# Prusti.toml
CHECK_BINARY_OPERATIONS = true

# Without a precondition, overflow is reported:
#[ensures(result > 0)]
fn increment(x: i32) -> i32 {
    x + 1  // might overflow when x == i32::MAX
}
# ============================================
# PART 8: FIXING WITH A PRECONDITION
# ============================================

#[requires(x < i32::MAX)]
#[ensures(result > 0)]
fn increment(x: i32) -> i32 {
    x + 1
}
# Prusti verifies that the precondition prevents overflow.
# ============================================
# PART 9: COMBINING TOOLS
# ============================================

# Use Kani for the unsafe parsing layer.
# Use Prusti for the safe business logic layer.
# Kani finds bugs without annotations.
# Prusti proves properties with contracts.

The nine parts trace the formal verification workflow: installing Kani, writing a harness, running the model checker, installing Prusti, writing contracts, running the deductive verifier, enabling overflow checks, fixing with preconditions, and combining the tools for a layered defense.


Quick Reference

Kani Commands

CommandPurpose
cargo install --locked kani-verifierInstall Kani
cargo kani setupConfigure toolchain
cargo kaniRun verification
kani::any()Symbolic value
#[kani::proof]Verification harness

Prusti Attributes

AttributePurpose
#[requires(...)]Precondition
#[ensures(...)]Postcondition
#[pure]Function has no side effects
#[trusted]Skip verification of this function

Verification Properties

ToolProperty
KaniMemory safety, assertions, bounds
PrustiFunctional correctness, panic freedom

Best Practices

✅ Do This:

#[kani::proof]
fn check_abs() {
    let x: i32 = kani::any();
    assert!(abs(x) >= 0);
}
// ✅ Symbolic input for exhaustive bounded checking
#[requires(x < i32::MAX)]
// ✅ Precondition prevents overflow

❌ Don’t Do This:

// Use Prusti on unsafe code
// ❌ Prusti cannot reason about unsafe blocks
// Assume Kani proves correctness for all inputs
// ❌ Kani is bounded; loops are unrolled to a limit

Common Pitfalls

PitfallWhy It HappensFix
Kani fails on generic codeKani only handles monomorphic codeMonomorphize before verification
Prusti reports overflowArithmetic not constrainedAdd #[requires] bounding the input
Prusti skips unsafeTool limitationUse Kani for unsafe blocks
Kani misses deep bugsLoop unrolling bound too lowIncrease bound or use Prusti
Verification too slowState space explosionReduce input size or add assumptions

Real-World Examples

1. Kani Harness

#[kani::proof]
fn check_add() {
    let a: u32 = kani::any();
    let b: u32 = kani::any();
    kani::assume(a < 1000 && b < 1000);
    assert!(a + b < 2000);
}

2. Prusti Contract

#[requires(x > 0)]
#[ensures(result > x)]
fn increment(x: i32) -> i32 { x + 1 }

3. Prusti Overflow Check

CHECK_BINARY_OPERATIONS = true

4. Prusti Panic Freedom

// Prusti checks that unwrap() does not panic.
#[ensures(result.is_some())]
fn safe_lookup() -> Option<u32> { Some(42) }

5. Kani Assume

kani::assume(x != 0);

6. Kani Unwind Bound

cargo kani --unwind 100

7. Prusti VS Code

Save file → Prusti runs → errors appear inline.

8. Kani Memory Safety

#[kani::proof]
fn check_unsafe() {
    let ptr = unsafe { alloc() };
    assert!(!ptr.is_null());
}

9. Prusti Precondition

#[requires(x + y <= u32::MAX)]
fn average(x: u32, y: u32) -> u32 { (x + y) / 2 }

10. Kani Counterexample

VERIFICATION FAILED
x = 4294967295

Visual

Verification Approaches

┌──────────────────────────────────────────────┐
│  Kani: Bounded Model Checking                │
│                                              │
│  Harness → MIR → goto-program → CBMC        │
│  Explore all paths up to unwind bound        │
│  Success: no assertion fails within bound    │
│  Failure: concrete counterexample            │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Prusti: Deductive Verification              │
│                                              │
│  Contracts → Viper → SMT solver              │
│  Prove postcondition for all inputs          │
│  Success: mathematical proof                 │
│  Failure: contract violation or panic        │
└──────────────────────────────────────────────┘

Kani explores concrete paths; Prusti reasons symbolically about all inputs.

Decision Flow

┌──────────────────────────────────────────────┐
│  Does the code contain unsafe blocks?        │
└──────────┬───────────────────────┬───────────┘
           │ YES                   │ NO
           ▼                       ▼
┌──────────────────────┐  ┌──────────────────────┐
│  Use Kani            │  │  Need unbounded      │
│  (memory safety)     │  │  proof?              │
└──────────────────────┘  └──────────┬───────────┘
                                     │ YES
                                     ▼
                          ┌──────────────────────┐
                          │  Use Prusti          │
                          │  (contracts)         │
                          └──────────────────────┘

Summary

ItemValue
Kani typeBounded model checker
Kani backendCBMC
Kani inputMIR → goto-program
Kani automationFully automated
Kani unsafe supportYes
Kani genericsNo (monomorphic only)
Prusti typeDeductive verifier
Prusti backendViper
Prusti inputContracts → Viper → SMT
Prusti automationRequires annotations
Prusti unsafe supportNo
Prusti genericsYes
Prusti panic checkingDefault

Key takeaways:

  • Kani is automated but bounded. It explores all paths up to a loop unrolling limit, catching memory safety violations and assertion failures without annotations. The bound is the limitation: a bug beyond the bound is not found .
  • Prusti is unbounded but annotated. It proves contracts for all inputs, including arbitrary loop iterations, but requires the programmer to write preconditions, postconditions, and loop invariants .
  • Unsafe code requires Kani. Prusti cannot reason about unsafe blocks. A crate with unsafe parsing or FFI must use Kani for those layers .
  • Generics require Prusti. Kani only verifies monomorphic code. A generic library with abstract invariants is a candidate for Prusti’s deductive approach .
  • Panic freedom is a Prusti default. Prusti checks that unreachable!(), division by zero, and overflow cannot occur unless proven otherwise. This is a stronger guarantee than Kani’s assertion-based checking .

Remember: Formal verification is not a replacement for testing; it is a complement. Kani finds the bugs that fuzzing misses—the edge cases that require symbolic reasoning. Prusti proves the properties that testing can only sample. The workflow is: test, fuzz, then verify. Kani for the unsafe and concrete layers. Prusti for the safe and abstract layers. The goal is not to prove the entire program correct—that is often infeasible—but to prove the critical functions correct and to find the bugs that the other methods cannot.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!