Rust 130 🦀 Formal Verification and Theorem Proving in Rust (Kani / Prusti)
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.
| Criterion | Kani | Prusti |
|---|---|---|
| Unsafe Rust | Yes | No |
| Generics | No (monomorphic only) | Yes |
| Automation | Fully automated (no annotations) | Requires contracts |
| Verification strength | Bounded (loop unrolling limit) | Unbounded (deductive) |
| Panic checking | Via assertions in harness | Default 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
| Command | Purpose |
|---|---|
cargo install --locked kani-verifier | Install Kani |
cargo kani setup | Configure toolchain |
cargo kani | Run verification |
kani::any() | Symbolic value |
#[kani::proof] | Verification harness |
Prusti Attributes
| Attribute | Purpose |
|---|---|
#[requires(...)] | Precondition |
#[ensures(...)] | Postcondition |
#[pure] | Function has no side effects |
#[trusted] | Skip verification of this function |
Verification Properties
| Tool | Property |
|---|---|
| Kani | Memory safety, assertions, bounds |
| Prusti | Functional 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Kani fails on generic code | Kani only handles monomorphic code | Monomorphize before verification |
| Prusti reports overflow | Arithmetic not constrained | Add #[requires] bounding the input |
| Prusti skips unsafe | Tool limitation | Use Kani for unsafe blocks |
| Kani misses deep bugs | Loop unrolling bound too low | Increase bound or use Prusti |
| Verification too slow | State space explosion | Reduce 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
| Item | Value |
|---|---|
| Kani type | Bounded model checker |
| Kani backend | CBMC |
| Kani input | MIR → goto-program |
| Kani automation | Fully automated |
| Kani unsafe support | Yes |
| Kani generics | No (monomorphic only) |
| Prusti type | Deductive verifier |
| Prusti backend | Viper |
| Prusti input | Contracts → Viper → SMT |
| Prusti automation | Requires annotations |
| Prusti unsafe support | No |
| Prusti generics | Yes |
| Prusti panic checking | Default |
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
unsafeblocks. 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.
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!