RustHorn: CHC-Based Verification for Rust Programs

Reduction to the satisfiablility problem for constrained Horn clauses (CHCs) is a widely studied approach to automated program verification. The current CHC-based methods for pointer-manipulating programs, however, are not very scalable. This paper proposes a novel translation of pointer-manipulating Rust programs into CHCs, which clears away pointers and heaps by leveraging ownership. We formalize the translation for a simplified core of Rust and prove its correctness. We have implemented a prototype verifier for a subset of Rust and confirmed the effectiveness of our method.


Introduction
Reduction to constrained Horn clauses (CHCs) is a widely studied approach to automated program verification [22,6]. A CHC is a Horn clause [30] equipped with constraints, namely a formula of the form ϕ ⇐= ψ 0 ∧ · · · ∧ ψ k−1 , where ϕ and ψ 0 , . . . , ψ k−1 are either an atomic formula of the form f (t 0 , . . . , t n−1 ) (f is a predicate variable and t 0 , . . . , t n−1 are terms), or a constraint (e.g. a < b + 1). 1 We call a finite set of CHCs a CHC system or sometimes just CHC. CHC solving is an act of deciding whether a given CHC system S has a model, i.e. a valuation for predicate variables that makes all the CHCs in S valid. A variety of program verification problems can be naturally reduced to CHC solving.
For example, let us consider the following C code that defines McCarthy's 91 function.
int mc91(int n) { if (n > 100) return n -10; else return mc91(mc91(n + 11)); } Suppose that we wish to prove mc91(n) returns 91 whenever n ≤ 101 (if it terminates). The wished property is equivalent to the satisfiability of the following CHCs, where Mc91 (n, r) means that mc91(n) returns r if it terminates.
Mc91 (n, r) ⇐= n > 100 ∧ r = n − 10 The full version of this paper is available as [47]. 1 Free variables are universally quantified. Terms and variables are governed under sorts (e.g. int, bool), which are made explicit in the formalization of § 3.
However, the current CHC-based methods do not scale very well for programs using pointers, as we see in § 1.1. We propose a novel method to tackle this problem for pointer-manipulating programs under Rust-style ownership, as we explain in § 1.2.

Challenges in Verifying Pointer-Manipulating Programs
The standard CHC-based approach [23] for pointer-manipulating programs represents the memory state as an array, which is passed around as an argument of each predicate (cf. the store-passing style), and a pointer as an index.
For example, a pointer-manipulating variation of the previous program void mc91p(int n, int* r) { if (n > 100) *r = n -10; else { int s; mc91p(n + 11, &s); mc91p(s, r); } } is translated into the following CHCs by the array-based approach: 3 Mc91p ( Mc91p additionally takes two arrays h, h representing the (heap) memory states before/after the call of mc91p. The second argument r of Mc91p, which corresponds to the pointer argument r in the original program, is an index for the arrays. Hence, the assignment *r = n -10 is modeled in the first CHC as an update of the r-th element of the array. This CHC system has a model Mc91p(n, r, h, h ) :⇐⇒ h [r] = 91 ∨ (n > 100 ∧ h [r] = n − 10), which can be found by some array-supporting CHC solvers including Spacer [40], thanks to evolving SMT-solving techniques for arrays [62,10].
However, the array-based approach has some shortcomings. Let us consider, for example, the following innocent-looking code. 4 bool just_rec(int* ma) { if (rand() >= 0) return true; int old_a = *ma; int b = rand(); just_rec(&b); return (old_a == *ma); } It can immediately return true; or it recursively calls itself and checks if the target of ma remains unchanged through the recursive call. In effect this function does nothing on the allocated memory blocks, although it can possibly modify some of the unused parts of the memory.
Suppose we wish to verify that just_rec never returns false. The standard CHC-based verifier for C, SeaHorn [23], generates a CHC system like below: 56 JustRec(ma, h, h , r) ⇐= h = h ∧ r = true JustRec(ma, h, h , r) ⇐= mb = ma ∧ h = h{mb ← b} ∧ JustRec(mb, h , h , r ) ∧ r = (h[ma] == h [ma]) r = true ⇐= JustRec(ma, h, h , r) Unfortunately the CHC system above is not satisfiable and thus SeaHorn issues a false alarm. This is because, in this formulation, mb may not necessarily be completely fresh; it is assumed to be different from the argument ma of the current call, but may coincide with ma of some deep ancestor calls. 7 The simplest remedy would be to explicitly specify the way of memory allocation. For example, one can represent the memory state as a pair of an array h and an index sp indicating the maximum index that has been allocated so far.
JustRec + (ma, h, sp, h , sp , r) ⇐= h = h ∧ sp = sp ∧ r = true JustRec + (ma, h, sp, h , sp , r) ⇐= mb = sp = sp + 1 ∧ h = h{mb ← b} JustRec + (mb, h , sp , h , sp , r ) ∧ r = (h[ma] == h [ma]) r = true ⇐= JustRec + (ma, h, sp, h , sp , r) ∧ ma ≤ sp The resulting CHC system now has a model, but it involves quantifiers: Finding quantified invariants is known to be difficult in general despite active studies on it [41,2,36,26,19] and most current array-supporting CHC solvers give up finding quantified invariants. In general, much more complex operations on pointers can naturally take place, which makes the universally quantified invariants highly involved and hard to automatically find. To avoid complexity of models, CHC-based verification tools [23,24,37] tackle pointers by pointer analysis [61,43]. Although it does have some effects, the current applicable scope of pointer analysis is quite limited.

Our Approach: Leverage Rust's Ownership System
This paper proposes a novel approach to CHC-based verification of pointermanipulating programs, which makes use of ownership information to avoid an explicit representation of the memory.
Rust-style Ownership. Various styles of ownership/permission/capability have been introduced to control and reason about usage of pointers on programming language design, program analysis and verification [13,31,8,31,9,7,64,63]. In what follows, we focus on the ownership in the style of the Rust programming language [46,55].
Roughly speaking, the ownership system guarantees that, for each memory cell and at each point of program execution, either (i) only one alias has the update (write & read) permission to the cell, with any other alias having no permission to it, or (ii) some (or no) aliases have the read permission to the cell, with no alias having the update permission to it. In summary, when an alias can read some data (with an update/read permission), any other alias cannot modify the data.
As a running example, let us consider the program below, which follows Rust's ownership discipline (it is written in the C style; the Rust version is presented at Example 1): if (*ma >= *mb) return ma; else return mb; } bool inc_max(int a, int b) { { int* mc = take_max(&a, &b); // borrow a and b *mc += 1; } // end of borrow return (a != b); } Figure 1 illustrates which alias has the update permission to the contents of a and b during the execution of take_max (5,3).
A notable feature is borrow. In the running example, when the pointers &a and &b are taken for take_max, the update permissions of a and b are temporarily transferred to the pointers. The original variables, a and b, lose the ability to access their contents until the end of borrow. The function take_max returns a pointer having the update permission until the end of borrow, which justifies the update operation *mc += 1. In this example, the end of borrow is at the end of the inner block of inc_max. At this point, the permissions are given back to the original variables a and b, allowing to compute a != b. Note that mc can point to a and also to b and that this choice is determined dynamically. The values of a and b after the borrow depend on the behavior of the pointer mc.
The end of each borrow is statically managed by a lifetime. See § 2 for a more precise explanation of ownership, borrow and lifetimes.
Values and aliases of a and b in evaluating inc_max (5,3). Each line shows each variable's permission timeline: a solid line expresses the update permission and a bullet shows a point when the borrowed permission is given back. For example, b has the update permission to its content during (i) and (iv), but not during (ii) and (iii) because the pointer mb, created at the call of take_max, borrows b until the end of (iii).
Key Idea. The key idea of our method is to represent a pointer ma as a pair a, a • of the current target value a and the target value a • at the end of borrow. 89 This representation employs access to the future information (it is related to prophecy variables; see § 5). This simple idea turns out to be very powerful. In our approach, the verification problem "Does inc_max always return true?" is reduced to the satisfiability of the following CHCs: The mutable reference ma is now represented as a, a • , and similarly for mb and mc. The first CHC models the then-clause of take_max: the return value is ma, which is expressed as r = a, a • ; in contrast, mb is released, which constrains b • , the value of b at the end of borrow, to the current value b. In the clause on IncMax , mc is represented as a pair c, c • . The constraint c = c + 1 ∧ c • = c models the increment of mc (in the phase (iii) in Fig. 1). Importantly, the final check a != b is simply expressed as a • != b • ; the updated values of a/b are available as a • /b • . Clearly, the CHC system above has a simple model. Also, the just_rec example in § 1.1 can be encoded as a CHC system r = true ⇐= JustRec( a, a • , r). Now it has a simple model: JustRec( a, a • , r) :⇐⇒ r = true ∧ a • = a. Remarkably, arrays and quantified formulas are not required to express the model, which allows the CHC system to be easily solved by many CHC solvers. More advanced examples are presented in § 3.4, including one with destructive update on a singly-linked list.
Contributions. Based on the above idea, we formalize the translation from programs to CHC systems for a core language of Rust, prove correctness (both soundness and completeness) of the translation, and confirm the effectiveness of our approach through preliminary experiments. The core language supports, among others, recursive types. Remarkably, our approach enables us to automatically verify some properties of a program with destructive updates on recursive data types such as lists and trees.
The rest of the paper is structured as follows. In § 2, we provide a formalized core language of Rust supporting recursions, lifetime-based ownership and recursive types. In §3, we formalize our translation from programs to CHCs and prove its correctness. In § 4, we report on the implementation and the experimental results. In § 5 we discuss related work and in § 6 we conclude the paper.

Core Language: Calculus of Ownership and Reference
We formalize a core of Rust as Calculus of Ownership and Reference (COR), whose design has been affected by the safe layer of λ Rust in the RustBelt paper [32]. It is a typed procedural language with a Rust-like ownership system.

Syntax
The following is the syntax of COR.
A pointer type P T can be an owning pointer own T (Box<T> in Rust), mutable reference mut α T (&'a mut T) or immutable reference immut α T (&'a T). An owning pointer has data in the heap memory, can freely update the data (unless it is borrowed), and has the obligation to clean up the data from the heap memory. In contrast, a mutable/immutable reference (or unique/shared reference) borrows an update/read permission from an owning pointer or another reference with the deadline of a lifetime α (introduced later). A mutable reference cannot be copied, while an immutable reference can be freely copied. A reference loses the permission at the time when it is released. 14 A type T that appears in a program (not just as a substructure of some type) should satisfy the following condition (if it holds we say the type is complete): every type variable X in T is bound by some µ and guarded by a pointer constructor (i.e. given a binding of form µX.U , every occurrence of X in U is a part of a pointer type, of form P U ).

Lifetime.
A lifetime is an abstract time point in the process of computation, 15 which is statically managed by lifetime variables α. A lifetime variable can be a lifetime parameter that a function takes or a local lifetime variable introduced within a function. We have three lifetime-related ghost instructions: intro α introduces a new local lifetime variable, now α sets a local lifetime variable to the current moment and eliminates it, and α ≤ β asserts the ordering on local lifetime variables.
Expressivity and Limitations. COR can express most borrow patterns in the core of Rust. The set of moments when a borrow is active forms a continuous time range, even under non-lexical lifetimes [54]. 16 A major limitation of COR is that it does not support unsafe code blocks and also lacks type traits and closures. Still, our idea can be combined with unsafe code and closures, as discussed in §3.5. Another limitation of COR is that, unlike Rust and λ Rust , we cannot directly modify/borrow a fragment of a variable (e.g. an element of a pair). Still, we can eventually modify/borrow a fragment by borrowing the whole variable and splitting pointers (e.g. 'let ( * y 0 , * y 1 ) = * x'). This borrow-and-split strategy, nevertheless, yields a subtle obstacle when we extend the calculus for advanced data types (e.g. get_default in 'Problem Case #3' from [54]). For future work, we pursue a more expressive calculus modeling Rust and extend our verification method to it.
Example 1 (COR Program). The following program expresses the functions take_max and inc_max presented in § 1.2. We shorthand sequential executions 14 In Rust, even after a reference loses the permission and the lifetime ends, its address data can linger in the memory, although dereferencing on the reference is no longer allowed. We simplify the behavior of lifetimes in COR. 15 In the terminology of Rust, a lifetime often means a time range where a borrow is active. To simplify the discussions, however, we in this paper use the term lifetime to refer to a time point when a borrow ends. 16 Strictly speaking, this property is broken by recently adopted implicit two-phase borrows [59,53]. However, by shallow syntactical reordering, a program with implicit two-phase borrows can be fit into usual borrow patterns. by '; L ' (e.g. L 0 : I 0 ; L1 I 1 ; goto L 2 stands for L 0 : I 0 ; goto L 1 L 1 : I 1 ; goto L 2 ). 17 fn take-max α (ma: mutα int, mb: mutα int) → mutα int { entry: let * ord = * ma >= * mb; L1 match * ord {inj 1 * ou → goto L2, inj 0 * ou → goto L5} L2: drop ou; L3 drop mb; L4 return ma L5: drop ou; L6 drop ma; L7 return mb } fn inc-max(oa: own int, ob: In take-max, conditional branching is performed by match and its goto directions (at L1). In inc-max, increment on the mutable reference mc is performed by calculating the new value (at L4, L5) and updating the data by swap (at L7).
The following is the corresponding Rust program, with ghost annotations (marked italic and dark green, e.g. drop ma ) on lifetimes and releases of mutable references.

Type System
The type system of COR assigns to each label a whole context (Γ, A). We define below the whole context and the typing judgments.
Context. A variable context Γ is a finite set of items of form x: a T , where T should be a complete pointer type and a (which we call activeness) is of form 'active' or ' †α' (frozen until lifetime α). We abbreviate x: active T as x: T . A variable context should not contain two items on the same variable. A lifetime context A = (A, R) is a finite preordered set of lifetime variables, where A is the underlying set and R is the preorder. We write |A| and ≤ A to refer to A and R. Finally, a whole context (Γ, A) is a pair of a variable context Γ and a lifetime context A such that every lifetime variable in Γ is contained in A.
Notations. The set operation A + B (or more generally λ A λ ) denotes the disjoint union, i.e. the union defined only if the arguments are disjoint. The set operation A − B denotes the set difference defined only if A ⊇ B. For a natural number n, [n] denotes the set {0, . . . , n−1}.
Generally, an auxiliary definition for a rule can be presented just below, possibly in a dotted box.
Program and Function. The rules for typing programs and functions are presented below. They assign to each label a whole context (Γ, A). 'S: LabelStmtF : the set of labeled statements in F IdA: the identity relation on A R + : the transitive closure of R On the rule for the function, the initial whole context at entry is specified (the second and third preconditions) and also the contexts for other labels are checked (the fourth precondition). The context for each label (in each function) can actually be determined in the order by the distance in the number of goto jumps from entry, but that order is not very obvious because of unstructured control flows.
means that running the statement S (under Π, f ) with the whole context (Γ, A) results in a jump to a label with the whole contexts specified by (Γ L , A L ) L or a return of data of type U . Its rules are presented below. 'I: Π,f (Γ, A) → (Γ , A )' is explained later.
The rule for the return statement ensures that there remain no extra variables and local lifetime variables.
Instruction. 'I: Π,f (Γ, A) → (Γ , A )' means that running the instruction I (under Π, f ) updates the whole context (Γ, A) into (Γ , A ). The rules are designed so that, for any I, Π, f , (Γ, A), there exists at most one (Γ , A ) such that I: Π,f (Γ, A) → (Γ , A ) holds. Below we present some of the rules; the complete rules are presented in the full paper. The following is the typing rule for mutable (re)borrow.
LifetimeT : the set of lifetime variables occurring in T After you mutably (re)borrow an owning pointer / mutable reference x until α, x is frozen until α. Here, α should be a local lifetime variable 18 (the first precondition) that does not live longer than the data of x (the third precondition). Below are the typing rules for local lifetime variable introduction and elimination.
On intro α, it just ensures the new local lifetime variable to be earlier than any lifetime parameters (which are given by exterior functions). On now α, the variables frozen with α get active again. Below is the typing rule for dereference of a pointer to a pointer, which may be a bit interesting.
The third precondition of the typing rule for mutbor justifies taking just α in the rule 'R α • R β := R α '.
Let us interpret Π: (Γ f,L , A f,L ) (f,L) ∈ FnLabel Π as "the program Π has the type (Γ f,L , A f,L ) (f,L) ∈ FnLabel Π ". The type system ensures that any program has at most one type (which may be a bit unclear because of unstructured control flows). Hereinafter, we implicitly assume that a program has a type.

Concrete Operational Semantics
We introduce for COR concrete operational semantics, which handles a concrete model of the heap memory.
The basic item, concrete configuration C, is defined as follows. Here, H is a heap, which maps addresses (represented by integers) to integers (data). F is a concrete stack frame, which maps variables to addresses. The stack part of C is of form '[f, L] F; [f , L ] x, F ; · · · ; end' (we may omit the terminator '; end'). [f, L] on each stack frame indicates the program point. 'x,' on each nontop stack frame is the receiver of the value returned by the function call.
Concrete operational semantics is characterized by the one-step transition relation C → Π C and the termination relation final Π (C), which can be defined straightforwardly. Below we show the rules for mutable (re)borrow, swap, function call and return from a function; the complete rules and an example execution are presented in the full paper. S Π,f,L is the statement for the label L of the function f in Π. Ty Π,f,L (x) is the type of variable x at the label.
S Π,f,L = let y = g · · · (x0, . . . , xn−1); goto L ΣΠ,g = · · · (x 0 : T0, . . . , x n−1 : Here we introduce '#T ', which represents how many memory cells the type T takes (at the outermost level). #T is defined for every complete type T , because every occurrence of type variables in a complete type is guarded by a pointer constructor.

CHC Representation of COR Programs
To formalize the idea discussed in § 1, we give a translation from COR programs to CHC systems, which precisely characterize the input-output relations of the COR programs. We first define the logic for CHCs ( § 3.1). We then formally describe our translation ( §3.2) and prove its correctness ( §3.3). Also, we examine effectiveness of our approach with advanced examples ( § 3.4) and discuss how our idea can be extended and enhanced ( § 3.5).

Multi-sorted Logic for Describing CHCs
To begin with, we introduce a first-order multi-sorted logic for describing the CHC representation of COR programs.
Syntax. The syntax is defined as follows.
A CHC system (Φ, Ξ) is a pair of a finite set of CHCs Φ = {Φ 0 , . . . , Φ n−1 } and Ξ, where Ξ is a finite map from predicate variables to tuples of sorts (denoted by Ξ), specifying the sorts of the input values. Unlike the informal description in § 1, we add Ξ to a CHC system.
Sort System. 't: ∆ σ' (the term t has the sort σ under ∆) is defined as follows.
Semantics. '[[t]] I ', the interpretation of the term t as a value under I, is defined as follows. Here, I is a finite map from variables to values. Although the definition is partial, the interpretation is defined for all well-sorted terms.
[   When M |= (Φ, Ξ) holds, we say that M is a model of (Φ, Ξ). Every wellsorted CHC system (Φ, Ξ) has the least model on the point-wise ordering (which can be proved based on the discussions in [16]), which we write as M least (Φ,Ξ) .

Translation from COR Programs to CHCs
Now we formalize our translation of Rust programs into CHCs. We define (|Π|), which is a CHC system that represents the input-output relations of the functions in the COR program Π. Roughly speaking, the least model M least (|Π|) for this CHC system should satisfy: for any values v 0 , . . . , v n−1 , w, M least (|Π|) |= f entry (v 0 , . . . , v n−1 , w) holds exactly if, in COR, a function call f (v 0 , . . . , v n−1 ) can return w. Actually, in concrete operational semantics, such values should be read out from the heap memory. The formal description and proof of this expected property is presented in § 3.3.
Auxiliary Definitions. The sort corresponding to the type T , (|T |), is defined as follows.P is a meta-variable for a non-mutable-reference pointer kind, i.e. own or immut α . Note that the information on lifetimes is all stripped off. We introduce a special variable res to represent the result of a function. 19 For a label L in a function f in a program Π, we defineφ Π,f,L , Ξ Π,f,L and ∆ Π,f,L as follows, if the items in the variable context for the label are enumerated as x 0 : a0 T 0 , . . . , x n−1 : an−1 T n−1 and the return type of the function is U .
ϕ CHC Representation. Now we introduce '(|L: S|) Π,f ', the set (in most cases, singleton) of CHCs modeling the computation performed by the labeled statement L: S in f from Π. Unlike informal descriptions in § 1, we turn to pattern matching instead of equations, to simplify the proofs. Below we show some of the rules; the complete rules are presented in the full paper. The variables marked green (e.g. x • ) should be fresh. The following is the rule for mutable (re)borrow.
The value at the end of borrow is represented as a newly introduced variable x • . Below is the rule for release of a variable. The body (the right-hand side of ⇐= ) of the CHC contains two formulas, which yields a kind of call stack at the level of CHCs. Below is the rule for a return from a function.
The variable res is forced to be equal to the returned variable x. Finally, (|Π|), the CHC system that represents the COR program Π (or the CHC representation of Π), is defined as follows.

Correctness of the CHC Representation
Now we formally state and prove the correctness of the CHC representation.
Readout and Safe Readout. We introduce a few judgments to formally describe how read out data from the heap.
First, the judgment 'readout H ( * a :: T | v; M)' (the data at the address a of type T can be read out from the heap H as the value v, yielding the memory footprint M) is defined as follows. 21 Here, a memory footprint M is a finite multiset of addresses, which is employed for monitoring the memory usage.  Here, the 'no duplicate items' precondition checks the safety on the ownership.
COS-based Model. Now we introduce the COS-based model (COS stands for concrete operational semantics) f COS Π to formally describe the expected inputoutput relation. Here, for simplicity, f is restricted to one that does not take lifetime parameters (we call such a function simple; the input/output types of a simple function cannot contain references). We define f COS Proof. The details are presented in the full paper. We outline the proof below. First, we introduce abstract operational semantics, where we get rid of heaps and directly represent each variable in the program simply as a value with abstract variables, which is strongly related to prophecy variables (see § 5). An abstract variable represents the undetermined value of a mutable reference at the end of borrow.
Next, we introduce SLDC resolution for CHC systems and find a bisimulation between abstract operational semantics and SLDC resolution, whereby we show that the AOS-based model, defined analogously to the COS-based model, is equivalent to the least model of the CHC representation. Moreover, we find a bisimulation between concrete and abstract operational semantics and prove that the COS-based model is equivalent to the AOS-based model.
Finally, combining the equivalences, we achieve the proof for the correctness of the CHC representation.
Interestingly, as by-products of the proof, we have also shown the soundness of the type system in terms of preservation and progression, in both concrete and abstract operational semantics. Simplification and generalization of the proofs is left for future work.

Advanced Examples
We give advanced examples of pointer-manipulating Rust programs and their CHC representations. For readability, we write programs in Rust (with ghost annotations) instead of COR. In addition, CHCs are written in an informal style like § 1, preferring equalities to pattern matching.  Unlike just_rec, the function linger_dec can modify the local variable of an arbitrarily deep ancestor. Interestingly, each recursive call to linger_dec can introduce a new lifetime 'b , which yields arbitrarily many layers of lifetimes.
Suppose we wish to verify that linger_dec never returns false. If we use, like JustRec + in § 1.1, a predicate taking the memory states h, h and the stack pointer sp, we have to discover the quantified invariant: In contrast, our approach reduces this verification problem to the following CHCs: ∧ LingerDec(mc, r ) ∧ r = (r && oldb >= b•) r = true ⇐= LingerDec( a, a• , r). This can be solved by many solvers since it has a very simple model: This is a program that manipulates singly linked integer lists, defined as a recursive data type. take_some takes a mutable reference to a list and returns a mutable reference to some element of the list. sum calculates the sum of the elements of a list. inc_some increments some element of a list via a mutable reference and checks that the sum of the elements of the list has increased by 1.
Suppose we wish to verify that inc_some never returns false. Our method translates this verification problem into the following CHCs. 23 Although the model relies on the function sum, the validity of the model can be checked without induction on sum (i.e. we can check the validity of each CHC just by properly unfolding the definition of sum a few times).
The example can be fully automatically and promptly verified by our approach using HoIce [12,11] as the back-end CHC solver; see § 4.

Discussions
We discuss here how our idea can be extended and enhanced. 23 [x|xs] is the cons made of the head x and the tail xs. [] is the nil. In our formal logic, they are expressed as inj 0 (x, xs ) and inj 1 ().
Applying Various Verification Techniques. Our idea can also be expressed as a translation of a pointer-manipulating Rust program into a program of a stateless functional programming language, which allows us to use various verification techniques not limited to CHCs. Access to future information can be modeled using non-determinism. To express the value a • coming at the end of mutable borrow in CHCs, we just randomly guess the value with non-determinism. At the time we actually release a mutable reference, we just check a' = a and cut off execution branches that do not pass the check.
For example, take_max/inc_max in § 1.2/Example 1 can be translated into the following OCaml program. 'let a' = Random.int(0)' expresses a random guess and 'assume (a' = a)' expresses a check. The original problem "Does inc_max never return false?" is reduced to the problem "Does main never fail at assertion?" on the OCaml program. 24 This representation allows us to use various verification techniques, including model checking (higher-order, temporal, bounded, etc.), semi-automated verification (e.g. on Boogie [48]) and verification on proof assistants (e.g. Coq [15]). The property to be verified can be not only partial correctness, but also total correctness and liveness. Further investigation is left for future work.
Verifying Higher-order Programs. We have to care about the following points in modeling closures: (i) A closure that encloses mutable references can be encoded as a pair of the main function and the 'drop function' called when the closure is released; (ii) A closure that updates enclosed data can be encoded as a function that returns, with the main return value, the updated version of the closure; (iii) A closure that updates external data through enclosed mutable references can also be modeled by combination of (i) and (ii). Further investigation on verification of higher-order Rust programs is left for future work.
Libraries with Unsafe Code. Our translation does not use lifetime information; the correctness of our method is guaranteed by the nature of borrow. Whereas lifetimes are used for static check of the borrow discipline, many libraries in Rust (e.g. RefCell) provide a mechanism for dynamic ownership check.
We believe that such libraries with unsafe code can be verified for our method by a separation logic such as Iris [35,33], as RustBelt [32] does. A good news is that Iris has recently incorporated prophecy variables [34], which seems to fit well with our approach. This is an interesting topic for future work.
After the libraries are verified, we can turn to our method. For an easy example, Vec [58] can be represented simply as a functional array; a mutable/immutable slice &mut[T]/&[T] can be represented as an array of mutable/immutable references. For another example, to deal with RefCell [56], we pass around an array that maps a RefCell<T> address to data of type T equipped with an ownership counter; RefCell itself is modeled simply as an address. 2526 Importantly, at the very time we take a mutable reference a, a • from a ref-cell, the data at the array should be updated into a • . Using methods such as pointer analysis [61], we can possibly shrink the array.
Still, our method does not go quite well with memory leaks [52] caused for example by combination of RefCell and Rc [57], because they obfuscate the ownership release of mutable references. We think that use of Rc etc. should rather be restricted for smooth verification. Further investigation is needed.

Implementation and Evaluation
We report on the implementation of our verification tool and the preliminary experiments conducted with small benchmarks to confirm the effectiveness of our approach.

Implementation of RustHorn
We implemented a prototype verification tool RustHorn (available at https: //github.com/hopv/rust-horn) based on the ideas described above. The tool supports basic features of Rust supported in COR, including recursions and recursive types especially.
The implementation translates the MIR (Mid-level Intermediate Representation) [45,51] of a Rust program into CHCs quite straightforwardly. 27 Thanks to the nature of the translation, RustHorn can just rely on Rust's borrow check and forget about lifetimes. For efficiency, the predicate variables are constructed by the granularity of the vertices in the control-flow graph in MIR, unlike the perlabel construction of § 3.2. Also, assertions in functions are taken into account unlike the formalization in § 3.2.

Benchmarks and Experiments
To measure the performance of RustHorn and the existing CHC-based verifier SeaHorn [23], we conducted preliminary experiments with benchmarks listed in Table 1. Each benchmark program is designed so that the Rust and C versions match. Each benchmark instance consists of either one program or a pair of safe and unsafe programs that are very similar to each other. The benchmarks and experimental results are accessible at https://github.com/hopv/rust-horn.
The benchmarks in the groups simple and bmc were taken from SeaHorn (https://github.com/seahorn/seahorn/tree/master/test), with the Rust versions written by us. They have been chosen based on the following criteria: they (i) consist of only features supported by core Rust, (ii) follow Rust's ownership discipline, and (iii) are small enough to be amenable for manual translation from C to Rust.
The remaining six benchmark groups are built by us and consist of programs featuring mutable references. The groups inc-max, just-rec and linger-dec are based on the examples that have appeared in § 1 and § 3.4. The group swap-dec consists of programs that perform repeated involved updates via mutable references to mutable references. The groups lists and trees feature destructive updates on recursive data structures (lists and trees) via mutable references, with one interesting program of it explained in § 3.4.
We conducted experiments on a commodity laptop (2.6GHz Intel Core i7 MacBook Pro with 16GB RAM). First we translated each benchmark program by RustHorn and SeaHorn (version 0.1.0-rc3) [23] translate into CHCs in the SMT-LIB 2 format. Both RustHorn and SeaHorn generated CHCs sufficiently fast (about 0.1 second for each program). After that, we measured the time of CHC solving by Spacer [40] in Z3 (version 4.8.7) [69] and HoIce (version 1.8.1) [12,11] for the generated CHCs. SeaHorn's outputs were not accepted by HoIce, especially because SeaHorn generates CHCs with arrays. We also made modified versions for some of SeaHorn's CHC outputs, adding constraints on address freshness, to improve accuracy of representations and reduce false alarms. 28 Table 1 shows the results of the experiments.

Experimental Results
Interestingly, the combination of RustHorn and HoIce succeeded in verifying many programs with recursive data types (lists and trees), although it failed at difficult programs. 29 HoIce, unlike Spacer, can find models defined with primitive recursive functions for recursive data types. 30

RustHorn
SeaHorn False alarms of SeaHorn for the last six groups are mainly due to problematic approximation of SeaHorn for pointers and heap memories, as discussed in § 1.1. On the modified CHC outputs of SeaHorn, five false alarms were erased and four of them became successful. For the last four groups, unboundedly many memory cells can be allocated, which imposes a fundamental challenge for SeaHorn's array-based approach as discussed in § 1.1. 31 The combination of RustHorn and HoIce took a relatively long time or reported timeout for some programs, including unsafe ones, because HoIce is still an unstable tool compared to Spacer; in general, automated CHC solving can be rather unstable.

Related Work
CHC-based Verification of Pointer-Manipulating Programs. SeaHorn [23] is a representative existing tool for CHC-based verification of pointer-manipulating programs. It basically represents the heap memory as an array. Although some pointer analyses [24] are used to optimize the array representation of the heap, their approach suffers from the scalability problem discussed in §1.1, as confirmed by the experiments in § 4. Still, their approach is quite effective as automated verification, given that many real-world pointer-manipulating programs do not follow Rust-style ownership.
Another approach is taken by JayHorn [37,36], which translates Java programs (possibly using object pointers) to CHCs. They represent store invariants using special predicates pull and push. Although this allows faster reasoning about the heap than the array-based approach, it can suffer from more false alarms. We conducted a small experiment for JayHorn (0.6-alpha) on some of the benchmarks of § 4.2; unexpectedly, JayHorn reported 'UNKNOWN' (instead of 'SAFE' or 'UNSAFE') for even simple programs such as the programs of the instance unique-scalar in simple and the instance basic in inc-max.
Verification for Rust. Whereas we have presented the first CHC-based (fully automated) verification method specially designed for Rust-style ownership, there have been a number of studies on other types of verification for Rust.
RustBelt [32] aims to formally prove high-level safety properties for Rust libraries with unsafe internal implementation, using manual reasoning on the higher-order concurrent separation logic Iris [35,33] on the Coq Proof Assistant [15]. Although their framework is flexible, the automation of the reasoning on the framework is little discussed. The language design of our COR is affected by their formal calculus λ Rust .
Electrolysis [67] translates some subset of Rust into a purely functional programming language to manually verify functional correctness on Lean Theorem Prover [49]. Although it clears out pointers to get simple models like our approach, Electrolysis' applicable scope is quite limited, because it deals with mutable references by simple static tracking of addresses based on lenses [20], not 31 We also tried on Spacer JustRec+, the stack-pointer-based accurate representation of just_rec presented in § 1.1, but we got timeout of 180 seconds.
supporting even basic use cases such as dynamic selection of mutable references (e.g. take_max in § 1.2) [66], which our method can easily handle. Our approach covers all usages of pointers of the safe core of Rust as discussed in § 3. Some serial studies [27,3,17] conduct (semi-)automated verification on Rust programs using Viper [50], a verification platform based on separation logic with fractional ownership. This approach can to some extent deal with unsafe code [27] and type traits [17]. Astrauskas et al. [3] conduct semi-automated verification (manually providing pre/post-conditions and loop invariants) on many realistic examples. Because Viper is based on fractional ownership, however, their platforms have to use concrete indexing on the memory for programs like take_max/inc_max. In contrast, our idea leverages borrow-based ownership, and it can be applied also to semi-automated verification as suggested in § 3.5.
Some researches [65,4,44] employ bounded model checking on Rust programs, especially with unsafe code. Our method can be applied to bounded model checking as discussed in § 3.5.
Verification using Ownership. Ownership has been applied to a wide range of verification. It has been used for detecting race conditions on concurrent programs [8,64] and analyzing the safety of memory allocation [63]. Separation logic based on ownership is also studied well [7,50,35]. Some verification platforms [14,5,21] support simple ownership. However, most prior studies on ownershipbased verification are based on fractional or counting ownership. Verification under borrow-based ownership like Rust was little studied before our work.
Prophecy Variables. Our idea of taking a future value to represent a mutable reference is linked to the notion of prophecy variables [1,68,34]. Jung et al. [34] propose a new Hoare-style logic with prophecy variables. In their logic, prophecy variables are not copyable, which is analogous to uncopyability of mutable references in Rust. This logic can probably be used for generalizing our idea as suggested in § 3.5.

Conclusion
We have proposed a novel method for CHC-based program verification, which represents a mutable reference as a pair of values, the current value and the future value at the time of release. We have formalized the method for a core language of Rust and proved its correctness. We have implemented a prototype verification tool for a subset of Rust and confirmed the effectiveness of our approach. We believe that this study establishes the foundation of verification leveraging borrow-based ownership.