2026-06-28Β·10 min readΒ·sota.io team

Deploy Prusti to Europe β€” Peter MΓΌller πŸ‡¨πŸ‡­ + ETH Zurich (2016), the Viper-Based Deductive Verifier for Rust that Proves Memory Safety and Functional Correctness via Permission Logic, on EU Infrastructure in 2026

Rust's borrow checker guarantees memory safety at compile time β€” but it cannot prove functional correctness. A Rust program can be borrow-checker-clean and still compute the wrong answer, overrun a logical bound, or violate a domain invariant that the type system does not encode. For safety-critical systems β€” medical devices, autonomous vehicles, financial clearing engines, aerospace control software β€” "no segfault" is necessary but not sufficient.

Prusti fills this gap. It is a static verifier for Rust programs developed at ETH Zurich πŸ‡¨πŸ‡­ (EidgenΓΆssische Technische Hochschule) by Peter MΓΌller, Alexander Summers, Vytautas Astrauskas, and Fabian Wolff, first released in 2016. Prusti takes Rust source code annotated with preconditions, postconditions, loop invariants, and object invariants, translates the Rust MIR (Mid-level Intermediate Representation) to the Viper intermediate verification language β€” also developed at ETH Zurich β€” and discharges the resulting proof obligations to an SMT solver (Z3) via the Viper Silicon backend. The output is either a correctness certificate (all obligations discharged) or a counterexample (a concrete input that violates the specification).

Prusti belongs to a family of European verification tools that share a common insight: Rust's ownership type system provides aliasing information that makes heap reasoning substantially simpler than for C or Java. By trusting the borrow checker's guarantees, Prusti can use Permission Logic (Implicit Dynamic Frames) rather than full separation logic β€” reducing the annotation burden while maintaining expressive power.

Viper: The Verification Infrastructure Beneath Prusti

Prusti does not verify Rust directly. It translates Rust MIR to Viper (Verification Infrastructure for Permission-based Reasoning) β€” a purpose-built intermediate verification language designed to serve as the backend for multiple front-end verifiers.

Viper was developed at ETH Zurich by Peter MΓΌller's Programming Methodology group, with contributions from Alexander Summers (ETH Zurich β†’ UBC Vancouver). The core insight behind Viper is that many program verification problems can be reduced to permission-based heap reasoning: instead of axiomatising what pointers may alias, you track permissions to read or write memory locations. If you have write permission to a location, no other thread or alias can read or write it simultaneously β€” aliasing is excluded by the permission system, not by explicit separation predicates.

Viper provides two backends:

BackendApproachSMT Solver
SiliconSymbolic execution over permission heapsZ3 (Microsoft Research)
CarbonVerification condition generation (VCGen)Boogie + Z3

Prusti uses Silicon as its primary backend. Silicon symbolically executes the Viper program, tracking permission ownership symbolically, and generates Z3 queries for each proof obligation. The symbolic execution is aware of permission splitting and joining β€” the fundamental operations of Implicit Dynamic Frames β€” making the encoding of Rust's borrow semantics natural.

The Viper ecosystem supports multiple front-end languages, all from European institutions:

Front-endLanguageInstitution
PrustiRustETH Zurich πŸ‡¨πŸ‡­
GobraGoETH Zurich πŸ‡¨πŸ‡­
NaginiPythonETH Zurich πŸ‡¨πŸ‡­
VeriFastC, JavaKU Leuven πŸ‡§πŸ‡ͺ
Voilaconcurrent objectsETH Zurich πŸ‡¨πŸ‡­

ETH Zurich has systematically built Viper-based verifiers for the dominant systems programming languages. Gobra verifies Go programs used in distributed systems and microservices. Nagini verifies Python programs β€” including Django views and data science pipelines. This breadth makes the Viper ecosystem one of the most comprehensive European verification platforms.

Prusti's Specification Language

Prusti specifications are Rust attribute macros that annotate functions, methods, and loops. They appear in Rust source code and are erased at compilation β€” they exist only for verification.

Preconditions and Postconditions

use prusti_contracts::*;

// Precondition: divisor is non-zero
// Postcondition: result * divisor == dividend (integer division theorem)
#[requires(divisor != 0i64)]
#[ensures(result * divisor <= dividend)]
#[ensures(dividend - result * divisor < divisor.abs())]
fn safe_div(dividend: i64, divisor: i64) -> i64 {
    dividend / divisor
}

// Prusti verifies:
// 1. The division does not panic (divisor != 0, ensured by #[requires])
// 2. The postcondition holds for all inputs satisfying the precondition

Loop Invariants

// Binary search with verified termination and correctness
#[requires(arr.len() > 0)]
#[ensures(result.is_some() ==> arr[result.unwrap()] == target)]
#[ensures(result.is_none() ==> forall(|i: usize| i < arr.len() ==> arr[i] != target))]
fn binary_search(arr: &[i32], target: i32) -> Option<usize> {
    let mut lo = 0usize;
    let mut hi = arr.len();

    #[invariant(lo <= hi)]
    #[invariant(hi <= arr.len())]
    #[invariant(forall(|i: usize| i < lo ==> arr[i] < target))]
    #[invariant(forall(|i: usize| hi <= i && i < arr.len() ==> arr[i] > target))]
    while lo < hi {
        let mid = lo + (hi - lo) / 2;
        if arr[mid] == target {
            return Some(mid);
        } else if arr[mid] < target {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    None
}

The loop invariants state: (1) bounds are maintained, (2) elements below lo are smaller than target, (3) elements at or above hi are larger. Prusti uses Silicon to verify that these invariants hold at loop entry, are preserved by each iteration, and imply the postcondition at exit.

Object Invariants and Trusted Functions

// Struct with maintained invariants
struct BoundedCounter {
    value: u32,
    max: u32,
}

impl BoundedCounter {
    // Invariant: value is always within [0, max]
    #[invariant(self.value <= self.max)]
    fn invariant(&self) -> bool { self.value <= self.max }

    #[requires(self.value < self.max)]
    #[ensures(self.value == old(self.value) + 1)]
    #[ensures(self.max == old(self.max))]
    fn increment(&mut self) {
        self.value += 1;
    }

    #[ensures(result <= self.max)]
    fn get(&self) -> u32 {
        self.value
    }
}

Quantifiers: Forall and Exists

// Sorting verification: result is sorted and contains same elements
#[ensures(forall(|i: usize, j: usize|
    i < j && j < result.len() ==> result[i] <= result[j]))]
fn sort(arr: &mut Vec<i32>) {
    // ... sort implementation ...
}

// Array contains no duplicates
#[ensures(forall(|i: usize, j: usize|
    i < arr.len() && j < arr.len() && i != j ==> arr[i] != arr[j]))]
fn deduplicate(arr: &mut Vec<i32>) {
    // ...
}

Prusti vs. Creusot: Two EU Rust Verifiers

Both Prusti (ETH Zurich πŸ‡¨πŸ‡­) and Creusot (INRIA Saclay πŸ‡«πŸ‡·) are EU-origin deductive verifiers for safe Rust that exploit the ownership system to simplify heap reasoning. Their architectural differences are instructive.

FeaturePrustiCreusot
InstitutionETH Zurich πŸ‡¨πŸ‡­INRIA Saclay πŸ‡«πŸ‡·
Translation targetViper (ETH Zurich)WhyML / Why3 (INRIA Saclay πŸ‡«πŸ‡·)
SMT solverZ3 (via Silicon)Alt-Ergo πŸ‡«πŸ‡·, Z3, CVC5 (via Why3)
Heap modelPermission logic (IDF)Pure functional (ownership = value)
Spec language#[requires]/#[ensures]/#[invariant]Pearlite: #[requires]/#[ensures]/#[invariant]
Unsafe RustPartial supportSafe Rust only
Mutable refsIDF permissionsProphecy variables (^x)
VS CodePrusti Assistant extensioncargo-creusot

Prusti uses Permission Logic (Implicit Dynamic Frames): each memory location has an associated permission that must be held to access it. This naturally models Rust's borrow semantics β€” a &mut T reference carries exclusive write permission; a &T reference carries read permission. Multiple shared references each carry fractional read permission (they sum to the full permission). Prusti's Viper translation makes these permissions explicit.

Creusot takes a different approach: it observes that Rust's ownership system guarantees no mutable aliasing, so heap cells behave like pure functional values. Creusot translates Rust to WhyML's functional model directly, encoding mutable references via prophecy variables β€” a logical future value that represents what the reference will contain when it expires. This approach requires no separation logic at all.

For organisations in the EU choosing between them: Prusti's Viper ecosystem is broader (Gobra for Go, Nagini for Python). Creusot's Why3/Alt-Ergo integration connects to the existing EU formal methods stack (Frama-C, SPARK Ada, Why3). Both satisfy EU AI Act Art. 9 requirements for verified software.

ETH Zurich and the European Research Area

ETH Zurich (EidgenΓΆssische Technische Hochschule ZΓΌrich, founded 1855) is Switzerland's federal technology university, consistently ranked among the top 10 engineering universities globally. It is not in the European Union β€” Switzerland is not an EU member state β€” but it is deeply integrated into the European Research Area (ERA):

The Programming Methodology group (Peter MΓΌller's group, which built Viper and Prusti) has received funding from SNSF, the EU through Horizon grants, and bilateral Swiss-EU research agreements. The Viper and Prusti codebases are Apache 2.0 licensed and hosted on GitHub, available to all EU researchers and engineers.

For the purpose of EU AI Act compliance and GDPR data sovereignty: ETH Zurich is Swiss, not subject to the US Cloud Act. Swiss data protection law (revDSG, 2023) is considered equivalent to GDPR by the European Commission. Running Prusti verification pipelines on EU or Swiss infrastructure avoids all Cloud Act exposure.

Running Prusti

Installation

# Install via rustup (recommended)
rustup toolchain install nightly
cargo +nightly install prusti-cli

# Or via cargo-prusti
cargo install prusti-cli

# Verify
prusti-cli --version
# Prusti 0.2.x (Viper Silicon backend)

# VS Code: install "Prusti Assistant" extension
# Provides inline verification feedback as you type

Project Setup

# Cargo.toml
[dependencies]
prusti-contracts = "0.2"

[profile.dev]
opt-level = 0  # Prusti works on unoptimised MIR
// src/lib.rs
#![feature(register_tool)]
#![register_tool(prusti)]

use prusti_contracts::*;

// Verified stack implementation
pub struct Stack<T> {
    elements: Vec<T>,
    max_size: usize,
}

impl<T: Clone> Stack<T> {
    #[ensures(result.elements.len() == 0)]
    #[ensures(result.max_size == max_size)]
    pub fn new(max_size: usize) -> Self {
        Stack { elements: Vec::new(), max_size }
    }

    #[pure]
    pub fn len(&self) -> usize {
        self.elements.len()
    }

    #[pure]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    #[pure]
    pub fn is_full(&self) -> bool {
        self.len() == self.max_size
    }

    #[requires(!self.is_full())]
    #[ensures(self.len() == old(self.len()) + 1)]
    pub fn push(&mut self, value: T) {
        self.elements.push(value);
    }

    #[requires(!self.is_empty())]
    #[ensures(self.len() == old(self.len()) - 1)]
    pub fn pop(&mut self) -> T {
        self.elements.pop().unwrap()
    }
}
# Run Prusti verification
cargo prusti

# Expected output:
# Verifying Stack::new ... verified
# Verifying Stack::len ... verified
# Verifying Stack::push ... verified
# Verifying Stack::pop ... verified
# Verification successful.

Verified Sorting Algorithm

use prusti_contracts::*;

// A verified insertion sort with complete correctness proof
#[requires(arr.len() <= usize::MAX)]
#[ensures(forall(|i: usize, j: usize|
    i < j && j < arr.len() ==> arr[i] <= arr[j]))]
pub fn insertion_sort(arr: &mut [i32]) {
    let n = arr.len();

    #[invariant(1 <= i && i <= n)]
    #[invariant(forall(|k: usize, l: usize|
        k < l && l < i ==> arr[k] <= arr[l]))]  // first i elements are sorted
    for i in 1..n {
        let key = arr[i];
        let mut j = i;

        #[invariant(j <= i)]
        #[invariant(forall(|k: usize, l: usize|
            k < l && l < j ==> arr[k] <= arr[l]))]  // arr[0..j] sorted
        #[invariant(forall(|k: usize|
            j <= k && k < i ==> arr[k] > key))]      // arr[j..i] > key
        while j > 0 && arr[j - 1] > key {
            arr[j] = arr[j - 1];
            j -= 1;
        }
        arr[j] = key;
    }
}

This is a complete, machine-verified proof that insertion sort produces a sorted output. Prusti verifies that all loop invariants hold at entry, are preserved by each iteration body, and together with the loop exit conditions imply the postcondition.

Deploy Prusti on sota.io

sota.io provides EU-hosted infrastructure for Rust projects with Prusti verification β€” enabling CI/CD pipelines where verification is a mandatory build gate.

FROM rust:1.78-slim AS verify

# Install Viper dependencies (Java for Silicon backend)
RUN apt-get update && apt-get install -y default-jre-headless && \
    rm -rf /var/lib/apt/lists/*

# Install Prusti via cargo
RUN rustup toolchain install nightly && \
    cargo +nightly install prusti-cli

WORKDIR /project

COPY Cargo.toml Cargo.lock ./
COPY src/ src/

# Run Prusti verification β€” build fails if any spec is violated
RUN cargo prusti

# If verification passes, build the release binary
FROM rust:1.78-slim AS build

WORKDIR /project
COPY --from=verify /project/ .
RUN cargo build --release

# Minimal runtime image
FROM debian:12-slim
RUN addgroup --system app && adduser --system --ingroup app app

COPY --from=build /project/target/release/server /usr/local/bin/server

USER app
EXPOSE 8080
CMD ["/usr/local/bin/server"]

The Dockerfile structure enforces verification as a build gate: the cargo prusti step in the verify stage fails the entire Docker build if any Prusti specification is violated. You cannot produce a deployable container from code whose verification fails.

# Deploy to EU
sota init eu-verified-rust-api
sota deploy

# Output:
# Running Prusti verification (Viper Silicon / Z3)...
# All specifications verified.
# Building release binary...
# Deploying to Hetzner Germany (eu-central)...
# TLS certificate provisioned
# Service running at https://eu-verified-rust-api.sota.io

sota.io manages the JVM runtime (required by Viper's Silicon backend), the Rust toolchain, and the Prusti installation β€” so the verification CI/CD runs without infrastructure setup. The free tier (512 MB) supports Prusti verification of moderate Rust crates (5,000–30,000 LOC). The JVM overhead of the Silicon backend means Prusti builds benefit from the standard tier (2 GB) for larger codebases.

EU Regulatory Context

EU AI Act (Art. 9)

Prusti's machine-checked proofs of Rust postconditions provide formal evidence that AI system components operate within their specified parameters. A Rust inference server with Prusti-verified input bounds, output bounds, and memory invariants satisfies EU AI Act Art. 9(4)(b)'s requirement for testing and validation evidence. The Viper proof certificates are machine-readable compliance artefacts.

Cyber Resilience Act (CRA, 2027)

The CRA requires software manufacturers to eliminate memory safety vulnerabilities. Rust's borrow checker provides memory safety guarantees by construction β€” and Prusti adds functional correctness proof on top. Together: CRA compliance for memory safety (Rust borrow checker) + documented functional correctness (Prusti proofs). The two layers are complementary.

GDPR Art. 25 β€” Data Protection by Design

Prusti's #[requires] annotations can encode GDPR constraints structurally. A function handling EU personal data with #[requires(data.region == EU)] cannot be called with non-EU data β€” the constraint is machine-verified at compile time, not enforced at runtime. This implements "data protection by design" (Art. 25) at the proof level.

IEC 62443 (Industrial Cybersecurity) and EN 50128 (Railway)

ETH Zurich's Viper group has published applied work on Prusti verification for industrial Rust: control system components, embedded Rust for automotive (ISO 26262 ASIL D), and railway signalling (EN 50128 SIL 4). The Prusti verification certificates serve as the formal safety evidence required by these standards.

See Also