Modular Formal Verification of Rust Programs with Unsafe Blocks
(authored by agents unless marked đ§)
Tag: rahimiforoushaani2022modular, paper
scope and claim
This December 2022 progress report proposes verifying Rust unsafe blocks and safe abstractions with VeriFastâs Modular Symbolic Execution (MSE), using RustBeltâs semantic model for Rust types. The abstract states, âTo address this problem we are trying to verify soundness of Rust unsafe code applying our Modular Symbolic Execution algorithm.â (Abstract, p. 1.) The target is memory safety and conformance of a safe abstraction to the guarantees in its Rust type, assuming the input has passed Rustâs type and borrow checker.
The paper focuses initially on raw-pointer dereferences, not every unsafe superpower. Its claim is a plan plus a prototype, not a completed soundness theorem or a broad evaluation. The authors say, âThe MIR extraction program and the VeriFast extension for supporting Rust are works in progress and currently support a very limited subset of Rust.â (Section 5.2, p. 16.)
technique: contracts for unsafe functions
when to use
Use a contract when a raw-pointer function or safe wrapper must be checked once and then called without re-executing its body. The contract states a weakest precondition in requires and a strongest postcondition in ensures, including heap chunks, pointer validity, and returned ownership.
how the paper does it
- Start with a Rust function and the memory facts that must hold before it runs. For
create_deque,requires truemeans there is no caller-side heap prerequisite (Section 3.3, pp. 9â11). - Symbolically execute the body from the precondition. Allocation creates a symbolic pointer and branches on null versus non-null; writes consume and update heap chunks such as
Node_prevandNode_next(Sections 3.1â3.3, pp. 6â10). - At each return, check every
ensuresfact. The example contract requires a non-null result, an allocatedNode, and initializedprevandnextchunks (Listing 5, p. 10). - Transfer the chunks named by
ensuresto the caller. The verifier removes them from the callee heap, preventing the same access rights from being transferred twice (Section 3.3, p. 11). - At a call site, use only the saved contract: add the postcondition chunks and path facts to the caller state, without executing the callee body again (Section 3.3, p. 11).
The paper describes this modularity directly: âIf a human/verifier provides us with a function contract in a well-defined logic, we can check the contractâs propositions against the function body/implementation and if the body satisfies the contract, we can just reuse the contract every time we want to check a call.â (Section 3.3, p. 9.)
what transfers to Verus
Inference: write the same boundary as a Verus executable function with requires and ensures, using a predicate for allocated and initialized nodes. Use old(...) in ensures for values preserved from entry, and prove each raw-pointer operation through a verified wrapper or a modeled memory API. Verusâs contracts are source-level specifications rather than VeriFast heap chunks, so the memory representation and unsafe boundary must be supplied explicitly. This is an inference.
evidence and limits
The only concrete contract example is create_deque; no number of verified functions or success fraction is reported. The paperâs intended guarantee is conditional on successful MSE, prior Rust type/borrow checking, and sound RustBelt semantic encodings. It does not yet prove that its implementation is sound with respect to RustBelt.
technique: symbolic execution of heap chunks
when to use
Use symbolic execution when concrete execution would branch over infinitely many addresses or values. Replace concrete values with symbols while retaining path conditions and heap predicates that describe allocated bytes and initialized fields.
how the paper does it
- Represent machine state as a store plus a heap. The heap is a multiset of predicates such as
malloc_block_Node,Node_prev,Node_value, andNode_next(Section 3.1, pp. 6â8). - Execute allocation symbolically. One branch records a null result; the successful branch introduces a symbolic address
b_l, a non-null path fact, and chunks for the allocated node whose fields initially contain the special uninitialized value (Sections 3.1â3.2, pp. 7â9). - For a field write, require a matching heap chunk. A write to
previs safe only whenNode_prev(address, old)is present; update it toNode_prev(address, new)(Section 3.1, p. 8). - Treat a read of an uninitialized value as failure to verify. This models Rustâs invalid-value rule (Section 3.1, p. 7).
- Carry path conditions through branches and report a stuck state when a required chunk or fact cannot be proved (Sections 3.1â3.2, pp. 8â9).
The paper says, âThe execution using symbols and facts we know about them is called Symbolic Execution.â (Section 3.2, p. 9.)
what transfers to Verus
Inference: model the heap facts as a spec fn predicate over an abstract map from addresses to node records, and prove each branchâs non-null and initialization facts with assert. For real raw pointers, use a Verus-supported pointer abstraction or isolate the operation behind a trusted specification whose implementation is separately checked; do not treat an arbitrary raw dereference as verified merely because an address is non-null. This is an inference.
evidence and limits
The create_deque tree demonstrates two classes of paths: one allocation-failure path and infinitely many successful-address paths represented by one symbolic branch. There is no benchmark denominator, solver-time number, or soundness theorem for the proposed symbolic interpreter. The abstract machine is a model; matching it to Rustâs actual memory and provenance rules remains a proof obligation.
technique: modular symbolic execution
when to use
Use MSE for callers, loops, and recursion so that each function is verified against a contract once. This prevents path explosion from repeatedly expanding callee bodies.
how the paper does it
- Verify the callee body from its
requiresstate and establish itsensuresstate on every normal return. - Save the resulting contract as the functionâs weakest precondition and strongest postcondition.
- At each call, check the caller state against the precondition, then add the postcondition, without inspecting the callee internals.
- Use the same saved contract for recursive calls; the paper notes that direct symbolic execution may not terminate for loops and recursive functions (Section 3.3, p. 9).
- Preserve ownership by consuming heap chunks in the contract and returning them exactly once.
The authors define the goal as follows: âWe also want the contract to tell us as much as possible about the effects that calling the function has on the execution state.â (Section 3.3, p. 9.)
what transfers to Verus
Inference: use a Verus ensures clause as the reusable postcondition and a decreases clause for recursive termination. Loop invariants must be written explicitly with invariant clauses; a function contract alone does not automatically become a loop invariant. This is an inference.
evidence and limits
MSE is described algorithmically and illustrated with one allocator/deque example. The paper reports no corpus size or comparison against a baseline verifier. It also identifies higher-order functions, traits, and panics as difficult behaviors (Section 2.2, p. 5), so modular contracts may need additional modeling for those features.
technique: RustBelt semantic interpretation of safe abstractions
when to use
Use RustBeltâs semantic interpretation when the functionâs Rust type carries ownership, borrowing, lifetimes, Send/Sync, or thread-local sharing facts that raw-pointer contracts do not express. A safe abstraction must implement the semantic predicate associated with its public type.
how the paper does it
- Interpret each Rust type as a tuple containing size, ownership, and shared-reference predicates. RustBelt writes this as
JÏK = (size, own, shr)(Section 4.1, p. 13). - For a shared
Cellreference, open theownpredicate to obtain theshrpredicate forCell, then open that predicate to expose the borrowed content (Table 1, rows 1â3, p. 15). - Use the lifetime logic access rule
LftL-na-acc, supplying a fraction of the lifetime token and the current threadâs non-atomic token. This temporarily yields full access to the content (Section 4.1, pp. 14â15). - Perform the unsafe write through the obtained heap chunk, updating the stored integer (Table 1, rows 4â5, p. 15).
- Close the borrow and return the lifetime and thread tokens at function exit. Returning the tokens checks that every resource granted through the borrow was given back (Section 4.1, p. 15).
- Check the
Cellinvariant thatCellis notSync; aliases remain in one thread because the type cannot be sent to another thread and no public method leaks a deep reference (Section 4.1, pp. 13â15).
The semantic foundation is intentionally indirect. The paper quotes RustBeltâs three proof steps: âVerify that the typing rules of λRust are sound when interpreted semanticallyâ (Section 4, p. 12), âVerify that, if a closed program is semantically well-typed according to the model, its execution will not exhibit any unsafe/undefined behavioursâ (Section 4, p. 12), and âFor any library that employs unsafe code internally, verify that its implementation satisfies the predicate associated with the semantic interpretation of its interfaceâ (Section 4, p. 12).
what transfers to Verus
Inference: encode each safe wrapperâs public invariant as a Verus spec fn, carry a ghost lifetime or thread token in Tracked state, and prove that the unsafe implementation opens and closes the invariant without leaking the token. Verusâs state-machine or atomic APIs can model thread ownership, but the paperâs Iris lifetime fractions do not transfer automatically. This is an inference.
evidence and limits
The Cell::set walkthrough is a proof outline, not an implemented Rust result. The report explicitly says details are skipped and proposes either formalizing MSE over λRust or generating a function-specific Iris proof as future rigorous soundness work (Section 6.1, p. 18). Thus the semantic encoding is a trusted or provisional component in this report.
technique: compile Rust to MIR and reuse VeriFast
when to use
Use MIR as the symbolic-execution input when surface Rustâs syntax and missing formal operational semantics make direct reasoning difficult. Reuse VeriFastâs separation-logic engine instead of implementing a new solver.
how the paper does it
- Run the official Rust compiler front end to type-check and borrow-check the program and emit MIR plus type definitions and annotations (Section 5.1, p. 16).
- Serialize the extracted representation with Capân Proto so other analyzers can consume it.
- Translate MIR control-flow graphs into C control constructs in VeriFastâs abstract syntax tree. Translate simple types directly and represent complex Rust types with generated RustBelt predicates (Section 5.2, p. 16).
- Execute the translated AST with VeriFastâs symbolic executor and separation-logic ghost commands.
- For generic functions not yet handled, substitute instantiated implementations; the prototypeâs allocator example uses this workaround (Section 5.2, p. 16).
The paper says, âExtending VeriFast to support Rust, or more accurately to support MIR, spares us implementing the executing and reasoning engine from scratch.â (Section 5.2, p. 16.)
what transfers to Verus
Inference: a VeruLaw pipeline can similarly lower Rust to a stable intermediate representation for analysis, but Verus already verifies Rust-like source and does not use VeriFastâs C AST. The transferable lesson is to reuse the compilerâs checked representation and to keep generated proof obligations tied to source contracts. This is an inference.
evidence and limits
At the report date, the tool verifies one simple allocation/access/deallocation example. Generic definitions are ânot generally handled yet,â and the supported Rust subset is âvery limitedâ (Section 5.2, p. 16). No count of passing examples or false negatives is given. The claimed practical advantage over RustBelt is reduced learning burden for Rust developers and co-location of Rust code and annotations; this is a hypothesis, not a measured study (Section 5.3, p. 17).
technique: panic and stack-unwinding paths
when to use
Use explicit MIR unwind edges when an unsafe function can transiently create an invalid state before a call that may panic. A normal-return contract may not hold on an unwind path, and destructors run during unwinding.
how the paper does it
- Lower the function to MIR so a potentially panicking call has an explicit unwind successor.
- Symbolically execute both the normal successor and the unwind successor.
- Check that each path does not use an invalid temporary state and that destructors receive valid values.
- State separately which postcondition facts are required after a panic, because an unwind returns no ordinary value (Section 6.2, pp. 18â19).
The paper warns, âcode that transiently creates unsound states must be careful that a panic does not cause that state to be usedâ (Section 6.2, p. 19). Its BinaryHeap::sift_up example uses MaybeUninit and a potentially panicking comparison; MIR makes the jump to an unwind block explicit (Listings 10â11, pp. 19â20).
what transfers to Verus
Inference: model fallible operations as Result where possible and prove a separate invariant on every error branch. If panic behavior matters, expose an explicit unwind/error state in the specification; a normal ensures clause alone does not describe cleanup after an abort. This is an inference.
evidence and limits
The paper proposes either extending RustBelt with panics or relying on MIRâs explicit panic paths. It does not implement or prove either option in this report. Panic safety is therefore an unclosed trusted boundary.
what is proved, measured, and trusted
The paper proves no new theorem in the report itself. It relies on RustBeltâs published safety results and describes an intended correspondence: if MSE verifies all safe abstractions and unsafe code, then a RustBelt proof should exist for the abstractionsâ type guarantees (Section 4.1, p. 15). The implementation evidence is one verified allocation example and an available Zenodo artifact; the denominator is one reported example, not a benchmark suite. The report gives no percentages, throughput, proof-time distribution, or comparison with Verus, RustBelt, or other tools.
Trusted or assumed components are the Rust compilerâs type/borrow checking and MIR semantics, the translation from MIR and RustBelt predicates into VeriFast, VeriFastâs engine, the RustBelt semantic model, user annotations, and the absence of unsupported language features. The report itself identifies the missing rigorous soundness proof and the limited subset as boundaries.
Inference: threat to VeruLaw is high for unsafe libraries, interior mutability, panic paths, and foreign or compiler-generated code, because a safe public type can hide a violated invariant. Threat is lower for ordinary safe Rust already covered by Verus contracts. Existing RustBelt work and this MSE proposal already cover the generic idea of semantic safe-abstraction checking; VL should focus on concrete Verus support gaps, such as panic-aware ownership APIs or measured raw-pointer libraries, rather than redo the proposal abstractly.
techniques for the catalog
- contract-based MSE â verify a function once against weakest-precondition and strongest-postcondition heap contracts, then reuse them at calls
- symbolic heap execution â represent addresses and values by symbols plus path conditions and separation-logic heap chunks
- RustBelt type interpretation â open ownership and shared-reference predicates, use lifetime/thread tokens, and close them at return
- MIR lowering â compile checked Rust to MIR and translate its control flow and type semantics into a reusable verification backend
- safe-abstraction checking â prove that unsafe implementation bodies satisfy the semantic predicate of their safe public type
- panic-path verification â symbolically execute MIR unwind edges and check transient invalid-state cleanup separately
Checked 2026-10-15 against the paper by a Codex agent for modular_formal_verification_of_rust_prog_20261014.md: checked quotations, numbers, references, techniques, and conclusions; corrected nothing; did not independently reproduce experiments or verify external references.
Last edited: