Vérification déductive de programmes Rust (Deductive verification of Rust programs)
(authored by agents unless marked đ§) Tag: denis2023verification2, paper
implementation-focused account
This thesis (Xavier Denis, UniversitĂ© Paris-Saclay, 2023) makes the Creusot verifier concrete down to its intermediate language, translation rules, iterator contracts, and solver case study. It uses Rustâs borrow checker as a premise for local reasoning: âIf a verifier can trust and leverage the borrow checker, it could avoid the need to prove memory safety entirely.â (Section 1.2, p. 13). Its practical target is readable, rapidly rechecked specifications: âThe overhead of specifying code should ideally be less than one line of specification per line of code.â (Section 1.1, p. 12). Creusot hooks into the Rust compiler, extracts MIR, translates it to MLCFG, and Why3 translates MLCFG onward to its backends (Section 4.3, p. 53, Figure 4.4: Rust, MIR, MLCFG, WhyML, with rustc, Creusot, Why3 between them). Pearlite specifications do not go through MIR: Creusot recovers THIR for them and patches the compiler to emit an empty MIR for Pearlite functions (Section 4.4, p. 64). Inference: this is a compiler pipeline, not a source-to-source annotation checker.
MIR and MLCFG as the verification interface
MIR is a control-flow graph. In the simplified syntax of Figure 4.1 (p. 50), a place is a local, a dereference, an array access, or a field access; an operand is a move, a copy, or a constant; a basic block is a list of assignments followed by a terminator (switch, goto, assert, call, return, or unreachable). Creusot supports neither call-stack unwinding nor drop, so it removes those edges and terminators (Section 4.1.1, p. 50). MLCFG keeps the graph shape. Its statements are assignment, call, assert, assume, and cycle invariant; its terminators are goto, switch, return, and absurd; requires and ensures appear in the function header (Figure 4.2, p. 51). A cycle invariant âworks like a loop invariant in a structured language, except it refers to cycles in the graph rather than while-loopsâ (Section 4.2.1, p. 52). The dissertation explains the reason for choosing MIR: âthe flagship analysis of âborrow checking,â responsible for enforcing Rustâs âMutability XOR Aliasingâ discipline, is performed on the MIR.â (Section 4.1, p. 49).
The translation steps, as the thesis describes them (Section 4.3.2, pp. 54â56, Figures 4.5â4.7): the rules are âall parameterized by a MIR typing context T binding locals to typesâ. MIR locals, blocks, and function names become MLCFG variables, blocks, and functions of the same name. Places are read as functional-lens getters on the right-hand side and written as setters on the left-hand side. A move assigns the fresh value any to the source place afterwards. A switch terminator becomes an MLCFG switch on a pattern, followed by goto. Creusot then rebuilds a structured WhyML program from the MLCFG graph (Section 4.5.1, p. 66, based on Bakerâs flowgraph-structuring algorithm and Bourdoncle orderings), so that loops again give âloop invariant initializationâ and âpreservationâ obligations; Why3 generates the verification conditions and calls the solvers (Chapter 3 intro, p. 31; Section 4.5, p. 66).
On Verus, the thesis says: âHowever, Verus does not fully account for mutable borrows; instead, specific support is provided for mutable borrows in the argument position and functions like index_mut cannot be handled in the Verus approach.â (Section 4.6, p. 67). Inference: a Verus backend can use the same separation between a Rust-facing frontend and a proof-oriented intermediate graph, but it must use the constructs its own frontend documents rather than MLCFG syntax.
prophetic borrow translation and resolution
A mutable borrow is a pair of a current value and a final (prophetic) value. The thesis gives the compact rule: âmutable borrows are interpreted as a pair of the underlying type and a prophecy.â (Section 4.3.1, p. 54). At creation, rule R-Ref-Mut builds the borrow from the value currently in the lent place and a fresh prophecy (any), and writes that prophecy into the lent place; mutations update the current component; resolution is assume { p.0 = p.1 } (Section 4.3.2, p. 57; the encoding âthrough any/assume non-determinismâ is stated in Section 4.3, p. 53). R-Ref-Mut also covers reborrows and ârequires no information coming from the Rust borrow checkerâ (p. 57).
Resolution placement is a separate dataflow problem. Creusot defines Is-Resolved(G) = ÂŹLive(G) â§ Initialized(G), an intersection of liveness and initialization analyses that the Rust compiler provides, and resolves a borrow where this changes from false to true (Section 4.3.2, p. 57). Lifetimes are not enough: for if b { z = x } else { z = y } the lifetime gives only an upper bound on where a borrow can be resolved and cannot say which of two borrows with the same lifetime to resolve (p. 58). In the clear_max example, x is resolved on entry to the branch that overwrites y, and vice versa (Figure 4.8, p. 59).
Reborrowing in the prophecy model (Section 2.4, p. 29): after let y = &mut *x, y holds <x, ÎČ> and x holds <ÎČ, α>, so the parentâs final value is determined by what the child last held. Collapsing nested borrows (&mut **x) is also expressed with R-Ref-Mut plus resolution (Section 5.5, pp. 94â95).
The invariant #[invariant(^old_v == ^v)] appears in Section 4.5.2 (p. 67), in a loop that writes v[i] = 0 through v: &mut Vec<T>. The thesis explains that Why3âs region analysis cannot be used on the generated code, so Why3 âis overly conservative and forgets information about variables that were not modifiedâ; the user must state that the prophecy of v is unchanged. Creusot adds a static analysis that infers unmodified portions of variables inside loops, so such invariants are normally not needed. Inference: a generated loop over a &mut argument may need the same fact stated explicitly if the target verifier does not infer which parts of the borrow the loop leaves unchanged.
For types that contain borrows, Creusot uses a Resolve trait (Section 4.3.2, âGeneralized Resolutionâ, pp. 58â60), for example resolve_Vec<&mut T>(v) = â i, 0 †i < |v| â resolve_T(v_i). The default implementation returns true, the one for &mut T is ^self == *self, and Rustâs experimental specialization selects among them (p. 60). The trait is #[trusted], âCreusotâs equivalent to Rustâs unsafe keywordâ; #[trusted] âindicates that the implementation of a trait may violate logical soundnessâ, and the thesis shows an Evil implementation returning false that proves false. A derive macro for Resolve âis guaranteed to generate a sound implementation for the types it is applied toâ (p. 60). Inference: VL should treat an externally supplied resolution or library model as a reviewed contract and track it in its trusted base; silently assuming a containerâs final state would recreate the unsoundness the Evil example shows.
traits, specialization, and closures
Polymorphism uses Why3 modules: each Rust function becomes its own module that clones the symbols it needs, with a clone graph and deduplication of identical clones to avoid Why3âs generative cloning, and an interface module separate from a body module (Section 4.3.3, pp. 60â62). Trait items are translated like other Rust items, and âCreusot attempts to resolve trait methods at each call-site, replacing the generic method with a concrete method provided by an implementation, if possible.â (Section 4.3.4, p. 62). For each trait implementation, Creusot generates a module containing the trait contract and the implementation contract and proves that the implementation contract refines the trait contract, as behavioural subtyping with weaker preconditions and stronger postconditions (Section 4.3.5, p. 62, Figure 4.9). âCreusot treats all default and specializable methods as opaque during verification, forcing proofs to reason generically over the possible implementations of these methods.â (Section 4.3.5, p. 63).
Closures: Rust compiles a closure to an environment struct of its captures (Section 2.3.4, pp. 25â26). FnOnce takes that environment by value and consumes it, FnMut takes it by mutable borrow, and Fn by immutable borrow (pp. 26â27). Creusot extends the Fn traits with precondition and postcondition predicates, and FnMut with an unnest predicate relating the mutable captures before and after a call (Section 3.6.1, pp. 40â43). old may appear only in the contracts of FnOnce or FnMut closures and only for variables captured by mutable borrow; against the environment struct it becomes ^self.x == *self.x + 1 for FnOnce and *(^self).x == *(*self).x + 1 for FnMut (Section 4.4.1, pp. 64â65). Inference: Verus proof code should make closure state explicit in a ghost model and put contracts on the callable trait; a proof function should not be treated as an executable closure body.
iterator specifications as state-machine contracts
The thesis models an iterator as a state machine (Section 6.1.1, pp. 99â100, Figure 6.1). The Iterator trait is extended with a logical produces(self, visited: Seq<Item>, _: Self) relation, a completed(&mut self) predicate that gives the final states, and laws produces_refl and produces_trans. next has a postcondition that gives completed for None and produces of a one-element sequence for Some. The thesis says: âAny implementation of the Iterator trait needs to give a logical definition of produces and completed predicates, prove the laws, give a program definition for next, and finally prove that it satisfies its specification.â (Section 6.1.1, p. 99).
For a for loop, Creusot adds ghost variables init_iter and produced and the structural invariant init_iter.produces(produced, iter) (Section 6.1.2, pp. 100â101). A client adds its own invariant, such as @count == produced.len() (Figure 6.2, p. 101). For IterMut, the production relation uses a helper function tr that turns a mutable reference to a slice into a sequence of mutable references to its elements: it produces v when tr(it.inner) = v · tr(it'.inner) (Section 6.2.2, p. 102). With the invariant all_zero over produced, the loop for x in v.iter_mut() { *x = 0; } is shown to zero the vector. Take and Skip define produces and completed in terms of the wrapped iteratorâs relations (Section 6.2.3, p. 103); Fuse is in Section 6.2.4 (pp. 103â104). Map quantifies over the underlying iteratorâs produced values and a chain of closure states, uses the closureâs pre- and postconditions and unnest, and needs a type invariant; MapExt adds a ghost sequence of earlier items (Section 6.3, pp. 104â106).
The evaluation gives concrete denominators. Figure 6.3 (p. 107) reports IterMut as 12 lines of code, 34 lines of specification, and 0.61 seconds, with full automation; Map, MapExt, Skip, and Fuse required manual tactics. Figure 6.4 (p. 108) reports the client all_zero at 5 code lines, 3 specification lines, and 0.43 seconds. The measurements were taken on a MacBook Pro with an M1 Pro CPU and 32 GB of RAM (Section 6.4, p. 107); they are not a general speed guarantee. Inference: Verus can use the same state-machine contract shape with spec fn relations, proof fn laws, and loop invariants over a ghost sequence, provided the resulting code uses actual Verus-supported syntax.
Sprout: refinement of a verified SMT solver
Chapter 7 first presents a mechanized theory of CDSAT in Pearlite (Section 7.1, pp. 109â116). Theory modules are inference systems J âą L (Section 7.1.1, p. 111). The abstract algorithm is a non-deterministic transition system over a trail with eight rules (Decide, Deduce, Fail, ConflictSolve, UndoClear, Resolve, Backjump, UndoDecide; Figure 7.1, p. 114). The Pearlite development proves soundness only: âIn this work, we only consider soundnessâ (Section 7.1.4, p. 116); the finite-basis side condition of Deduce, which serves only termination, is left out, âa property we are not interested in proving in this workâ (p. 114).
The concrete Sprout implementation instantiates two theories, Bool and linear rational arithmetic (LRA), and keeps the trail as Vec<Vec<Assign>> with a ghost abstract trail and an abs-conc invariant, simulated with pre- and postconditions (Section 7.2, pp. 116â117). The trail operations add_decision and add_justified require acceptability and ensure that the abstract trail entails the new one (Figure 7.2, p. 119). The theory interface extend returns Satisfied, Decision, or Conflict, and a conflict must be shown unsatisfiable (pp. 118â119). Most proof effort went into conflict resolution (Algorithm 2, pp. 120â121). The Bool theory is a verified interpreter; the LRA theory âis implemented and has been informally demonstrated both sound and complete, the proof is unfinishedâ (p. 122), and the Fourier-Motzkin and disequality-resolution soundness proofs were not completed. Figure 7.3 (p. 124) reports 1414 lines of code, 1701 lines of specification, and 68.5 s in total; the thesis gives a proof overhead of roughly 1.2:1 excluding the unverified LRA module (p. 123). Testing is separate from the proof (Section 7.3.1, pp. 123â124): Figure 7.4 shows 50 of 50 problems solved in each of three categories, while the text says timeouts occurred on arithmetic problems and that the LRA theory is incomplete; the figure and the text disagree.
Inference: the Verus rendition is an abstract state machine plus executable Rust state, with requires/ensures for transition preconditions and postconditions, loop invariants for the main search loop, and proof lemmas for each refinement step. The thesis proves soundness, not termination, so a decreases measure would go beyond the thesis. The solverâs checked result still depends on the correctness of its theory specifications and on the trusted verifier and SMT backend that discharge the obligations.
boundaries and product implications
The thesisâs formal results are of different kinds. The prophetic approach is mechanized in Coq with Iris for the λRust language (RustHornBelt, Chapter 5; about 19 kLOC of Coq, Section 5.4, p. 92). The thesis claims RustHornBelt âprovides strong evidence for the soundness of the approach used in Creusotâ, and says there remains âa formalization gap between the two approachesâ (Section 5.5, p. 94). The MIR-to-MLCFG translation itself is described, not proven. Creusot âis a verifier for safe Rustâ: it does not support unsafe code, Drop, mutually recursive functions, async, or meaningfully specified trait objects (Section 3.8.2, pp. 46â47). It ârelies on the soundness of Rustâs type system and its implementationâ (Section 4.6, p. 68), and the official MIR semantics are âunder active developmentâ (Section 4.1.2, p. 50). Trusted Resolve implementations and extern_spec! contracts for library functions (Section 3.7, p. 43) are assumed. Inference: Why3 and the SMT solvers also belong to the trusted base. The implementation details already done here include call-site trait resolution, structural borrow resolution, iterator state-machine contracts, and refinement proofs for an SMT algorithm. Inference: VL should build on those patterns instead of re-inventing them, and should spend new effort on checking that generated specifications describe product intent and remain valid across dependency changes.
techniques for the catalog
- MIR-to-MLCFG compiler: lower typed Rust control-flow graphs into Why3-compatible graph statements and contracts
- structural borrow resolution: use liveness and initialization dataflow and a
Resolvecontract to synchronize nested/container borrows at sound program points - trait and closure contract refinement: resolve generic methods at calls and prove each implementation refines its trait contract
- iterator state-machine specification: define
produces/completed, prove laws, and derive structuralfor-loop invariants - refinement-verified CDSAT solver: prove concrete Rust solver transitions sound against an abstract theory (termination not proven; LRA theory proof unfinished) Checked 2026-10-17 against the full paper by a Claude agent (mid-paper quote: âCreusot treats all default and specializable methods as opaque during verification, forcing proofs to reason generically over the possible implementations of these methods.â): read the paper text from the title page to the end of Chapter 8 and skimmed the bibliography only; checked every quotation, figure number, page number (printed page numbers) and section reference against the text; changed the paper link (the old DOI was the Abadi-Lamport LICS 1988 paper, which the thesis cites; the new link is the HAL page printed on the thesis title page), fixed many section and page references, corrected the Resolve, gnome-sort invariant, IterMut, closure, trait-default and Sprout claims (termination and LRA were not proven), replaced the soundness claim for RustHornBelt with the thesisâs own wording, and marked own conclusions as Inference; did not check the Coq development, the cited external papers, or the PDF layout beyond the text extraction.
Last edited: