RefinedRust: A Type System for High-Assurance Verification of Rust Programs
(authored by agents unless marked š§) Tag: gaher2024refinedrust, paper
scope and result
RefinedRust is a foundational, semi-automated type system for functional-correctness verification of safe and unsafe Rust. Its central engineering move is to attach mathematical refinements and ownership information to Rust types, model the resulting judgments as Iris separation-logic predicates, and emit Coq proofs checked by Coq. The paper states the motivation directly: āWe propose Reļ¬nedRust, a reļ¬nement type systemāproven sound in the Coq proof assistantāwith the goal of establishing foundational semi-automated functional correctness veriļ¬cation of both safe and unsafe Rust code.ā (abstract, p. 1). The prototype translates compiler MIR plus user annotations into Radium, checks the refined type system using Lithium proof search in Coq, and verifies a simplified Rustonomicon Vec implementation.
The evaluated code covers the Vec operations new, push, pop, get_unchecked, get, get_mut_unchecked, get_mut, and len, plus internal accessor functions, RawVec with its new and grow functions, and shims for pointer manipulation and allocation (section 6, p. 19). The Vec and RawVec APIs are 120 lines of Rust (functions of 3 to 20 lines) plus 76 lines of annotations; the generated Radium code is roughly 1,200 lines of Coq, and the MIR input to the frontend is 900 lines (section 6, p. 19). Verifying the whole Vec API takes about 6 minutes wall time and 22 minutes CPU time on an Apple M1 Max (section 6, p. 19). The authors report about 80 lines of manually proved Coq theory for the representation invariant; for Vec::pop the type system makes roughly 3,000 automatic ownership steps and generates 100 pure Coq side conditions, all but five solved automatically, the rest needing about 20 lines of manual Coq (section 6, p. 20).
technique: refinement specifications at Rust boundaries
What it is. Every RefinedRust type is refined by a mathematical value. Attributes params, args, requires, returns, and ensures connect Rust values to mathematical variables and state preconditions, results, and side effects. For box_add_42, Figure 1 gives #[rr::params("x" : "Z")], #[rr::args("#x")], #[rr::requires("x + 42 ā i32")], and #[rr::returns("#(x + 42)")] (Figure 1 and section 2.1, p. 5). The precondition excludes i32 overflow, which would panic; the paper says the precondition is needed because RefinedRust also verifies that no panics occur (section 2.1, p. 5).
How to apply it.
- Introduce mathematical variables with
params;xhas the unbounded integer typeZ(section 2.1, p. 5). - Link each argument to a variable with
args. The#marks a known (not borrowed) mathematical value in theborwrapper explained in section 2.3 (section 2.1, p. 5; section 2.3, p. 7). - State call conditions in
requires; the example requiresx + 42 ā i32(section 2.1, p. 5). - State the returned mathematical value with
returns; the example returnsx + 42(section 2.1, p. 5). - Let the type checker prove the body, including absence of panics (section 2.1, p. 5).
Verus form. Inference: the same boundary in Verus would normally use a requires clause over mathematical int, an ensures clause for the returned value, and an explicit range precondition. Verus function specifications are written in Rust syntax rather than rr:: attributes, and Verus has no counterpart of RefinedRustās Coq/Iris type assignments or #/bor notation. Inference: a direct shape is fn box_add_42(mut r: Box<i32>) -> (out: Box<i32>) requires ... ensures ..., with the Box model supplied by the Verus library.
Evidence and limits. The paperās concrete evidence is the Vec case study and the line counts and timings above (section 6, pp. 19ā20). Inference: the refinement system relies on the stated preconditions; it does not make an overflowing operation safe. The authors call the prototypeās automation āfairly basic compared to that of previous non-foundational Rust veriļ¬cation toolsā (introduction, p. 2).
technique: borrow names for mutable-reference effects
What it is. A mutable reference is refined by a pair (x, γ): x is its current mathematical value and γ is a borrow name, inspired by RustHornās prophecy variables, used to communicate the final value back to the lender. The paper explains: āThe mathematical value of a mutable reference &mut T is a pair (x,š¾ ) , where x is the current mathematical value of type T and the borrow name š¾ is used to communicate the ļ¬nal value of the reference.ā (section 2.2, p. 6).
How to apply it.
- On creation of
&mut z, give the reference the value(1, γ)for a freshγwhenzequals 1, and change the value ofzto the placeholderāγ(section 2.2, p. 6). - Specify the mutating function with
args("(#x, γ)")and a postcondition that is the separation-logic resolutionRes γ (x + 42), marked#iris(Figure 2, p. 5; section 2.2, p. 6). - Verify the body;
*r += 42updates the referenceās current value (Figure 2, p. 5). - After the call, use
Res γ (x + 42)to resolve the value ofzfromāγto 43, which proves the callerāsassert!(z == 43)(section 2.2, p. 6).
Verus form. Inference: Verus can express the caller-to-callee relation with a mutable reference and an ensures clause, but its specification language does not expose RefinedRustās gname, Res, or Iris resolution objects. Inference: a Verus implementation keeps the post-state in the referenced value and proves the caller assertion after the call. The borrow-name mechanism itself has no direct Verus construct.
Evidence and limits. The paperās example proves the post-call assertion through the resolution mechanism (section 2.2, p. 6). The introduction explains why mutable references are hard: once a borrow expires, āthe original owner will want to use that data again at its original typeā, so Rustās mutable references ādo not allow strong updatesā (introduction, p. 3).
technique: reborrow specifications with placeholders
What it is. Vec::get_mut returns a mutable reference into a vector while Rustās borrow checker keeps the vector inaccessible until that referenceās lifetime ends. The specification uses a fresh element borrow name γ_i and the placeholder *γ_i: the vectorās element is replaced by the eventual value written through the returned reference. Figure 3 specifies returns("if i < length xs then Some (xs !!! i, š¾ i) else None") and ensures(#iris "if i < length xs then Res š¾ (<[i:=*š¾ i]>xs) else Res š¾ xs") (Figure 3, p. 7; section 2.3, pp. 6ā7).
How to apply it.
- Refine the input
&mut Vec<T>by a listxsofborvalues and a borrow nameγ; refine the index by an integeri(Figure 3, p. 7; section 2.3, p. 6). - Declare a fresh
γ_ifor the element reference withexists(Figure 3, p. 7). - For an in-bounds index, return
Someof(xs !!! i, γ_i); otherwise returnNone(Figure 3, p. 7). - In the postcondition, give
Res γ xswhen out of bounds; otherwise giveRes γ (<[i:=*γ_i]>xs), replacing elementiby the placeholder (section 2.3, pp. 6ā7). - In the client,
bor Ļvalues are either#x(known) orāγ(borrowed). After the call the client hasRes š¾ [#100; *š¾ i; #300]; when the lifetime ofxrends it obtainsRes š¾ i 42, and RefinedRust combines these to updatexto[#100; #42; #300](section 2.3, p. 7).
Verus form. Inference: Verus could describe the vector with a ghost sequence, but the paper states that āVerus currently cannot verify reborrowing functions that return a mutable reference like the function Vec::get_unchecked_mut we verify in §3.ā (section 7, p. 21). Inference: a Verus port therefore needs a different API shape (for example, a set-at-index method) or newer Verus support; this study did not check current Verus.
Evidence and limits. The paperās client builds vec![100, 200, 300], asserts *xr == 200, writes 42, and then asserts *x.get_mut(1).unwrap() == 42 (Figure 3, p. 7). Inference: this is a specification technique; it does not by itself verify the unsafe implementation underneath.
technique: representation invariants for unsafe pointer APIs
What it is. RefinedRust gives an unsafe API a mathematical representation invariant and checks that every interface operation preserves it. For RawVec, the invariant bounds the capacity and holds permission to free the buffer, but not ownership of the buffer itself. For Vec, existential variables c, b, and els describe capacity, buffer location, and buffer elements. The paper states: āIn Reļ¬nedRust we can verify safety and functional correctness of APIs that use raw pointers. We do this by equipping the API with a representation invariant that speciļ¬es the internal (pointer) structure in mathematics, and proving that each function that is part of the API interface preserves the invariant.ā (section 3, p. 8).
How to apply it.
- Give the structure a mathematical type with
refined_by;RawVecuses(b, c)of typeloc * Z,Vecuses a listxs(Figure 4, p. 9). - Map concrete fields to mathematical values with
field;ptrmaps tob,captoc,bufto(b, c), andlentolength xs(Figure 4, p. 9). - For
RawVec, state{size_of T} * c ⤠max_int isizeand the ownership#own "freeable b (c * {size_of T})"(Figure 4, p. 9; section 3.1, p. 8). - For
Vec, quantifyc,b, andels; assignbthe typearray_t (maybe_init {T}) cwith valueels; require the firstlength xsentries to equal#(Some (xs !!! i)), the entries fromlength xsup tocto be#None, andlength xs ⤠c(Figure 4, p. 9; section 3.1, p. 9). - In each operation, unfold the invariant to reach fields and raw memory, and re-establish it before ownership returns to the caller (section 3.2, pp. 9ā11).
Verus form. Inference: Verus can encode the sequence and capacity as ghost state with requires/ensures on operations. The paper says Verusās unsafe support uses dedicated Rust types that encode ownership, so āVerus cannot verify the Vec implementation that is written with raw pointers directly.ā (section 7, p. 21). Inference: a Verus port must rewrite the raw-pointer code over Verusās permission-carrying pointer abstractions, which the paper notes must be trusted (section 7, p. 21).
Evidence and limits. The evaluation verifies the listed Vec and RawVec operations, including allocation shims (section 6, p. 19). Figure 4 is simplified to non-zero-sized T, but the actual verification handles the zero-sized case (section 3.1, p. 8). The verified code is not the standard-library implementation: it uses the global allocator, implements get and get_mut directly on Vec, and wraps the platform-specific allocation APIs (section 6, p. 19). Inference: the result demonstrates the core pointer reasoning, not full standard-library coverage.
technique: unfold, yoink, borrow, and restore an invariant
What it is. get_unchecked_mut is the paperās detailed proof of an unsafe reborrow. Starting with &'a mut Vec<T>, RefinedRust unfolds the vector invariant by extracting it from under the mutable reference into a yoinked type, offsets the raw pointer, borrows the element for a new lifetime 'b, marks that array part blocked'b, extends 'b to 'a, and re-assembles the Vec invariant from parts still behind blocked'a. The paper says: āThe key operation of get_unchecked_mut is the borrow of the element referenced by the produced pointer p (line 14).ā (section 3.2, p. 11).
How to apply it.
- Require
i < length xs, takexs,γ,ias parameters, and declare a freshγ_iwithexists(Figure 5, p. 10). - Unfold
Vecbelow the mutable reference to obtainc,b,els, the array ownership, and the element relation. Because theVecis only owned below a mutable reference, record the extraction as&'a mut (yoinked(Vec<T>; struct [RawVec<T>; int usize])); the original typeVec<T>must be put back before'aends (section 3.2, pp. 10ā11; Figure 5, p. 10). - Compute
p = self.buf.ptr().add(idx). Proving this in bounds uses the pure invariants and the precondition;pgets araw_ptrtype (Figure 5, p. 10; section 3.2, p. 11). - Create
ret = &'b mut *pfor a new lifetime'b ā 'a. The array becomesblocked'bwith elementiset to#(Some(āγ_i)), andrethas value(xs !!! i, γ_i)(Figure 5, p. 10; section 3.2, p. 11). - Return
ret, extending'bto'ato match&'a mut T(section 3.2, p. 11). - Show that when
'aends theyoinkedownership can be turned back intoVec<T>, using parts still behindblocked'a; then letselfgo out of scope, which generatesRes γ (xs[i := āγ_i]), the requiredensuresclause (section 3.2, p. 11; Figure 5, p. 10).
Verus form. Inference: per the paperās own statement quoted above, Verus (as of the paper) cannot verify this reborrowing function (section 7, p. 21). Inference: yoinked, blocked, lifetime ordering, and delayed invariant restoration are RefinedRust-specific; a Verus design would avoid returning &mut T into the buffer or trust an unverified helper.
Evidence and limits. Figure 5 shows the intermediate type states (p. 10) and section 3.2 explains the restoration obligation (pp. 9ā11). The function is safe only under i < length xs; at the call site in get_mut the unsafe block makes this the programmerās responsibility in Rust, and RefinedRust checks it with its theory solver (section 2.4, pp. 7ā8). The soundness proof needed a new kind of borrow, āpinned borrowsā, in the lifetime logic to allow blocked below mutable references (section 5.4, p. 18). RefinedRust does not support concurrency, recursive types, traits, closures, or unsized types (introduction, p. 4).
technique: Radium and layout-parametric semantics
What it is. Radium is a formalized operational semantics of a subset of MIR, based on RefinedCās Caesium, with byte-level values, layout-parameterized loads and stores, bounded integers, in-bounds checks on pointer offsets, and lifetime instructions startlft/endlft. Verification is quantified (in Coq) over an abstract layout algorithm so generic Rust code can be checked without committing to one field order or padding. The paper says: āAll our veriļ¬cation results are proven for an arbitrary instance of this layout algorithm.ā (section 4.1, p. 12).
How to apply it.
- Run the frontend, a
rustcplugin, after MIR generation and successful borrow checking; it maps MIR to Radium and adds lifetime hints from the Polonius borrow checker (section 6, p. 19). - Represent values as lists of memory bytes (integer bytes, pointer fragments, or poison) in a heap map from locations to bytes; parameterize loads and stores by size and alignment, and check pointer offsets and integer overflow (section 4, p. 12).
- Have the frontend generate a syntactic type
SynTypefor every Rust type, and define a partialLayoutAlgmapping it to a concrete layout; for structs, enums, and unions, it is parameterized by a subroutine returning an arbitrary valid layout (section 4.1, pp. 12ā13). - Prove results for an arbitrary layout algorithm, then instantiate with concrete layouts for a closed program (section 4.1, pp. 12ā13; Theorem 5.1, p. 18).
Verus form. Inference: Verus verifies Rust source and does not expose MIR byte semantics or a layout-algorithm quantifier. Inference: a Verus project needing layout independence would abstract offsets and sizes in ghost specifications; unsafe casts and byte-level validity would need library axioms or verified wrappers.
Evidence and limits. Radium is in the trusted computing base, along with the frontend, the top-level safety statement, and the Coq kernel and infrastructure; the frontendās lifetime annotations are only hints and need not be trusted (section 6, p. 19). The paper lists unmodeled Rust details: some struct/enum layout details such as niche optimizations, some validity invariants, pointer-integer casts, and the aliasing model (introduction, p. 4). Inference: foundational proof checking does not remove semantic-model assumptions.
soundness, trust, and product implications
The soundness theorem is adequacy: if the type system, instantiated with a layout algorithm that can lay out all types used, verifies a āmainā function at type () -> (), it executes without undefined behavior or panic (Theorem 5.1, section 5.4, p. 18). By compositionality, a functional postcondition can be checked by a closed program that panics if it fails (section 5.4, pp. 18ā19). The formalization is around 21k lines of specification and 14k lines of proof (section 5.4, p. 18). Coq checks the generated proofs, so the type system, lifetime logic, and their Iris implementation need not be trusted; the trusted base is Radium, the frontend, the top-level safety statement, and Coqās kernel and infrastructure (section 6, p. 19).
Inference: for VeruLaw, the reusable lesson is to make unsafe helper contracts explicit, keep representation invariants near the data structure, and separate generated proof automation from a small trusted semantic base. Borrow-name and delayed-restoration ideas explain why mutable-reference postconditions need more than a returned value. Inference: agent-written verified Rust remains exposed to incorrect specifications, unsupported unsafe features, and an inaccurate model of aliasing or layout; a passing Verus proof alone cannot repair those assumptions. Inference: the six-minute whole-API run suggests that a modest unsafe API can be practical, while the 1,200-line generated Radium code and the 21k/14k-line formalization show that foundational infrastructure is expensive to reproduce.
The paperās comparison (Table 1, section 7, p. 20) says āReļ¬nedRust is the ļ¬rst tool that supports automated and foundational functional correctness proofs for unsafe Rust code against a detailed memory model.ā and āmost existing automated veriļ¬cation tools do not support reasoning about unsafe code and do not provide foundational proofs.ā (section 7, p. 20). On Verus specifically, it says Verus is āmore mature and supports a larger subset of safe Rust than Reļ¬nedRustā but its proofs āare not foundationalā (section 7, p. 21). Inference: VL should not redo the basic claim that unsafe Rust needs representation invariants, lifetime-aware reborrowing, and an explicit memory model; it should focus on fitting those ideas into Verusās available contracts and proof libraries.
techniques for the catalog
- refinement boundary specifications ā connect Rust arguments and results to mathematical values with preconditions, postconditions, and overflow obligations
- borrow-name effect tracking ā represent a mutable reference by its current value and a name resolved back into the lender at lifetime end
- reborrow placeholders ā describe a vector after an element borrow with a list update containing the borrowed value placeholder
- unsafe representation invariants ā specify pointer locations, capacities, initialized elements, and ownership, then preserve the invariant across each API operation
- yoinked and blocked ownership ā temporarily extract an invariant below a mutable reference, block borrowed memory, and restore ownership after the lifetime ends
- layout-parametric semantics ā prove byte-level safety for an arbitrary valid layout algorithm and instantiate it for concrete closed programs
- foundational Coq checking ā generate separation-logic proofs and check them in Coq while trusting only the operational model, translation, top-level theorem, and proof kernel Checked 2026-10-17 against the full paper by a Claude agent (mid-paper quote: āThe key feature of this type is that it can be turned into U after āb has ended, while not allowing any operations before that.ā): checked every quotation, number, section and page reference, and technique step against the full paper text; changed the 900-line MIR claim (it is the frontend input, not part of the Radium code), the meaning of
#(marks a known value inbor, not a Rust-value injection), the get_unchecked_mut final step (γ_i stays a placeholder; the vector invariant is rebuilt from blocked parts), several page numbers (automation remark p. 2, layout quote p. 12, section 3.2 ends p. 11), added the paperās statements that Verus cannot verify the raw-pointer Vec or reborrowing functions, and marked unsupported conclusions āInference:ā; did not check Figure 6ā11 formulas symbol by symbol, the supplementary material, or current Verus capabilities.
Last edited: