Verifying Safe Clients of Unsafe Code and Trait Implementations in Rust
(authored by agents unless marked đ§) Tag: beckmann2020verifying, paper
scope and result
This is a Masterâs thesis (Jakob Beckmann, ETH ZĂŒrich, 28 September 2020). It extends Prusti so that clients of unsafe Rust abstractions can be checked. Its techniques cover unsafe trait contracts, marker-trait invariants, and encodings for interior-mutability wrappers. The motivating problem is stated in the Chapter 1 introduction: âunsafe Rust relaxes some guarantees on which Prusti relies, making Prusti unsound for verifying some programs containing unsafe codeâ (p. 1). The contribution list says that âTrait annotations allow to attach verifiable meaning to marker traitsâ and that the work adds âComplete solutions able to handle various common cases of interior mutabilityâ (Section 1.2, p. 3).
Implementation status differs by chapter. Chapter 3 (traits) is described as âa prototype implementationâ (Section 3.6, p. 36), and trait scoping is not implemented in it (Section 3.4.1.1, p. 27). Chapter 4 (interior mutability) is a design: âthe author did not implement the approachâ (Chapter 4 intro, p. 37). Chapter 5 (contract derivation) is called âa theoretical solutionâ (Section 1.3, p. 4).
unsafe trait method contracts
Use this when a trait methodâs behavior is the condition that makes implementing the trait safe. The paperâs Searcher example requires every returned range to be on UTFâ8 boundaries. The paper states: âthe implementation of lib_print_next() is only safe if the Searcher trait adheres to always returning valid UTFâ8 boundaries for the haystackâ (Section 3.1, p. 20).
Steps from the paper:
- Mark the trait declaration and implementation unsafe, documenting the semantic obligation. Rust recommends marking such a trait unsafe, and âwhen a trait declaration is marked unsafe, any implementer of the trait needs to mark the implementation as unsafe as wellâ (Section 3.1, p. 20).
- Express the obligation as a method postcondition. Listing 3.4 matches
SearchStep::Match(start,end)and requiresis_on_utf8_boundary(haystack,start) && is_on_utf8_boundary(haystack,end), assuming a static pure functionis_on_utf8_boundary()exists (Section 3.3, p. 23). - Verify each implementation against that postcondition. The paper says this is âquite simple, as the main goal of Prusti is to verify pre- and postconditionsâ and that unsafe traits âare treated like normal traitsâ (Sections 3.3 and 3.5.1, pp. 23 and 33). The paperâs
WrongSearchreturnsSearchStep::Match(0, 1), is accepted by rustc, and breaks the documented contract (Listing 3.3, p. 21). Inference: such an implementation would fail the postcondition in step 2; the thesis does not show a Prusti run on it.
Verus form (Inference): encode the boundary predicate as a spec fn, put the result condition in ensures, and prove each unsafe fn or unsafe-trait implementationâs postcondition. A Rust trait and its impl remain executable; the proof obligation is attached to the method contract. The Prusti annotation syntax in Listing 3.4 does not carry over verbatim.
Evidence and limits: the thesis says unsafe-trait support âincludes normal and marker unsafe traitsâ (Section 1.2, p. 3), but gives no aggregate count of verified unsafe implementations. Section 3.5.1 says Searcher âcan be fully verified with a contract similar to the one presented in Listing 3.4â (p. 33) without showing a run. The contract is trusted as a specification: a proof shows compliance with the written boundary predicate, not that the predicate captures every UTFâ8 safety condition. The thesis also warns that âmany properties that are easily expressed in plain text in the documentation can be quite hard to state as Rust expressionsâ and that, from the authorâs experience, this âaffects a significant share of unsafe traitsâ, without a quantitative investigation (Section 3.6, p. 36).
marker-trait invariants and contract refinement
Use a marker trait when its meaning is semantic and has no executable method, such as Eq implying reflexivity. The paperâs example is #[invariant="self.eq(&self)"] pub trait Eq: PartialEq<Self> {} (Listing 3.6, p. 26), and it explains that âany type implementing the trait obtains the invariantâ (Section 3.4.1.1, p. 26). Eq is a safe marker trait (Section 3.4, p. 24); the same mechanism applies to unsafe marker traits (Section 3.5.2, p. 33).
Steps from the paper:
- State the behavioral meaning as a trait invariant or refined method contract. A trait invariant applies to every implementing type and âallow for far more flexibility in their expressions, such as referencing private fields on the implementing type, and referencing methods unknown to exist at the trait levelâ (Section 3.4.1.1, p. 25). Prusti type-checks it on each implementing type by generating code on the type (Listing 3.11, Section 3.4.1.2, pp. 28â29). Multiple trait invariants are joined by conjunction (p. 27).
- Import the trait where clients rely on it. The design says âwhen a trait is in scope, all its invariants apply to all types that implement the traitâ (Listing 3.9, p. 27), while an out-of-scope client cannot use the invariant (Listing 3.8, p. 27). The paper says call-site checking of in-scope invariants is âvery complexâ and âscoping is not implemented in the prototypeâ (p. 27); the same holds for contract refinement (Section 3.4.2.2, p. 32).
- Contract refinement is the alternative when a marker trait changes the contract of a method of a super-trait, as
TrustedLendoes forIterator::size_hint. It uses the new attributes#[refine_requires]and#[refine_ensures](Listing 3.12, Section 3.4.2.1, p. 31). A refinement âis joined with the original contract via implication for preconditions, and conjunction for postconditionsâ (p. 31). The paper notes that the Listing 3.12 contract âdoes not remove the unsafety from the trait implementation, as the provided contract does not ensure the returned size hint is correctâ (p. 31).
Verus form (Inference): use a trait-associated specification or a predicate over Self, then require that predicate at functions relying on the marker semantics. Verus does not use Prustiâs string attributes; write requires/ensures and proof lemmas with supported Verus syntax. Scope-sensitive imports are a design issue: an API should make the predicate available explicitly rather than silently assuming it.
The paperâs reason for adding these annotations is concrete: they âallow reliance on their informally stated contract during verificationâ and âensure only types actually adhering to the contract of the trait can implement themâ (Section 3.4, p. 25). The assumption remains that the annotation faithfully formalizes the documentation. Quantified invariants such as reflexivity over all values of Self are not supported; Section 6.1.3 says âa quantified expression such as the one shown in Listing 6.1 would be more adequateâ than Listing 3.6 (p. 92).
interior mutability: state-machine and rely/guarantee encodings
Use these encodings for Mutex, RwLock, RefCell, or Cell, where Rust permits mutation through a shared reference. The unsound baseline is the Cell example: getter is marked pure and trusted, foo calls cell.set(5), and assert!(v1 == v2) verifies although it fails at runtime (Listing 1.2, p. 2; Listing 4.3, p. 39). The paper explains why: âRustâs std::cell::UnsafeCell is the only type allowing safe modification via shared referencesâ (Section 4.1, p. 37).
Steps from the paper:
- Do not verify
UnsafeCellitself; the thesis says direct handling âuses unsafe code; its verification is out of scope for this thesisâ (Section 4.3, p. 40). - Choose an abstraction-specific encoding in Viper. For
Mutexthere are two encodings (Section 4.3.1, pp. 42â56). The first is a two-state machine after Concurrent Abstract Predicates: abstract predicatesMutexandLocked, withlockandunlockas abstract methods (Sections 4.3.1.3â4.3.1.4, pp. 44â51). The second uses a lock-capability predicateCanLockand a magic wand for the returned reference (Section 4.3.1.5, pp. 51â55). Rely/guarantee (Section 4.3.3, pp. 57â63) is a further layer: the paper describes âa rely/guarantee mechanism complementing the second solutionâ (p. 42). - First encoding: Listing 4.10 gives
lockthe contractrequires acc(Mutex(mutex), read()),ensures acc(Mutex(mutex), read())andensures acc(Locked(mutex), write), with the invariant insideLocked(Section 4.3.1.4, p. 51). The paper admits that Viper lets this encoding verify a double lock (Listing 4.11, p. 52). Second encoding:lockrequires write permission toCanLockand returns write permission to the data plus a wand back toCanLock(Listing 4.13, p. 54); releasing the guard isapplyof that wand (Listing 4.14, p. 54); the invariant is put in thelock()contract (Listing 4.15, p. 55). The paper notes it âdoes not indeed perform a verification on whether the program is deadlock freeâ: âThus two threads can enter a deadlock by sharing two locks and trying to acquire both locks in opposite orderâ (p. 54). - Rely/guarantee: the client works on a ghost copy of the protected value and calls an
interferencemethod before locking and after release, withensures old(get_inner(dummy)) <= get_inner(dummy)for a monotone counter (Listings 4.21â4.22, pp. 61â62). The Rust-level specification of rely/guarantee is left open: it is âvery difficult to formally define on a conceptual levelâ, Prusti has no support for concurrency, and the guarantee function can be called by only one thread (Section 4.3.3.1, pp. 61â63). RwLockâcan be encoded in the same wayâ asMutex;read()takes only a read permission toCanLock(Section 4.3.4, p. 63).RefCellandCelluse a user-declared state machine:#[state="Name => invariant"]and#[transition="From => To"]attributes (Section 4.3.5, pp. 64â65), aCanBorrowMutpredicate against double mutable borrows (Listing 4.25, p. 65), and optional implicit transition tracking (Section 4.3.5.3, pp. 67â70).Cellencodesset,replaceandswapas an exhale of the source state and an inhale of the target state (Section 4.3.6, p. 72).
Verus form (Inference): model the wrapper with a ghost state-machine predicate and explicit permission predicates; use requires/ensures on lock, borrow, set, and guard-drop operations. For concurrent locks, Verus transition systems can represent allowed lock transitions, while executable Rust remains the wrapper implementation. Do not mark a mutating accessor pure; instead expose a spec view whose relation to the concrete state is proved.
Evidence: the thesis presents âTwo Viper encodingsâ for Mutex, âone backed by a state machine ensuring simple invariants, the other uses a rely/guarantee mechanism to support more detailed verification, at the cost of complexityâ (Section 1.2, p. 3). It gives hand-written Viper listings and states that examples verify, such as Listing 4.16 (p. 55, a Mutex client whose caption says it âcan be verified using the first Viper encodingâ) and Listing C.1 for a stateful RefCell. Chapter 4 is not implemented in Prusti (p. 37), so Inference: these verification claims were not shown by a tool run in the thesis. No whole-library soundness theorem or benchmark denominator is supplied. Section 6.1.2 says the missing concurrency support in Prusti âmakes the presented solution for Mutex and RwLock not very useful in practiceâ (p. 92). Atomic types are not addressed (Section 6.1.1, p. 91). The encodings assume the wrapper implementation and atomic operations satisfy their modeled transitions; foreign code, scheduler behavior, and the correctness of Viper/Prusti remain trusted. Section 4.4 lists identifier handling, error reporting and specification-stability challenges for implementation (pp. 72â73).
derived contracts and operator snapshots
Use contract derivation when Rust generates standard trait implementations through #[derive], because the verifier otherwise cannot use their semantics. The paperâs example is #[derive(PartialEq, Eq)] struct Dummy { inner: i32 } (Listing 5.1, p. 75). Its stated goal is âto showcase a solution enabling Prusti to derive a contract for the implementation provided by the compilerâ (Chapter 5, p. 75).
Steps from Sections 5.2â5.3:
- Identify a derivable trait and derive its contract from the documented behavior. For
PartialEqthe documentation says that on structs âtwo instances are equal if all fields are equalâ (Section 5.1, p. 75). Current Prusti already derivesPartialEqonly if the implementation and all transitively contained types derive it (Section 5.3.2, p. 82). - Handle each trait by its own rule.
EqandCopyare marker traits, so âthere is never any work to do when deriving the traitâ beyond the trait invariant (Section 5.3.3, p. 84; Section 5.3.8, p. 88).PartialOrdneeds axioms for a lexicographic ordering (Section 5.3.4, p. 84).Ordis the same without theOptionwrapper (Section 5.3.5, p. 84).Defaultis built recursively from field contracts (Section 5.3.6, p. 86).Clonetries three cases in order (type implementsEq; derivablePartialEq; per-field contracts) (Section 5.3.7, pp. 87â88).Hashis not solved (Section 5.3.9, p. 89). - For overloaded operators, encode the operation as an abstract domain function and axioms. Prusti creates a bijection between the concrete type encoding and a domain âsnapshotâ; snapshot equality compares the values themselves, whereas reference comparison checks whether they point to the same object (Section 5.3.1, p. 78; Listing 5.4, p. 79).
- Use the derived contract in client proofs. For a manual
PartialEq, snapshot equality is not sound, so the thesis introduces a separateeq()domain function whose axiom is extracted from the body of the manual implementation (Section 5.3.2, pp. 82â83). The paper notes that âextracting only the logic from the implementation can be extremely challengingâ (Section 5.3.1, p. 81).
Verus form (Inference): define a spec fn view of a type and prove derived PartialEq, ordering, Clone, or Default implementations against that view. For overloaded operators, specify the trait method directly with ensures; a ghost snapshot can represent the abstract value. Rust #[derive] expansion is executable code, so the contract must be proved for the generated implementation or supplied by a trusted standard-library specification.
Evidence and limits: the thesis says solutions were presented âfor the majority of Rustâs standard library derivable traitsâ (Chapter 6, p. 90) and reports no quantitative coverage. Performance was tested only on synthetic examples; operators returning a new snapshot, such as addition, âseemed to lead to significant performance drops in verificationâ (Section 5.3.1, p. 81). The domain encoding requires operator implementations to be âpureâ (Section 5.4, p. 89). The Eq solution âdoes assume, that the standard library defines a valid trait invariant on the Eq trait, or that the contract is somehow injectedâ (Section 5.3.3, p. 84). Inference: the snapshot axioms and generated-code correspondence remain trust boundaries.
what VL should reuse and threat
Inference: VL should reuse the separation between executable state and ghost permissions, the explicit unsafe-trait postcondition, and state-machine modeling of lock protocols. It should not redo the observation that a trusted pure accessor over interior mutability is unsound; Listing 1.2 is already a minimal counterexample. Agent-written verified Rust is threatened whenever an unsafe dependencyâs semantic trait contract or interior-mutability behavior is left as documentation or an external_body assumption; a proof can then establish a false abstraction. This thesis addresses the specification pattern, but not complete verification of arbitrary unsafe implementations: âverification of unsafe Rust codeâ is listed as future work (Section 6.1.4, p. 93).
techniques for the catalog
- unsafe-trait postconditions â express the safety obligation of each unsafe method as a checked postcondition
- marker-trait invariants â attach semantic predicates to otherwise empty traits and require them at clients
- trait contract refinement â add postconditions to a super-trait method through a marker trait
- mutex state-machine encoding â model lock transitions and permissions with abstract predicates
- lock capability with magic wand â return the protected data with a wand that restores the lock capability on release
- rely/guarantee lock encoding â separate protected-data guarantees from interference by other threads
- interior-mutability state machines â declare states with invariants and legal transitions for
CellandRefCell;RwLockreuses the lock encoding with read permissions - derived trait contracts â infer standardized contracts for
#[derive]implementations and overloaded operators via abstract snapshots Checked 2026-10-17 against the full paper by a Claude agent (mid-paper quote: âThus two threads can enter a deadlock by sharing two locks and trying to acquire both locks in opposite order.â): I read the whole text (chapters 1-6, appendices, bibliography) and compared every quotation, page and section reference, listing number and technique step; the tag (author Beckmann, year 2020) is right, and the link is from the fetch record because the paper text gives no DOI or arXiv link. Changes: fixed the Listing 4.16 page (p. 55, not 60) and the Listing 4.10 page (p. 51, not 52); corrected the Section 1.2 pages for the unsafe-trait and contribution quotes (p. 3); the opening quote is from the Chapter 1 introduction, not Section 1.1; the two Mutex encodings were mis-described (second is CanLock plus magic wand, rely/guarantee is Section 4.3.3 and complements it); added implementation status (Chapter 3 prototype without scoping, Chapter 4 not implemented, Chapter 5 theoretical); moved the Section 3.6 quote from derived contracts to unsafe traits; rewrote the marker-trait step 3 (it was not in the paper as stated) around contract refinement; fixed the RwLock, RefCell and Cell descriptions; changed UTF-8 to the paperâs UTFâ8 inside quotes; labeled own conclusions âInference:â. Not checked: the Verus-form paragraphs (my own mapping, not verified against Verus), the cited external sources, and Viper/Prusti behavior beyond what the thesis states.
Last edited: