Prove Rust Code Correct with Verus

Image for Developing provably correct Rust code with Verus

After studying this material, you should be able to:

  1. Explain what Verus is and why it exists
  2. Distinguish between Rust's built-in safety and formal program verification
  3. Understand how Verus specifications work (preconditions/postconditions)
  4. Identify key features and capabilities of Verus
  5. Recognize real-world applications of Verus

Step-by-Step Teaching

Step 1: Why Rust Alone Is Not Enough

Before understanding Verus, you need to understand the gap it fills.

Rust's Strengths

  • High performance (like C)
  • Type system automatically prevents many bugs

Rust's Limitations

Problem TypeC BehaviorRust BehaviorIdeal Behavior
Array out of boundsDangerous, unpredictableHalts program ✓Never happens at all ✓✓
Wrong computationSilently wrongSilently wrongMathematically proven correct
Secret data leaksPossiblePossibleProven impossible

Key Insight: Rust makes code safer, but not provably correct. There is a meaningful difference.


Step 2: What Is Verus?

Verus is an automated program verifier for Rust.

Think of it in three parts:

YOU provide  →  Formal specification (what code SHOULD do)
VERUS checks →  Does the code ACTUALLY match that specification?
RESULT       →  Mathematical proof valid for ALL possible inputs

Why "All Possible Inputs" Matters

Traditional Testing:

Test array: [1, 3, 5, 7]  ✓
Test array: [2, 4, 6]     ✓
Missing:    empty array?  ❓
Missing:    target at end? ❓

Verus Verification:

Every possible array → Proven correct ✓✓✓

Key Insight: Testing finds bugs that exist. Verification proves bugs cannot exist.


Step 3: How Verus Specifications Work

Verus uses two core concepts written directly in Rust source files:

Preconditions (requires)

  • Conditions that must be true BEFORE the function runs
  • Think of it as: "I will only work correctly if you give me this"

Postconditions (ensures)

  • Conditions that must be true AFTER the function runs
  • Think of it as: "I guarantee this will be true when I finish"

Concrete Example: Binary Search

FUNCTION: search(array, target)

PRECONDITION (requires):
  → The array must be sorted

POSTCONDITION (ensures):
  → If returns Some(index):
      - index is within array bounds
      - array[index] equals target value
  
  → If returns None:
      - target value is NOT anywhere in the array

Why the Second Clause Matters

Without the None clause, this implementation would pass verification:

// Technically satisfies incomplete specification
fn search(array, target) {
    return None;  // Always returns "not found"
}

Key Insight: Specifications must be complete. A missing clause creates a loophole that allows incorrect code to pass.


Step 4: Key Design Decisions That Make Verus Practical

Verus made specific choices that separate it from other verifiers:

1. Rust-Like Syntax

Other verifiers  →  Learn entirely new language for proofs
Verus            →  Write proofs in familiar Rust-like syntax

Benefit: Developers who wrote the code can also write the proofs

2. Fast Feedback Loop

Traditional verifiers  →  Minutes to verify one function
Verus                  →  Under one second feedback

Benefit: Works like normal coding with instant error messages, even "red squiggles" in VS Code

3. Proofs Stay With Code

Specifications live directly in source files
→ Proofs stay synchronized with code changes
→ No separate proof files to maintain

Step 5: Advanced Capabilities

Verifying Unsafe Rust Code

Rust allows unsafe code blocks for high performance, but the compiler stops checking safety there.

Normal Rust:   Compiler checks safety automatically
Unsafe Rust:   Developer must get it right manually ← RISKY
Unsafe + Verus: Mathematical proof restores safety guarantees ← BEST OF BOTH

Verifying Concurrent Code

Concurrent programs run in parallel and use locks to control data access.

Without Verus:

Thread A acquires lock → modifies data → releases lock
Is the data still valid? → Hope so...

With Verus:

Lock has an INVARIANT property attached
→ Acquiring lock: guaranteed data satisfies invariant
→ Releasing lock: must PROVE data still satisfies invariant
→ Lock implementation itself can be proven correct

Step 6: What Verus Guarantees Depend On

Verus is powerful but not magic. Its guarantees rely on:

Verus itself being correct
        +
Your specifications being accurate
        +
Assumptions about Rust standard library
        +
Compiler correctly converting source to executable
= Trustworthy verification

Key Insight: If your specification is wrong, Verus proves your code matches the wrong specification. Garbage in, garbage out.


Step 7: Real-World Applications

Here is how Verus is used across different domains:

ProjectDomainWhat Was Verified
VestData formatsParser/serializer correctness and security
VerdictCryptographyCertificate validation correctness
CapybaraKVStorageCrash safety of persistent memory logs
AtmosphereOperating SystemsMicrokernel correctness
AnvilCloud ComputingKubernetes controllers reach stable state
CortenMMMemory ManagementConcurrent code with complex locking
Amazon NitroCloud InfrastructureVirtual machine isolation correctness

Summary: The Complete Picture

PROBLEM:  Rust is safer than C, but not provably correct

SOLUTION: Verus adds formal verification to Rust

HOW:      Developer writes specifications (requires/ensures)
          Verus automatically proves code matches specs
          Valid for ALL possible inputs, not just test cases

RESULT:   Mathematically proven correct Rust code
          With fast feedback, familiar syntax, and AI support

Quick Self-Check Questions

  1. What is the difference between Rust halting on an out-of-bounds access versus Verus preventing it?
  2. Why is the None case in the binary search postcondition essential?
  3. What would happen if you wrote an incorrect specification and Verus verified it successfully?
  4. How does Verus handle unsafe Rust code differently than the Rust compiler?
  5. Name two real-world projects using Verus and what property they verified.

More to study