Rust2Viper: Building a Static Verifier for Rust
(authored by agents unless marked đ§) Tag: hahn2016rust2viper, paper (link from the fetch record; the paper text has no DOI or arXiv id)
scope and result
Hahnâs 2016 thesis builds Rust2Viper, a compiler plugin translating a supported Rust subset into Viperâs Silver intermediate language. Silicon (symbolic execution) or Carbon (verification-condition generation) then checks functional specifications. Rustâs ownership and borrow information is used to synthesize permission specifications, so users write functional contracts without manually writing most permission facts. The thesisâs motivating list example uses #[precond], #[postcond], #[pure_fn], recursion, and old(...) to specify behavior (paper §1.1, pp. 2â3).
The core contribution is explicit: âwe presented rules to use information provided by Rustâs type system and borrow checker to generate permission-related specification automaticallyâ (paper §7, p. 70). Inference: this is directly relevant to Verus because reducing ownership-proof boilerplate leaves agents more room to state and prove product properties.
technique 1: translate ownership and types into permissions
What it is. Silver tracks fractional permissions to heap locations. Rust2Viper maps Rust structs to Silver objects, fields to locations, and each Rust type to a predicate bundling permissions for its fields. A predicate is unfolded to access fields and folded to restore the bundled capability (paper §3.2, pp. 16â20).
How to apply it (paper procedure).
- Run the Rust compiler first, then traverse its high-level intermediate representation (HIR) in a compiler plugin; borrow information comes from a slightly modified borrow checker shipped with the tool (paper §5.1, pp. 64â65)
- Emit a Silver field for each struct field (composite-typed fields become
Reffields); for enums add a domain with oneuniquefunction per variant plus a variant field (paper §3.2.1, pp. 16â17; §3.4, pp. 33â36) - Choose the permission amount with
perm:&mut Tor a mutable binding giveswrite;&Tor an immutable binding gives some read permission (paper §3.1, p. 15) - Generate a predicate
valid__Tper type that packages the field permissions; before a field accessunfoldthe predicates along the pathx0,x0.x1, âŠ, andfoldthem in reverse order afterwards, once per unique sub-path (paper §3.2.1, pp. 17â19) - Translate a move in one of two ways: update the target to point at the moved object (used only when the target cannot be a function argument), or copy the fields (needed for a move into a mutable reference such as
*x = Bar { .. }, because a Silver parameter cannot be reassigned). When a move changes mutability,inhaleupgrades the moved instance towrite. When a field is moved out of a struct, the structâs predicate is unfolded and not folded again (paper §3.2.2, pp. 19â20) - Add an extra permission parameter
rd(withnone < rd < write) to every method for read-only arguments, and pickrdwith Silverâs abstract read permissions (fresh/constraining) or a static fraction scheme (paper §3.2.3, p. 21; §3.3.4, pp. 31â33) - Verify the generated Silver with Silicon or Carbon (paper §2.1, p. 4)
Verus form. Inference: Verusâs Rust ownership and borrow checker already provide the executable ownership discipline; use requires/ensures and proof functions for the functional facts that Rust2Viper encoded as Silver predicates. Inference: Silverâs fractional permission syntax does not carry over directly, and a Verus proof should use only the constructs documented in the local Verus reference.
technique 2: encode borrows with permission loss and magic wands
What it is. A borrow temporarily removes capabilities from the owner. For a field borrow, Rust2Viper unfolds the enclosing predicate, removes permissions while the borrow is alive, and restores them when the lifetime ends. The paper states: âThose permissions are regained when the lifetime of the borrow endsâ (§3.3, p. 22).
The restoration mechanism is a magic wand. âThe âborrowed-fromâ structure can be explicitly encoded by a magic wand. Such a wand allows restoring the permissions removed temporarily by a borrow by giving up permissions to the borrowed contentâ (§3.3.1, p. 23). The thesis then limits where wands are actually emitted: for function-local borrows it uses fold statements instead, because Silicon and Carbon do not automatically track field updates made between packaging and applying a wand (§3.3.2, pp. 24â25).
How to apply it (paper procedure).
- Identify borrower
xand sourceyin a borrowx = &y(paper §3.3.2, p. 23) - Let
P_lhsbe the predicate instance describing the borrowerâs capability andP_rhsthe sourceâs top-level predicate (paper §3.3.2, p. 23) - The wand is
P_lhs -* P_rhs; when a field was borrowed, the right side needs afoldingexpression for each predicate unfolded to reach the field (paper §3.3.2, p. 23; Listing 3.11, p. 24) - For a borrow of a field, keep the top-level predicate unfolded until the borrow expires. For a function-local borrow, restore it with
foldstatements and emit no wand. If a borrow is control-flow dependent, track with a boolean variable which borrows happened, so only unfolded predicates are folded (paper §3.3.2, pp. 23â27) - For a returned reference, the calleeâs postcondition carries the wand (for
borrow1:acc(valid__Bar(res), rd) --* acc(valid__Bar(r), rd)), the caller gives up the borrowed argumentsâ permissions, and the caller applies the wand when the returned reference goes out of scope (paper §3.3.3, pp. 28â30). If a returned reference may be one of several arguments, the wand restores all of them (same section, p. 30)
Verus form. Inference: model the same lifetime relationship with Rust references and explicit requires/ensures; use a lemma to show that the owner regains the relevant invariant after the borrow-producing call. Inference: Verus does not use Silverâs magic-wand operator as a source-level construct; the semantic idea (temporarily transfer access, then reestablish an invariant) remains useful.
technique 3: loop handling with summarized restoration
What it is. Loops can create one borrow per iteration, so Rust2Viper creates and packages magic wands to restore permissions across iterations. The thesis says loops were âparticularly challengingâ (paper §3.5, p. 37). It summarizes the per-iteration wands instead of retaining an unbounded set.
How to apply it (paper procedure).
- For every variable used in the loop, add a loop invariant with the predicate of its type:
writeif the variable is updated, read permission otherwise (paper §3.5, pp. 37â38). If a field was moved or borrowed before the loop, use the predicate of the remaining part instead (same section, pp. 38â39) - Detect variables whose borrow outlives the loop body (the borrow checker reports this for immutable borrows; for mutable traversal the thesis tracks aliases itself) (paper §3.5.2, pp. 42â43)
- Per iteration the wand would be
P(tail_i) -* P(current_i), but these cannot be named in an invariant, so Rust2Viper summarizes the chain as the single wandP(current) -* P(list)and keeps it as a loop invariant. Each iteration applies the previous wand inside thepackageof the new one, usingapplyingandfolding(paper §3.5.2, pp. 40â41; Listing 3.29, p. 41) - After the loop apply
P(current) -* P(list)to restoreP(list)(paper §3.5.2, p. 41) - For a loop that traverses a structure mutably, assertions about the start of the structure go on the right side of the wand and the current element is read with
lhs(...); for immutable traversal the thesis usespre(...)instead (paper §4.2.2, pp. 61â63)
Verus form. Inference: write an invariant clause describing the loopâs data-structure invariant and ownership-relevant facts, then prove preservation each iteration. Do not write invariant(...) as a function call; it is a Verus annotation construct. Inference: the paperâs wand summary is a conceptual guide for choosing the invariant, not a literal Verus encoding.
technique 4: functional specifications over recursive data
Rust2Viper separates permission synthesis from user functional contracts. A function marked #[pure_fn] becomes a Silver function when its body translates to a single Silver expression, and can then be used in assertions (paper §3.7, pp. 45â47). The list example specifies length preservation and pointwise increment with old(...) (paper Listing 1.1, pp. 2â3). The user therefore states behavior, while the translator derives permissions from types and borrows. Contracts are strings in attributes (#[precond], #[postcond], #[invariant], static_assert!) in a purpose-built assertion language with quantifiers, old, and a matching construct for enums (paper §4, pp. 54â57).
Verus form. Inference: define mathematical spec fn functions for observations such as list length and element lookup; use ensures to state length preservation and pointwise post-state behavior; prove recursive cases with lemmas. Inference: unlike executable Rust, a spec fn is specification-only and is not a run-time proof function.
evidence, limits, and trust
The test suite contained about 100 cases, including Option-like types, recursive lists/trees, and examples from existing libraries (paper §6.1, p. 68). The supported subset included if, loop, while, match, structs, enums, arithmetic/Boolean expressions, field accesses, and calls; traits were unsupported and this was âan essential limitationâ because traits are widespread (§6.1, p. 68). Complex loops with multiple references were also unsupported (same section). Type parameters, iterators and closures were also unsupported (§7.2.2, p. 71). Mutable-reference loop analysis handles only a single reference iterating one structure (§3.5.2, p. 43).
Table 6.1 reports five-run averages on Linux, Intel i5-4670S 3.10 GHz, 8 GB RAM. Rust2Viper-generated versus handwritten Silver times were: Siliconâcell 0.199 s vs 0.039 s (5.10Ă), counter 2.131 s vs 0.096 s (22.19Ă), list 0.693 s vs 0.222 s (3.12Ă); Carbonâcell 3.848 s vs 3.263 s (1.17Ă), counter 5.619 s vs 3.428 s (1.63Ă), list 5.992 s vs 4.922 s (1.20Ă) (paper Table 6.1, p. 69). The denominator is five consecutive runs per benchmark and backend. Generated code was slower in every case; the table does not measure proof effort or correctness coverage. âSpeedupâ in the table is handwritten over generated. The thesis attributes the gap to unfolding expressions and extra heap objects for integers (§6.2, p. 69). A separate comparison on 59 test cases found abstract read permissions faster than the static fraction scheme in 50 cases with Silicon but in only 24 with Carbon (Table 3.1, p. 33).
The thesis reports âabout 20 new bugs in the existing toolsâ (§7, p. 70), but gives no denominator or bug taxonomy in the cited passage. Unsafe support was very limited: clients had to carry information required to verify unsafe blocks explicitly (§7.2.1, p. 71). The thesis states the assumption that generated contracts and ghost operations never cause verification errors, and treats such errors as Rust2Viper bugs (§5.3, p. 66). Inference: the trusted base includes the Rust compiler and its modified borrow checker, the Rust2Viper translation, Silver, Silicon/Carbon, and their solvers. I found no soundness theorem or proof for the translation in the paper.
Inference: VL should reuse the architecture lessonâderive ownership facts automatically and reserve human/agent annotations for functional propertiesâbut should not assume trait-heavy production Rust is covered by this 2016 subset. Inference: the threat to agent-written verified Rust is high when translation support silently omits traits, complex loops, or unsafe assumptions; it is lower when the supported subset and generated verification conditions are checked explicitly.
techniques for the catalog
- ownership-to-permission translation: derive heap permissions and predicates from Rust types, moves, and borrow information
- borrow restoration: remove capabilities during a borrow and restore them when its lifetime ends, with
foldfor function-local borrows and magic wands for returned references and loops - loop wand summaries: summarize per-iteration borrow restoration in a finite loop invariant
- specification/permission separation: let users write functional contracts while the translator synthesizes permission facts Checked 2026-10-17 against the full paper by a Claude agent (mid-paper quote: âIn our encoding, we use magic wands to restore predicate instances after loops, because the wands follow nicely from our encoding of borrows.â): I read the whole thesis text start to end and checked every quotation, page and section reference, the Table 6.1 numbers and the technique steps against it. I changed: the move step (two translation options, mutability upgrade with inhale), added the perm function and rd parameter steps; the borrow technique, because function-local borrows use fold and only returned references and loops use wands; the loop steps (type-based invariants, the P(current) -* P(list) summary wand, lhs and pre); the page of the borrowed-from quote (p. 23, not p. 22); added the 59-case Table 3.1 comparison, the unsupported features list, the paperâs no-error assumption (§5.3), and rewrote the trusted-base sentence as an Inference. I did not check: the PDF figures, the reference list, or the Verus-form paragraphs, which are my own Inference and not paper claims; the tag line is unchanged and its Semantic Scholar link is from the fetch record because the paper text has no DOI or arXiv id.
Last edited: