Technology Apr 21, 2026 · 11 min read

# Rust and Formal Verification: How to Prove Your Code Actually Works

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world! Let's talk about making software that doesn't just seem to work, but that we can prove works correctly. This is the world of f...

DE
DEV Community
by Nithin Bharadwaj
# Rust and Formal Verification: How to Prove Your Code Actually Works

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Let's talk about making software that doesn't just seem to work, but that we can prove works correctly. This is the world of formal verification. It can sound intimidating, like something reserved for rocket scientists and academic papers. But what if you could get much closer to that level of certainty using a language you already write for practical projects? That's where Rust comes in.

Think of it this way. Testing is like checking the lights in your house. You flip a switch and see if the kitchen light turns on. You test a few rooms. It looks good. Formal verification is like having the complete electrical blueprint. You can trace every wire from the fuse box to every bulb and switch. You can prove, on paper, that the kitchen light will turn on when you flip its specific switch, and only then. No hidden crossed wires, no chance of the toilet flushing turning off your TV. Rust helps us write those blueprints into the code itself.

I used to think of program crashes or bugs as inevitable, just things you hunt down with tests. Then I started with Rust and experienced something different. The compiler became my first and most stubborn reviewer. It wouldn't let me make a mess with memory. It was frustrating at first, but then it clicked. This wasn't just nagging; it was proving certain bad things could not happen in my program. That’s a powerful feeling.

So, how does a practical language like Rust do this? It starts with its core rules: ownership and borrowing. These aren't just fancy features; they are mathematical laws baked into the syntax. The compiler uses these laws to prove your code is free from memory bugs and data races. It's a built-in verification step for some of the most common and dangerous errors in software.

Let's look at a simple piece of code. We want a function that takes a reference to a number and adds one to it.

fn add_one(number: &mut i32) {
    *number += 1;
}

fn main() {
    let mut my_number = 5;
    add_one(&mut my_number);
    println!("My number is: {}", my_number); // Prints 6
}

This seems straightforward. But Rust is proving things here. The &mut i32 type is a "mutable reference." The ownership rules prove that while this reference exists, it is the only way to access my_number. No other part of my code can read or write my_number while add_one is running. This proves the operation is safe from certain kinds of interference. In other languages, you just hope that's true.

We can encode more sophisticated proofs using Rust's type system. Let's say we're writing software for a smart home. A light bulb can be either On or Off. A naive approach uses a boolean: true for on, false for off. But what does true mean? Could it get confused with something else? Let's use types to prove the state is always valid.

struct LightBulb {
    brightness: u8,
}

struct On;
struct Off;

impl LightBulb {
    fn turn_on(self) -> (LightBulb, On) {
        // Turning on sets a default brightness.
        let bulb = LightBulb { brightness: 100 };
        (bulb, On)
    }

    fn turn_off(self) -> (LightBulb, Off) {
        // Turning off might reset brightness.
        let bulb = LightBulb { brightness: 0 };
        (bulb, Off)
    }

    fn adjust_brightness(self, level: u8, _state: On) -> (LightBulb, On) {
        // You can only call this if you prove you have an `On` state token.
        let bulb = LightBulb { brightness: level.clamp(10, 100) };
        (bulb, On)
    }
}

fn main() {
    let bulb = LightBulb { brightness: 0 };
    let (bulb, state) = bulb.turn_on();
    // Now `state` is of type `On`.
    let (bulb, _) = bulb.adjust_brightness(75, state); // This works.
    // let (bulb, _) = bulb.adjust_brightness(75, state); // ERROR! `state` was used up above.
}

See what happened? The function adjust_brightness takes a parameter _state of type On. Not a boolean, but the actual type On. To call this function, you must possess an On value. You only get that value from turn_on. Furthermore, because we use the value (we take ownership of it), you can't call adjust_brightness twice with the same proof. The type system proves the sequence of operations: you turned it on, then you adjusted it. You cannot adjust a lightbulb that is off. This logic is checked at compile time.

This pattern is incredibly useful for security and safety. Imagine a network socket. You can't send data on a socket that isn't connected. You can't close a socket that's already closed. You can design your API so that the only way to get a "Send" capability is to first go through a "Connect" function that returns a connected socket type. The compiler verifies the correct workflow.

Where does this matter in the real world? Everywhere correctness is non-negotiable.

Cryptography: A classic vulnerability is a "timing attack." If checking a password takes a slightly different amount of time when the first character is wrong versus the last, an attacker can guess the password. A cryptographic library must prove its operations take constant time, regardless of the data. Rust's focus on explicit control and lack of hidden runtime checks helps. You can write code that performs the exact same sequence of instructions every time, and the compiler won't insert a secret branch that breaks the guarantee.

Financial Systems: Consider a function that transfers money between accounts. A critical property is "conservation of money." The total money in the system before and after the transfer must be the same; it just moves. You can structure your code to make this provable.

struct Money(u64);

struct Accounts {
    alice: Money,
    bob: Money,
}

impl Accounts {
    fn transfer(&mut self, amount: u64) -> Result<(), &'static str> {
        if self.alice.0 < amount {
            return Err("Alice has insufficient funds");
        }
        // This operation is critical. It must be atomic.
        self.alice.0 -= amount;
        self.bob.0 += amount;
        Ok(())
    }
    // A property we want: alice + bob is always the same.
    fn total(&self) -> u64 {
        self.alice.0 + self.bob.0
    }
}

While this simple code can't fully prove conservation at compile-time (Alice's balance could change elsewhere in a more complex system), Rust's ownership ensures that if we have a single &mut Accounts reference, no other code can change alice or bob during transfer. This isolates the critical block, making reasoning about it much easier. External tools, which we'll discuss, can then take this isolated block and prove the conservation property.

Embedded and Safety-Critical Systems: This is a major area. The Tock operating system for microcontrollers uses Rust's type system to build a secure kernel. Different driver capsules in the kernel are isolated from each other not by hardware, but by Rust's ownership. The compiler proves that a USB driver cannot suddenly access the memory of the temperature sensor. This creates a trusted computing base with far fewer lines of code that need to be manually audited.

Now, Rust's built-in system is powerful, but it's not the end of the story. The ecosystem is building tools that bring full-scale formal methods to Rust code.

One exciting project is Kani, a model checker from AWS. A model checker explores every possible execution path of your code, given certain limits. You give it invariants—things that must always be true—and it tries to find a path that breaks them.

Let's say we have a function that is supposed to always return a positive number.

fn absolute_value(x: i32) -> u32 {
    if x >= 0 {
        x as u32
    } else {
        (-x) as u32
    }
}

This looks correct. But does it handle the edge case where x is i32::MIN? The absolute value of -2147483648 is 2147483648, which doesn't fit in an i32. Our function casts -x to u32. When x is i32::MIN, -x overflows in a panic in debug mode, or wraps in release mode. This is a bug. Kani can find this.

You would write a verification harness:

#[cfg(kani)]
#[kani::proof]
fn verify_absolute_non_negative() {
    let x: i32 = kani::any();
    let result = absolute_value(x);
    // Our invariant: The result is always the absolute value.
    // We'll check a simpler property: result is not crazy large.
    assert!(result <= i32::MAX as u32);
}

When you run Kani, it doesn't use random numbers. It treats x as "any possible i32 value." It will systematically find that when x is i32::MIN, the -x operation panics, and it will report this as a failure. You've just formally proven the function is not correct for all inputs.

Another tool is Prusti. It sits between the compiler and full formal proof. You write annotations in your code about what your functions require (pre-conditions) and what they guarantee (post-conditions). Prusti then tries to prove these hold.

use prusti_contracts::*;

#[requires(x >= 0)] // This function requires x to be non-negative.
#[ensures(result == x * 2)] // It guarantees the result is twice x.
fn double_positive(x: i32) -> i32 {
    x + x // Let's intentionally write a bug: x + x for x=1 is 2, correct.
    // What if we wrote `x * 3` by mistake? Prusti would catch it.
}

These tools feel like a super-powered linter. They don't just check syntax; they check logic.

You might wonder, why not just use a dedicated proof language like Coq or Isabelle? Those are fantastic for what they do. But there's a gap: you prove an algorithm in Coq, then you have to manually translate that correct algorithm into C or Java for your real product. That translation step can introduce bugs. Rust flips this. You write your real product in Rust. Then, you use Rust's own features and tools like Kani or Prusti to verify the actual implementation. You're proving properties about the code that will ship.

This approach is practical. It fits into a normal development workflow. You write code. You write unit tests. You can also write a few key verification harnesses for the most critical properties. You run cargo kani or cargo prusti alongside cargo test. It becomes part of the suite of checks that give you confidence.

I find this changes how I think. I start designing APIs not just for convenience, but for provability. I ask: "What impossible states can I make unrepresentable in the type system?" If a function can fail, I make the success and error types distinct, so the caller must handle both. I use the type system to guide users to correct usage and prove away whole categories of mistakes.

This doesn't replace testing. They work together. Fuzz testing (cargo fuzz) throws random, structured data at your code to find panics. Property-based testing (proptest) lets you say "for all integers x and y, this property should hold" and it tests hundreds of random examples. Formal verification goes further: for all integers x and y, this property must hold, and here is the proof. Testing can find bugs; verification can show their absence.

The journey to verified software in Rust starts small. You don't need to prove your entire web server. Start with a core data structure. Write an Invoice type where the total field is guaranteed to equal the sum of the line_items. Encode it in the types so it can't be constructed incorrectly. Write a Kani harness to prove a critical function never panics. This incremental approach makes formal methods accessible.

Rust brings the dream of verified practical software closer. It turns the compiler from a grammar checker into a reasoning partner. It lets you embed proofs in your code structure. You get stronger guarantees, often with zero runtime cost, because the proof happens when you compile. For anyone building systems where failure has real consequences—whether financial, medical, or human—this isn't just academic. It's the future of reliable software engineering, and it's available now.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!

101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools

We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

DE
Source

This article was originally published by DEV Community and written by Nithin Bharadwaj.

Read original article on DEV Community
Back to Discover

Reading List