Abstract
Reduction to satisfiability of constrained Horn clauses (CHCs) is a widely studied approach to automated program verification. Current CHC-based methods, however, do not work very well for pointer-manipulating programs, especially those with dynamic memory allocation. This article presents a novel reduction of pointer-manipulating Rust programs into CHCs, which clears away pointers and memory states by leveraging Rust’s guarantees on permission. We formalize our reduction for a simplified core of Rust and prove its soundness and completeness. We have implemented a prototype verifier for a subset of Rust and confirmed the effectiveness of our method.
1 INTRODUCTION
Reduction to constrained Horn clauses (CHCs) is a widely studied approach to automated program verification of functional correctness [6, 24].
Technically, a CHC is a Horn clause [32] equipped with constraints, i.e., a formula of the form
, where each of the formulas
is either a constraint (e.g.,
) or an atomic formula of the form
, where
is a predicate variable and
are terms. Each free variable in a CHC is semantically universally quantified over some fixed sort (e.g.,
,
), which we usually omit for brevity. To aid understanding, we extend the notion of CHCs to allow disjunctions and existential quantifiers in the body (i.e., the right-hand side of the implication). Any CHC in this extended form can easily be translated into a conjunction of standard CHCs. A system of CHCs or a CHC system is a finite set of CHCs, which semantically means conjunction of the component CHCs.
CHC solving is the process of deciding whether a given system of CHCs has a solution, i.e., a valuation of predicate variables that makes all the CHCs in the system valid. We say that a system of CHCs is satisfiable if it has a solution. A variety of program verification problems can be naturally reduced to CHC solving [6, 24].
For example, let us consider the following C code that defines McCarthy’s 91 function:
Suppose we wish to verify that, for any
,
) returns 91 if the computation terminates, which is a kind of (partial) functional correctness of the function
is sugar for
):2
3

means that
) returns
(if it terminates). The first CHC in the system above describes the specification of 
A CHC solver provides a common infrastructure for a variety of programming languages and properties to be verified. There are efficient CHC solvers [13, 20, 31, 42] that can solve instances obtained from actual programs. For example, the above CHC system on
can be solved instantly by many CHC solvers, including Spacer [42] and HoIce [13]. As a consequence, many modern automated program verification tools [25, 27, 30, 39, 40, 66] reduce verification programs to CHCs and use CHC solvers.
Current CHC-based methods, however, do not work very well for pointer-manipulating programs, especially those with dynamic memory allocation, as we see in Section 1.1. In this article, we focus on programs written in the Rust programming language, which provides strong guarantees on permission or ownership of pointers. We present a novel reduction of Rust programs into CHCs, which clears away explicit representation of pointers and memory states for smooth verification, as we overview in Section 1.2.
1.1 Challenges in Verifying Pointer-manipulating Programs
A standard CHC-based approach [25] for pointer-manipulating programs represents the memory state as an array that maps each address to the data at the address, which is passed around as an argument of each predicate (cf. the store-passing style). In particular, SeaHorn [25], a standard CHC-based verification tool for C/C++, uses this array-based reduction.
For example, let us consider the following pointer-manipulating variation of the previous program:
It is reduced into the following system of CHCs by the array-based approach:

denotes the array made from
by replacing the value at index
with
, and
denotes the value of the array
at index
. Unlike
, the predicate
additionally takes two arrays
and
, which, respectively, represent the memory states before and after the call of
of
, representing the pointer argument
, obtained by updating the
th element of the memory-state array. In the else part,
represents 
However, the array-based approach has some shortcomings. Let us consider, for example, the following innocent-looking code (here,
Depending on the return value of
Suppose we wish to verify that

to denote binary operations that return a Boolean value. An underscore “
” denotes any fresh variable, which semantically means that we do not care the value.Unfortunately, the CHC system above is not satisfiable, which causes a false alarm of unsafety. This is because
may not necessarily be completely fresh in this formulation. Although
is made different from the argument
of the current call, it may coincide with
of some ancestor call. For example, we can derive contradiction from the CHCs above as follows; here,
denotes the array that maps the address 0 to
and 1 to
(and any other address to 0):

The simplest remedy would be to model a memory allocation strategy more faithfully. For example, one can also manage the stack pointer
, which represents the maximum address index that has been used for memory allocation so far.


, which states that the memory region of
, which has an unbounded size, remains unchanged.Finding quantified invariants is known to be very difficult in general, despite active studies on it [2, 21, 28, 38, 44]. The quantified formula needed above is fairly simple, but for more realistic programs, much more complex quantified formulas can be necessary to represent solutions. Therefore, current array-supporting CHC solvers usually fail in finding quantified invariants for CHC outputs of the existing method. Indeed, the Spacer CHC solver fails in solving even the above CHC system for
.
To avoid this kind of difficulty, many verification tools for pointer-manipulating programs analyze pointer usage to refine the memory model [25, 26, 39]. For example, for the verification problem of
1.2 Our Approach: Leverage Rust’s Guarantees on Permission
Rust [47, 58] is a systems programming language that supports low-level efficient memory operations like C/C++ and at the same time provides high-level safety guarantees using a permission-based type system. Despite its unique type system, Rust attains high productivity and has been widely used in industry recently [18, 51, 53].
This article proposes a novel approach to CHC-based automated verification of programs written in Rust. Our method clears away explicit representation of pointers and memory states by leveraging Rust’s permission guarantees.
Rust’s Permission Control. Before describing our approach, we briefly explain the permission control mechanism of Rust. Various styles of permission/ownership/capability have been introduced to control and reason about pointers in programming language design, program analysis, and verification [7, 8, 9, 14, 33, 68, 69]. The permission control mechanism of Rust’s type system, which we focus on, inherits a lot from existing approaches but also has some unique features.
In Rust, whenever an alias (or pointer) accesses an object, it needs permission for that. There are two types of permission in Rust: update permission, which allows both write and read access, and read permission, which allows only read access. At a high level, Rust’s permission control guarantees that whenever an alias can read from an object (with update or read permission) any other alias cannot write to the object (i.e., does not have update permission). In this article, we mainly focus only on update permission. For understanding our approach, it suffices to keep in mind that at most one alias can have update permission to each object.
For flexible permission control, Rust supports an operation called borrowing. In short, it is a temporary transfer of permission to a newly created pointer called a reference. A reference that borrows update or read permission is called a mutable or immutable reference, respectively.5 When borrowing is performed, the deadline is determined. The reference can use its permission only until this deadline.
As a simple example of Rust’s borrowing, let us consider the program below, which is also an interesting target of verification. It is written in C to aid understanding for a wide range of readers; the version in Rust is presented later in this subsection.
Figure 1 illustrates which alias/variable has the permission to update the integer objects of
Fig. 1. Values and aliases of and in executing inc_max(5, 3). Each row shows each alias/variable’s timeline of the update permission. A solid line expresses possession of the update permission. A bullet shows the time point when the borrowed update permission is given back. For example, b has the permission to update its integer object during (i) and (iv), but temporarily loses it during (ii) and (iii), because the pointer mb, created upon the function call of take_max(&a, &b), borrows b until the end of (iii).
Rust achieves the permission control described above using an elaborate type system. In particular, Rust uses the notion of lifetime to statically manage the deadline of each borrow. The type system of Rust will be discussed more in depth in Section 2.
The Key Idea of Our Reduction. Although Rust’s permission-based type system cleverly ensures memory safety, we wish to verify a more fine-grained property, functional correctness. For smooth verification, we leverage Rust’s permission guarantees to reduce Rust programs into CHCs without explicit representation of pointers and memory states. A naive approach would be to model each pointer as the value of its target object. However, if we do so, then the lender of a mutable borrow does not know the value of the borrowed object just after the deadline. For example, if we took this naive approach for the

The key idea of our method is to represent a mutable reference
consisting of the values of the target object of
and the value at the deadline of the borrow
. The trick is that we access some future information
, which is related to the notion of prophecy variable [1, 36, 73].
For example, our approach reduces the previous verification problem to the following system of CHCs:

, and similarly for
, because now we throw away
of
. The constraint
corresponds to
. After incrementing the value of
), the borrowed update permission of
. Now, the final check
, because the new values of
and
. The important point is that both the values
and
have been determined at this point; one is determined in
(by either
or
), and the other is determined in
by
. For example, in evaluating
and
, respectively. Although the verified program uses pointer manipulation, the system of CHCs obtained by our reduction is free of complex features like arrays, and thus can easily be solved by many CHC solvers.Also, our reduction turns the verification problem on

, which can easily be found by standard CHC solvers. Remarkably, unlike the array-based reduction discussed in Section 1.1, the CHC system output by our reduction is free of arrays and its solution does not require quantifiers.Our reduction can be flexibly applied to various features of Rust, such as reborrowing, nested references, and recursive data types. Our approach can reduce a substantial subset of Rust to CHCs in a fairly uniform manner. In Section 3.4, we present some advanced examples of our verification method. Example 5 presented there features a Rust program that handles a mutable reference to a singly linked list, where our reduction experimentally succeeded in automated verification of a fairly challenging property.
Formalizing Our Reduction. Later, in Section 2 and Section 3, we formalize (a subset of) Rust and our reduction. Here, we provide an informal overview of the formalization.
As a running example, we reuse the
The type
For formalization, we use a normalized program like below, where each function body is decomposed into a set of simple instructions labeled by program points. This is also similar to an intermediate representation used by the Rust compiler, which is called MIR (mid-level intermediate representation) [54]. This representation is convenient for the formalization of the type system and the reduction to CHCs.
In the function
Now, we describe how Rust’s type system works. The type system of Rust gives some permission to a pointer, which changes in the process of execution. As a result, the type system is flow-sensitive and assigns a different type context to each program point. For example, the following is the function
The variable
Now, we sketch the formalization of our reduction to CHCs. Our reduction of Rust programs to CHCs is type-directed, in that it leverages the type assigned to each variable by Rust’s type system to decide the model of the variable. For example, a reference of the type
,
, and
represent the program points

models the relation between the values of
) and the return value of
). At
. To ensure that what we took as the final value
agrees with the actual final value, we add the constraint
here. The function 
for the final target value, i.e., the value of the borrowed integer object at the deadline of the borrow. We model the mutable reference
. Now, we can simply model
here, because the type system ensures that
be the value of
. At
of
when we performed the borrow at Contributions. We have developed a novel method of reducing Rust programs to CHCs that leverages permission guarantees provided by Rust’s type system, as introduced above. We have formalized our reduction of Rust programs to CHCs on a newly formalized core language of Rust and proved the soundness and completeness of this reduction. We have also implemented a prototype automated verifier for the core of Rust based on the idea and confirmed the effectiveness of our approach through preliminary experiments. The core language we support includes particularly reborrow and recursive types. Our approach has succeeded in automated verification of some non-trivial properties of programs with destructive update via pointers on recursive data types such as lists and trees.
This article is a revised and extended version of the same-titled paper published in the proceedings of ESOP 2020 [49]. Compared with the conference version, in this article we have augmented explanations, polished the formalization, added the proof of the main theorem, and expanded the experiments.
Structure of the Rest of the Article. In Section 2, we provide a formalized core of Rust. In Section 3, we formalize our reduction from programs to CHCs and outline the proof of its soundness and completeness; we also introduce advanced examples on our reduction and discuss extension of our method. In Section 4, we give the complete proof of the soundness and completeness of our reduction. In Section 5, we report on the implementation and the experimental results. In Section 6 we discuss related work, and in Section 7 we conclude the article.
2 FORMALIZATION OF RUST: CALCULUS OF OWNERSHIP AND REFERENCE
Now, we present our formalization of the core of Rust, which we call Calculus of Ownership and Reference (COR). It is a typed procedural calculus with a lifetime-based permission control system in the style of Rust. Its design is inspired by
[34].
The calculus is carefully designed to simplify our reduction of Rust programs into CHCs (presented in Section 3) and the proof of its soundness and completeness (given in Section 4). For simplicity, we impose some restrictions in this calculus. Lifetime information, type information, and data releases should be explicitly annotated in the program. Each function body should be written as a set of primitive commands connected with goto jumps. Also, each variable should be a pointer. Still, the calculus covers various features of Rust, as we see later.
In Section 2.1 we introduce the syntax of this calculus. Then we present the type system in Section 2.2 and the operational semantics in Section 2.3.
Notation. An arrow above a variable denotes a sequence (e.g.,
). It can be used in a composite form; for example,
denotes
. The empty sequence can be denoted by
. Also, the length of a sequence can be specified with a superscript (e.g.,
). We sometimes omit commas used as separators in a sequence.
The set operation
(or more generally
) denotes the disjoint union, i.e., the union
(or
) defined only if the arguments are disjoint. The set operation
denotes the proper set difference, i.e., the set difference
that is defined only if
.
2.1 Syntax
The syntax of this calculus is as follows:

for a non-mutable-reference pointer kind, i.e.,
or
.Program, Function, and Statement. A program
is a sequence of function definitions
. For simplicity, here we do not specify the entry function (i.e., the
A function definition
consists of the name
, the signature
, and the body
. A function is parametrized over constrained lifetime parameters, but for simplicity our calculus does not support polymorphism over types, like
[34]. For simplicity, the input/output types of a function are restricted to pointer types, i.e., types of the form
. In a function signature, we simplify
to
and omit
.
A label
is a program point that contains a statement
, which performs some simple command and jumps to some label or return from the function. Later, this style with labels and unstructured control flow simplifies the formalization of our reduction in Section 3.2. We require that the function body contains the entry point label
. Also, we require that every label in the function is syntactically reachable from the label
(i.e., reachable in the directed graph whose vertices are the labels and whose edges are
jumps); this restriction is for uniqueness of typing, as we see in Section 2.2. There are three types of statements. A statement
performs the instruction
and jump to the label
. A statement
returns from the function with the variable
. A statement
conditionally branches to a label
by the tag of the variant
and take a pointer
to the body of the variant.
Instruction. An instruction
performs a simple command. We have various types of instructions, whose meanings are briefly explained above.
For most kinds of instructions, the inputs are consumed. Only for the copy instruction
and the operation instruction
, the inputs are not consumed.
The swap instruction
takes pointers
and
and swaps the target objects of
and
. An unusual design of this calculus is that it uses swap instead of assignment for the primitive for destructive update. Assignment is a bit trickier than swap in terms of resource management, because when some object is assigned to a variable, the old object of the variable is implicitly released. We can still express assignment combining a number of instructions. For example, if we have a pointer
to an integer and wish to assign its integer value to a mutable reference
, then we can do that by the following sequential execution:
.
Pointer, Borrow, and Lifetime. A pointer can be either an owning pointer or a reference. An owning pointer models Rust’s box pointer
to denote lifetimes. A lifetime variable can be either (i) a lifetime parameter taken by a function or (ii) a local lifetime introduced within a function.
By the instruction
, we mutably borrow
under the lifetime
and obtain a mutable reference
. Here,
can be either an (unfrozen) owning pointer or mutable reference (when
is a mutable reference, this operation is called a reborrow). Also, by the instruction
, we can weaken a mutable reference
into an immutable reference.
We have three lifetime-related ghost instructions. The instruction
introduces a local lifetime
. The instruction
sets a local lifetime
to the current moment and eliminates it. The instruction
promises that
comes earlier than
in the process of computation.
We can subdivide pointers in various ways. The instruction
splits a pointer
to a pair into pointers
to each element of the pair. The statement
turns a pointer
to a variant into a pointer
to the body object of the variant, discarding the permission to the tag of the variant. The instruction
takes a pointer to a pointer
and returns a pointer
to the inner target object of
, which can also be regarded as a pointer subdivision.
Type. In the calculus, various forms of types
are supported, whose meanings are briefly explained above. A pointer type has the form
, where
is called the pointer kind. The type of an owning pointer is
, which corresponds to Rust’s
and
, which correspond to
We say that a type is complete if it satisfies the following: Every occurrence of a type variable in
should be bound by the recursive binder
and guarded by a pointer constructor
inside the binder. A type
that appears in a program (not just as a substructure of some type) should be complete. For example, the singly linked integer list type
is complete, whereas
(without
) is not complete.
(Expressivity and Limitations).
Although older versions of Rust determined lifetimes just by lexical scopes, the current versions of Rust have a mechanism that overcomes that restriction, which is called non-lexical lifetime [57]. The Rust borrow checker uses a flow sensitive analysis to determine the lifetimes of references and allows many flexible borrow patterns. Our calculus can the core behavior of non-lexical lifetimes. The point is that, even under non-lexical lifetimes, the set of program points where a borrow is active forms a continuous range.8
A major limitation of our calculus is that it does not support unsafe code blocks and also lacks type traits and closures. How to overcome them is discussed later in Section 3.5. Another limitation of COR is that, unlike Rust and
, we do not have a primitive for directly modifying or borrowing a substructure of a variable (e.g., the first element of a pair). Still, combining some operations, we can modify or borrow a substructure by borrowing the whole variable first and then subdividing pointers (e.g.,
). Nevertheless, this borrow-and-subdivide strategy cannot fully support some advanced borrow patterns like
(Program).
The Rust program with
The first letter of a variable name indicates the pointer kind (
for an owning pointer and
for a mutable reference). We swapped the two branches of the
statement in
to make the order the same as if-else branching. We use shorthand for sequential execution; for example,
denotes for two labeled statements
and
.
Here, we have more program points than in the informal description of Section 1.2. Each time we release a variable
, we need an instruction
, which simplifies formalization. Also, we use ghost instructions
and
to manage lifetimes.
2.2 Type System
The type system assigns a whole context
to each label (program point) of each function in a program. A whole context is a pair of a data context
and a lifetime context
. A data context manages the information on data variables and a lifetime context manages the information on lifetime variables. These notions are explained in more detail soon.
Context. A data context
is a finite set of items of the form
, where
should be a complete pointer type and
(which we call activeness) is of the form either “
” (active; i.e., the permission is not borrowed) or “
” (frozen until lifetime
; i.e., the permission is borrowed until
). For simplicity, we do not consider the situation where only the write permission of a variable is frozen. When a variable
is tagged
, we cannot read or update the target object of
through
. We usually abbreviate
to
. A data context should not contain two items on the same variable.
A lifetime context
is a finite preordered set of lifetime variables, where
is the underlying set and
is the preorder. We write
and
to refer to
and
.
Finally, a whole context
is a pair of a data context
and a lifetime context
such that every lifetime variable in
is contained in
.
Auxiliary Judgments. The subtyping judgment is of the form
, where
is a finite set of assumptions of the form
, which are used for coinductive reasoning on recursive types. The common subtyping judgment
is defined as
, where we have no assumptions. The full subtyping judgment
is defined by the following rules:
We have two rules for judging
, Subtype-Rec-Covar and Subtype-Rec-Invar, which admit coinductive reasoning of a simple form. The former Subtype-Rec-Covar is provided for the case where
appears covariantly in
. For example, the judgment
holds when
holds. The latter Subtype-Rec-Invar is provided for the case where
appears invariantly in
(e.g.,
is under a mutable reference). For example, the judgment
holds when both
and
hold.
We also introduce the following copyability judgment
:

means that the owning pointer
and mutable reference
constructors do not occur in
except under the immutable reference constructor
.Typing Judgment for Instructions. The instruction typing judgment is of the form
. It means that, by running the instruction
in
under the whole context
, a renewed whole context
is obtained. Below are the complete rules for the judgment. For brevity, we omit
and
here (except in
) because they are always fixed. Also, we additionally require that every variable can be used at most once in each instruction.
For most instructions, the input variables are consumed and thus do not appear in the output type context. The typing rules above are defined so an instruction has a unique type; more precisely, for any
and
, there exists at most one whole context
satisfying
.
The instruction
mutably borrows an (unfrozen) owning pointer or mutable reference
under the lifetime
(Type-Inst-Mutbor). After the borrow,
gets frozen until
, being registered to the type context as
. We have a precondition that
is a local variable that is outlived by any lifetime in
. Because
is local, the borrow ends within a function and the created reference does not leak outside the function.
The instruction
removes
from the type context (Type-Inst-Drop). The precondition on
says that when the dropped variable is an owning pointer its target type should be copyable. This precondition does not weaken the expressivity, because we can always satisfy this precondition by repeating subdivision of pointers beforehand (by dereference
, pair destruction
and variant destruction
). Thanks to the precondition, we do not need nested releases of owning pointers in the operational semantics and can avoid adding complicated constraints on mutable references in our reduction.
The instruction
weakens a mutable reference
into an immutable reference (Type-Inst-Immut). Technically, this is a variant of
on a mutable reference
where we retain an immutable reference.
The instruction
destructively updates the targets of
and
, swapping the target objects (Type-Inst-Swap). To reduce the number of patterns to consider, we restrict
to a mutable reference, whereas we let
be either an owning pointer or a mutable reference. We do not lose expressivity by this; swap between two owning pointer variables can be performed by swapping just the names of the two variables.
The instruction
consumes the variable
and allocates its object to get an owning pointer
(Type-Inst-Own).
The instruction
dereferences a pointer to a pointer (Type-Inst-Deref). The kind of the output pointer type is determined from the outer and inner pointer kinds
of the input, by an auxiliary operation
. The kind
is an identity on this operation. When we compose reference pointer kinds
, the output is
, where
is the weakest of
and
. Here, we can just take the lifetime of the outer reference
, which is safe, because when we performed a borrow we ensured that the lifetime is outlived by any lifetimes in the target.
The instruction
copies the target object of
typed
and wraps it into an owning variable typed
(Type-Inst-Copy). We have a precondition
, which prevents us from copying mutable references and owning pointers.
The instruction
modifies the type
of a variable
into another type
(Type-Inst-As). We have a precondition
on subtyping.
The instruction
calls a function
with lifetime arguments
and data arguments
. The inputs
are consumed by the function
and thus do not appear in the output type context.
The instruction
introduces a new local lifetime
(Type-Inst-Intro). We promise that the new local lifetime
is outlived by any lifetime parameters, because it will be eliminated in the current function. The instruction
eliminates the local lifetime
and reactivates every variable frozen under
in the data context (Type-Inst-Now). As a precondition, we check that
is strictly the least element in the input local lifetime context, i.e.,
does not hold for any lifetime variable
other than
. The instruction
adds a promise on the elimination order of local lifetimes (Type-Inst-Lftin). This promise is registered to the lifetime context and can be used for subtyping.
The instructions
,
, and
, respectively, newly allocate a constant, the result of an integer operation, or a non-deterministic integer to get an owning pointer to the result
(Type-Inst-Const, Type-Inst-Op, Type-Inst-Rand). Note that the inputs
are not consumed for the instruction
.
The instructions
and
, respectively, allocate a variant object or a pair by consuming the input owning pointer(s) (Type-Inst-Inj, Type-Inst-Pair). The instruction
splits a pointer to a pair into pointers to each element of the pair, retaining the pointer kind (Type-Inst-Pairdestr). For example, by splitting a mutable reference to a pair, we get mutable references to each element of the pair.
Typing Judgment for Statements. The statement typing judgment is of the form
. It means that the statement
in the function
in the program
under the whole context
jumps to a label
with a whole context
or safely returns from the current function call. The following are the rules for the judgment (we omit here
and
, except in
):

The rule for the instruction statement
simply uses the typing judgment for
. In the rule for the return statement
, we require that there remain no extra variables and no local lifetimes. In the rule for the match statement, we check both branches. The input variable
of the type
is consumed and in the
branch we get a new variable
of the type
.
We use a meta-variable
to denote a finite map from labels to whole contexts. The typing judgment for statements is defined so for any
and
, there exists at most one whole context assignment
such that
holds. This uniqueness can easily be proved, using the typing uniqueness on instructions.
Typing Judgment for Functions. The typing judgment for functions is
, where
denotes the set of labels in
. In short, the judgment assigns a whole context to each label in the function. The judgment is defined by the following rule:

is constructed from the function signature (the second and third preconditions) and then the contexts for other labels are examined (the fourth precondition). The whole context for each label can be determined in the order of the distance from the label
in the directed graph by the
jumps. Therefore, a typing derivation on a function is unique. That is, for any
and
, there exists at most one whole context assignment
such that
holds.Typing Judgment for Programs. The typing judgment for programs is
, where
is the set of program points
in
(
can be any function in the program
and
can be any label in the function
). In short, the judgment assigns a whole context to each program point in the program. The judgment is defined simply by the following rule:

, there exists at most one whole context assignment
such that
holds. We say that a program
is well typed when it has a whole context assignment in this judgment.(Soundness of the Type System).
This type system is sound, but to fully state the theorem, we must also formally describe the safety condition on concrete configurations. The safety condition is introduced later in Section 4.2. The progress and preservation properties of the safety condition over well-typed programs are then proved (Proposition 3 and Corollary 5).
2.3 Operational Semantics
The following are the basic concepts of the operational semantics:

, which indicates the program point (the function and the label). Each non-top stack frame also has “
,” which specifies the variable that will receive the return value of the function call of the stack frame just above. We also use a meta-variable
for a sequence of non-top stack frames
.We also define the type size
, which represents how many memory cells the type
takes at the outermost level, as follows:

, the definition above determines the type size for every complete type (defined in Section 2.1), in which occurrences of type variables are restricted.The operational semantics is characterized by the one-step transition judgment
and the termination judgment
. We assume that the program
is well-typed. The type information we use here is quite limited; we use the type size to know how many memory cells are required and also check whether the pointer kind of a variable is
or
. The following are the complete rules for the two judgments. We omit
here.
At each step, we remove invalidated variables from the concrete stack frame
, just as we did in the type system.
On a function call, we add a new stack frame to the head of the stack (Step-Call). The initial label is set to the entry point
. When we return from a function, we remove the head stack frame from the stack and continue computation if we have remaining stack frames (Step-Return). If the current stack frame is the only stack frame in the stack, then the computation ends by the rule End-Return (actually, this is the only rule for the judgment
).
In general, instructions of the form
allocate memory cells for the newly created owning pointer
. For example, an instruction
allocates a memory cell for the integer data
(Step-Const).
Some operations behave differently for depending on whether the input is an owning pointer or a reference. The instruction
deallocates the target object from the heap if
is an owning pointer (Step-Drop-Own) but does not perform deallocation if
is a reference (Step-Drop-Ref). The instruction
performs deallocation of the target memory cell of
if
is an owning pointer (Step-Deref-Own) but does not otherwise (Step-Deref-Ref). Similarly, the match statement
deallocates the memory cells for the index and the padding if
is an ownership pointer (Step-Match-Own) but does not otherwise (Step-Match-Ref).
When we create a variant object by the instruction
(Type-Inst-Inj), we allocate a padding by zeroes if
has a smaller size than
does, which makes the size of the variant object
in total.
(Execution in the Operational Semantics).
The following is an execution sequence in the operational semantics for the program presented in Example 1. The inputs to
and
are set to 5 and 3. The symbols
represent some mutually distinct addresses.
3 OUR REDUCTION FROM RUST PROGRAMS TO CHCS
Now, we formalize our reduction from Rust programs to CHCs, discussed in Section 1 as a reduction from a program in our calculus COR to a CHC system, which is guaranteed to precisely characterize the input-output relation of each function in the program. We first define the first-order multi-sorted logic for CHCs in Section 3.1. We then formally describe our reduction in Section 3.2. We formalize its soundness and completeness and outline the proof of that in Section 3.3 (we present the complete proof in Section 4). Also, we examine effectiveness of our approach with advanced examples in Section 3.4 and discuss various topics about our idea in Section 3.5.
3.1 Multi-sorted Logic for CHCs
To begin with, we introduce a first-order multi-sorted logic for CHCs.
Syntax. The following is the syntax of the logic:
Also, a CHC system is defined as a pair
of a finite set of CHCs
and a finite map
from a predicate variable to a tuple of sorts (denoted by
), specifying the sorts of the arguments for the predicate variable. Unlike the informal description in Section 1, we explicitly specify the sort information
. For simplicity, we often omit the universal quantifier
of CHCs.
CHCs in this logic have a fairly restricted form, in comparison to informal CHCs used in Section 1. Every formula
should be of form
and we do not have a category for constraints like
. Also, the head of each CHC should be of form
, where
is a pattern, consisting only of variables and constructors, not having operators. Even in this restriction, we can express various predicates using the idea of pattern matching. For example, the equality relation
on a sort
can be introduced in a CHC system by adding the following rule on
:
(precisely speaking,
is the equality relation in the least solution of the CHC system). This restriction helps to simplify our proof of the soundness and completeness later in Section 4.
In this logic, we have two special data types, a box container, whose value is
and whose sort is
, and a mut container, whose value is
and whose sort is
. In our reduction, owning pointers and immutable references are modeled as a box container and mutable references are modeled as a mut container.
Sort System. The sort-giving judgment
(the term
has the sort
under
) is defined as follows. Here,
is a finite map from variables to sorts.

as
.We introduce the well-sortedness judgments for a CHC system
, for a CHC
and for a formula
and give them the following rules:

Semantics. An evaluation
is a finite map from variables to values. A predicate structure
is a finite map from predicate variables to predicates on values of some fixed sorts.
We define the sort-giving judgment on an evaluation
as follows:

The interpretation of a term
into a value over an evaluation
, denoted
, is defined as follows:

, the interpretation is defined for every well-sorted term (i.e.,
is defined if
holds for some
satisfying
and some
), which follows from straightforward induction.The validity of a CHC
and the validity of a formula
are defined as follows:

is defined as follows:

We say that
is a solution to
if
holds. Every well-sorted CHC system
has a least solution with respect to the point-wise ordering, which can be proved based on the standard discussion [74]. We write the least solution of
as
.
3.2 Our Reduction from Programs to CHCs
Now, we formalize our reduction of Rust programs to CHC systems. We define the CHC representation
of a well-typed COR program
, which is a CHC system that represents the input-output relations of the functions in
.
We assign a predicate variable
to each program point
(e.g., each label
in each function
). Roughly speaking, the predicate
represents the input-output relation of the continuation from the program point
in the function
, where the inputs are the values of the local variables at
and the output is the return value of
.9 For each
, we add one or two CHCs to the resulting CHC system, which represent the operation of the statement at
. As explained in Section 1.2, in the resulting CHCs, we represent a mutable reference as
, a pair of the current target value
and the final target value
, and do not explicitly model addresses and memory states.
Roughly speaking, our CHC representation is designed so its least solution
satisfies the following property: for any values
, the validity
holds if and only if a function call
can return
in the program. Actually, since such values should be extracted from the heap memory in the operational semantics, the actual definition is a bit more involved. The formal description and the proof of this expected property are presented later in Section 3.3.
Preliminaries. We introduce some preliminary definitions and notions.
The sort corresponding to the type
,
, is defined as follows. Note that the information on lifetimes is all stripped off.

We assume some fixed linear order on data variables and enumerate the elements of a data context, a stack frame, and so on, in this order. Also, we fix a well-typed program
as an implicit parameter.
The predicate signature of
, denoted by
, is defined as
, where
is the data context at
and
is the return type of
.
Our Reduction. Now, we fully define our reduction.
In our reduction, for each program point
, we generate a CHC or a pair of CHCs with the head of the form
, which models the computation performed by the statement. Here,
is a special variable that represents the result of the function (we put this variable at the last in the fixed linear order). We add just one CHC for a statement of the form
or
and we add two CHCs for a match statement
(recall that we do not allow here disjunction in the body of each CHC, unlike informal description in Section 1).
For example, let us consider a labeled statement
in a function
, which allocates an integer memory cell. The CHC we generate for the statement is as follows, letting
be the local variables in
(we omit the universal quantifier):

represents the value of
, i.e., the newly created owning pointer that has the integer data 3. This CHC can be read as a rewriting rule from left to right: The statement creates a new owning pointer
and passes it with the carryover variables
to the next statement at
.For another example, let us consider a labeled statement
in
, which performs a mutable borrow. Assume that the data context at
is
, which sets the data context at
to
. The CHC we generate for this statement is as follows:

For convenience, we introduce the notation
for the pattern formula
, where
are the local variables at
. (Note that we reuse data variables
of COR as logic variables.) For example, the CHC of the previous example can be written as follows:

Now, we define the CHC representation
of a well-typed program
as follows:

is one CHC or a pair of CHCs we generate for the labeled statement
in
in
, which is defined by the following rules: For simplicity, we omit here universal quantifiers and
. For some statements, depending on the pointer kinds of the input variables, we generate fairly different CHCs.
The important rule is Chc-Stmt-Mutbor, the rule for a mutable (re)borrow. The first and second cases, respectively, correspond to a borrow and a reborrow. In both cases, we take a fresh variable
that represents the value of the target object at the deadline of the (re)borrow. Letting
be the current target value, we model the created mutable reference as
, the pair of the current and final target values. After the (re)borrow, the (current) target value of the lender is set to
, which is valid, because the lender gets frozen in the type system. In the case of reborrow, letting
be the original value of the lender mutable reference, the new value of the lender is set to
, where the lender’s own final target value is retained.
When a mutable reference
is released (the second case of Chc-Stmt-Drop), the final target value of
is set to the current target value
. We use pattern matching here instead of equality, unlike informal explanation in Section 1.2. A similar thing happens when we weaken a mutable reference into an immutable reference (Chc-Stmt-Immut).
The rule for dereference
Chc-Stmt-Deref is tricky. This instruction turns a pointer to a pointer
into a pointer to the inner target object
. We have six cases here, depending on the type information, or the pointer kinds of the outer and inner pointers. Let us see each case more closely. (i) An owning pointer to a pointer is simply dereferenced into the inner pointer. (ii, iii) An immutable reference to a pointer is dereferenced into an immutable reference to the inner object. If the inner pointer of
is a mutable reference, then we discard the final target value. (iv) Dereference of a mutable reference to an owning pointer can be regarded as a subdivision of the mutable reference. (v) Dereference of a mutable reference to an immutable reference yields an immutable reference, which weakens the update permission of the inner mutable reference into the read permission. Therefore, we constrain the value of
in a manner similar to Chc-Stmt-Immut.10 (vi) Dereference of a mutable reference to a mutable reference can be regarded as a subdivision of the outer mutable reference, as in the fourth case. At the low level, the address of the outer mutable reference is fixed to the current one by this dereference. Therefore, in our CHC representation, we fix the final target value of the inner mutable reference to the current one
. A subtle point is that, for a mutable reference to a mutable reference, we can destructively update the address of the inner mutable reference (we can see an example of such update later in Section 5.2, in the function named
In the second case of Chc-Stmt-Match and the second case of Chc-Stmt-Pair-Split, we perform subdivision of a mutable reference. Both the current and final target values are subdivided by the operations. For Chc-Stmt-Pair-Split, when the mutable reference
turns into
,
loses the update permission to the tag of the variant.
For a function call, the CHC body has a conjunction (Chc-Stmt-Call). Recall that, in the operational semantics, we add one stack frame when we call a function. The conjunction in the CHC body actually introduces a behavior analogous to the stack frame addition in an algorithm called resolution, as seen in Section 4.1 and Section 4.3.
When we return from a function (Chc-Stmt-Return), we set the return value
for the
statement. Again, instead of writing
, we use pattern matching to constrain
to be equal to
.
(CHC Representation).
We present below the CHC representation of the program presented in Example 1, consisting of
and
. We give variables the following fixed linear order:
.

3.3 Soundness and Completeness of Our Reduction
Now, we formally state the soundness and completeness of our reduction and outline the proof of it. The complete proof is presented in Section 4.
To formally state the soundness and completeness of our CHC representation with respect to the actual behavior in the operational semantics, we first define the judgment to extract structured values from the heap memory and also check the safety condition on the heap memory based on ownership. Then, using that, we define the OS-based model
of a function
, which is a predicate that describes the input-output relation of the function
with respect to its behavior in the operational semantics. Here, for simplicity,
is restricted to what we call a simple function, i.e., a function whose input/output types do not contain mutable references. Finally, we state the soundness and completeness theorem and outline the proof of it.
Notation. We use
(instead of
) for multisets.
(or more generally
) denotes the multiset sum. For example,
.
Basic Extraction-examination Judgment. We build a mechanism for extracting structured values from the heap memory, which is a finite map from addresses to integers. Also, we formally describe the safety condition on the heap memory with respect to the type information, which is designed to ensure invariants on permission. Because we currently target only simple functions, we can ignore mutable references and ignore frozen variables. (Later, in Section 4, we extend the judgments to actually handle them.)
We first introduce the notion of weak abstract configuration.

is an item either of the form
or
, representing the permission on the memory access. A memory footprint
is a multiset of items of form
.Now, we introduce the two basic judgments for structurally extracting the value from the heap memory,
and
. The former structurally extracts from the heap memory
the pointer object typed
of the address
as a value
, yielding a memory footprint
, under the access mode
. The latter is similar to the former but extracts the object typed
stored at the address
. The two judgments are mutually inductively defined by the following rules:
For example, the following judgments hold (
can be any addresses):
Next, we introduce the judgment
for extracting values from a concrete stack frame as a weak abstract stack frame, which is defined by the following rule:

for extracting values from a concrete configuration as a weak abstract configuration, which is defined by the following rule:

We introduce the safety judgment on a memory footprint
. It is defined through an auxiliary judgment
as follows:

Finally, we introduce the basic extraction-examination judgment
. It is the judgment for extracting a weak abstract configuration from a concrete configuration and also examining the safety and is defined by the following rule:

OS-based Model. Now, we define the OS-based model
of each simple function
in a program
. It is the predicate that describes the input-output relation of the function
with respect to its behavior in the operational semantics (abbreviated as OS). We say that a function is simple when it does not take mutable references in the input and output types. The OS-based model
is defined as the predicate on values of sorts
where
, given by the following rule:

Soundness and Completeness Theorem. Finally, the soundness and completeness of our reduction is simply stated as follows:
For any well-typed program
and any function
in
,
is equivalent to
.
The complete proof of the theorem is presented in Section 4. We outline the proof below.
We first introduce a deduction algorithm on CHCs called SLDC resolution, which is a variant of SLD resolution [43]. We show that SLDC resolution is complete with respect to the least model of the CHC system (Lemma 2).
Next, we extend the basic extraction-examination judgment to accept mutable references and frozen variables. The key idea is to model the final target value
of each mutable reference as a syntactic variable in logic, which is semantically universally quantified. Roughly speaking, a mutable reference is modeled as a pair of the current target value and a unique logic variable, and a frozen variable is modeled as a value with some borrowed parts remaining to be logic variables.
Finally, we complete the proof by establishing a bisimulation between the operational semantics and SLDC resolution under our CHC representation (Theorem 4). A key point is that, at the moment we release a mutable reference, we specialize the logic variable for the mutable reference.□
3.4 Advanced Examples
Here, we present two advanced examples of verifying pointer-manipulating Rust programs by our reduction. For readability, we write CHCs again in an informal style like Section 1.
Let us consider the following Rust program, which is a variant of
Unlike
The Rust program above can be expressed in COR as follows:
(which jumps to either
or
), the decrement instruction
, the true-value taking instruction
, the Boolean conjunction instruction
, and the multiple-variable release instruction
. These additional features can be expressed by composition of original features. Also, we omitted some labels.
Suppose we wish to verify that
in Section 1.1, a predicate taking the memory states
and the stack pointer
, then we have to discover the quantified invariant:
. In contrast, our approach reduces this verification problem to the following CHCs:


Combined with recursive data types, our method turns out to be more powerful. Let us consider the following Rust program that features a singly linked list:
This program handles a singly linked list type
The Rust program above can be expressed in COR as follows:

is sugar for the recursive type
. We have omitted the implementation of the function
, which releases the data of a list of integers. Also, we have admitted the no-op jump statement
, the immutable borrow instruction
, and the increment instruction
.Suppose we wish to verify that

denotes the nil list and
denotes the cons list made of the head
and the tail
. In our formal logic introduced in Section 3.1, they are, respectively, expressed as
and
. An important technique used above is subdivision of a mutable reference performed in the function
.We can give this CHC system a very simple solution, using an auxiliary recursive function
defined by
and
.

; specifically, we can check the validity of each CHC just by unfolding
at most once. Notably, we do not need auxiliary notions like index access on lists to express the solution, which makes our approach scalable to richer recursive data types like trees.Notably, in our experiments reported in Section 5, the example presented above was fully automatically and promptly verified by our prototype verifier RustHorn, using HoIce [12, 13] as the back-end CHC solver. Our verifier also successfully verified the variant of this example for trees instead of lists, which indicates high scalability of our approach for recursive data types. Note still that the CHC solver HoIce adopts a rather heuristic approach to find solutions that handle recursive functions over recursive data types (details are presented in Reference [12]). For example, for the CHCs for
by analyzing the CHCs for the predicate variable
. It remains to be seen how well our approach verifies Rust programs with mutable references and recursive data types in general, given also that CHC solving techniques are still evolving.
3.5 Discussions
We discuss here various topics about our idea.
Combining Our Reduction with Various Verification Techniques. Our idea can also be expressed as a reduction 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 model the target value
at the end of the mutable borrow, we just randomly guess the value with non-determinism. At the time we actually release a mutable reference, we just check
For example,
Here, the bindings
This representation allows us to use various verification techniques for Rust programs, including model checking (higher-order, temporal, bounded, etc.), semi-automated verification (e.g., in Boogie [50]), and verification in proof assistants (e.g., Coq [16]). The verified properties can be not only partial correctness but also total correctness and liveness. Also, our reduction can be used with various bug finding techniques such as symbolic testing (because we get an equivalent representation of the Rust program, as Theorem 1 states). Further investigation in these directions is needed.
Verifying Higher-order Programs. Rust supports closures, internally encoding them as the tuple of the function pointer and the captured objects, creating a fresh internal type for each closure. Our reduction can support such closures simply by desugaring them as the captured objects.
As an advanced feature, Rust support trait objects, which performs dynamic dispatch. Using a trait object, Rust can use a boxed closure, which is required to get the full expressivity of higher-order programming. If we use rich verification frameworks like higher-order CHCs [11], then our reduction can still model Rust programs that operate boxed closures, using some tricks. To model a closure that captures mutable references, we can equip the model of a closure with the “drop predicate,” which expresses the constraint that we should add when we release the closure. To model a closure that updates objects it captures, we can equip the output of the closure the updated version of the closure (using some recursive type). We need further investigation on verifying Rust programs with boxed closures and trait objects.
Libraries with Unsafe Code. Although the subset discussed earlier is quite limited, we can easily apply our reduction to some Rust libraries. For example, the vector (dynamically allocated array) type
However, Rust libraries such as
from a
. This precisely models
Even when we find a model for some Rust library, verifying the implementation of the library itself can be tough, since it usually relies a lot on unsafe code, which is Rust code without static permission control. RustBelt [34] mechanically proved (in the Coq proof assistant) memory safety of well-typed Rust programs supporting various Rust libraries, including those with interior mutability. We discuss this work more in Section 6. Matsushita [48] discusses how to extend RustBelt to verify functional correctness with respect to our reduction, but the proof is not mechanized yet.
One caveat about our verification method is that it loses completeness in the presence of memory leaks. A memory leak [55] is an act to throw away an object without successful cleanup, such as memory deallocation and lock release. Although a basic subset of Rust (including the features supported in COR) does not allow memory leaks, advanced libraries in Rust can cause memory leaks. For example, when we build a cyclic graph using interior mutability by
4 PROOF OF THE SOUNDNESS AND COMPLETENESS OF OUR REDUCTION
In this section, we give the complete proof of Theorem 1 stated in Section 3.3, the soundness and completeness of our reduction.
Clearly, the tricky point is that our model of a mutable reference
has future information, namely, the final target value
. Our proof gets around this by keeping all possibilities about the future and narrowing them in the course of execution. A key ingredient is resolution, a deduction algorithm over CHCs that can handle syntactic variables that are universally quantified over values. This is nice for encoding future possibilities. Our proof goes by building a bisimulation between execution in Rust and resolution over the CHCs obtained by our reduction, where the final target value of each mutable reference is modeled by a syntactic variable in logic.
In Section 4.1, we introduce a special sort of resolution called SLDC resolution. In Section 4.2, we extend the extraction-examination judgments introduced in Section 1 to model mutable references and frozen variables. In Section 4.3, we complete the proof of Theorem 1, the soundness and completeness of our reduction, by establishing a bisimulation between program execution and SLDC resolution (Theorem 4).
4.1 SLDC Resolution
We introduce a deduction algorithm on CHC systems, which we call SLDC resolution (Selective Linear Definite clause Calculative resolution). It is a variant of SLD resolution [43] with calculative steps. SLDC resolution is designed to be complete with respect to the logic (Lemma 2). Interpreting each CHC as a deduction rule, resolution can generally be understood as a top-down construction of a proof tree, and this idea is related to computation. As we see later, SLDC resolution is designed to form bisimulation with execution in the operational semantics (Theorem 4).
SLDC resolution is described as a transition system on resolutive configurations
, which are of the form
. In a process of transition, it also uses resolutive pre-configurations
, which are of the form
. Recall that
is a meta-variable for a pattern formula, which does not have integer operators
; however,
is a meta-variable for a usual formula, which can have operators. The pattern
on the right side of a configuration/pre-configuration is used to track how variables are instantiated. Later, SLDC resolution is associated with execution in the operational semantics; the pattern formulas
in a configuration/pre-configuration can be understood as a model of a call stack, and the pattern
records the final return value. Resolutive (pre-)configurations that are alpha-equivalent are considered identical.
The one-step transition relation judgment of SLDC resolution
is defined by the following non-deterministic transformation:
(1) |
| ||||
(2) | Now, we calculate and specialize More precisely, the calculation and specialization process repeats the following operations (the order of the operations can actually be freely chosen). | ||||
For any CHC system
, for each predicate variable
taking one or more arguments, the predicate given in the least solution
is equivalent to the predicate
on values of the appropriate sorts defined by the following rule:

unifies to
” means that replacing variables in
with some values we obtain
.Similar to the proof of completeness of SLD resolution [43].□
(Resolution Sequence in SLDC Resolution).
Below is an example resolution sequence in SLDC resolution for the CHC system presented in Example 3, which represents the program introduced in Example 1. It corresponds to the example execution in the operational semantics presented in Example 2.

), the mutable references
and
are modeled, respectively, as
and
, where
and
are logic variables freshly taken for the borrows. Here, the frozen variables
and
are, respectively, modeled as
and
. In the seventh line (
), the mutable reference
has been thrown away, and now
is specialized to 3, which makes
modeled as a value
without a logic variable. In the twelfth line (
), the variable
has now been specialized to 6. Note that each logic variable is specialized before the deadline of the corresponding borrow and the timing is determined dynamically.4.2 Extending the Basic Extraction-examination Judgment
We extend the basic extraction-examination judgment
defined in Section 3.3 to accept mutable references and frozen variable. The key idea is to model the final target value
of each mutable reference as a logic variable, which is semantically universally quantified. To model each variable, we now use a value with logic variables, i.e., a pattern
. A mutable reference is modeled as a pair of the current target pattern and a logic variable uniquely assigned to the mutable reference. Each mutably borrowed part of a frozen variable is set to the logic variable of the mutable reference that borrows that part.
The new extraction-examination judgment is of the form
, where
is an abstract configuration, which is an extension of a weak abstract configuration with logic variables. To formally describe this judgment, we introduce new judgments for collecting the global information on lifetime variables.
The safety judgment on a concrete configuration
is defined as
. Later, we prove the progress and preservation properties for this safety condition for a well-typed program, which can be regarded as the proof of soundness of the type system.
Taking Global and Dynamic Information on Lifetime Variables. First, for a well-typed program
and a concrete configuration
, we construct the global lifetime context
, which is the lifetime context for all the stack frames in
. A local lifetime
in the
th stack frame (indexed from the bottom) is named
. It also has the global elimination order, which is constructed based on the promises in each function call (i.e., the lifetime context given by the type system for each stack frame) and the hierarchy of stack frames (i.e., the property that
implies
). We also add to the global lifetime context the lifetime parameters in the base stack frame. Formally,
is defined as follows:

Also, for each stack frame, indexed
, we define the lifetime substitution
. It maps each lifetime variable in the stack frame to the corresponding lifetime variable in the global lifetime context
. For each local lifetime
in the stack frame, we just add the tag
. For each lifetime parameter, we should find the lifetime variable assigned to it. Therefore, formally,
is defined as follows:13

Extracting Rich Information from the Heap Memory. First, we define an abstract configuration
and an abstract stack frame
as follows:

and a concrete stack frame
, but map each data variable to a pattern, which may contain logic variables. The use of the logic variables here is related to the notion of prophecy variables [1, 36, 73].We also introduce some auxiliary notions. A logic variable summary
is a finite multiset of items of the form described below:

is a finite multiset of items of the form described below:

has been introduced for data contexts in Section 2.2; it is of the form
(active) or
(borrowed until the lifetime
). An extended memory footprint records the memory access employed in extracting the data from a concrete configuration. An extended access mode
is an item of the form either
or
.Now, we introduce the two basic extraction judgments,
and
. The former structurally extracts from the heap memory
the pointer object typed
of the address
as a pattern
, yielding a logic variable summary
and the extended memory footprint
, under the activeness
and the extended access mode
. The latter is similar to the former but extracts the object typed
stored at the address
. They are defined by the following rules:
The two judgments are extensions of the judgments
and
introduced in Section 3.3.
For the judgment
, we have two rules for extracting the data of a mutable reference, namely, Extract-Mut-Update and Extract-Mut-Read. The former is the one used for update access; in this case, we use a logic variable
at the second argument of the
container and record the variable into the logic variable summary. The latter is the one used for read access; in this case, we do not care about the second argument of the
container.
Also, for the judgment
, we can stop exploring the heap memory and just return a logic variable using the rule Extract-Take-Variable. Although we impose here no special restriction on using this rule, the use of the rule is recorded in the logic variable summary
as the “taker” item
. Later, in the safety condition on the logic variable summary, we require that the giver and the taker correspond one to one with some agreement conditions (Safe-Summary-Correspond).
Next, we introduce the judgment for extracting the data of a concrete stack frame into an abstract stack frame,
. Here,
is a substitution on lifetime variables associated with the stack frame (soon later,
is assigned to
). It is defined by the following rule:

introduced in Section 3.3. Since the logic variable summary records the information of lifetime variables in types, we apply the lifetime substitution
to the type
of each variable.Now, we define the judgment for extracting values from the concrete configuration as an abstract configuration,
, by the following rule:

introduced in Section 3.3.Examining the Safety. Now, we define safety conditions.
First, we introduce the safety judgment on a logic variable summary
. It uses the global lifetime context
. It is defined by the following rules, using an auxiliary judgment
:

such that
, we require that the logic variable summary
has exactly one giver and one taker of
, that they agree on the address and the type (up to equivalence), and that the lifetime of the giver is no later than the lifetime of the taker (Safe-Summary-Correspond).Next, we introduce the safety judgment on an extended memory footprint
. It is defined by the following rules, using an auxiliary judgment
:

, introduced in Section 3.3. We now have to deal with frozen access. Since a mutable (re)borrow completely masks the lender with a logic variable, even if we have
that comes form a mutable reference, we do not have any other items of the form
, which keeps the situation simple. When we have shared references that comes from some lender, we need to see agreement between the frozen update access and the borrowed read access, which is performed by the rule Safe-Footprint-Update-Read.Finally, we define the extended extraction-examination judgment
by the following rule:

Safety Condition on a Concrete Configuration. The safety judgment on a concrete configuration,
, is defined simply as
.
For any well-typed program, we have the progress property on the safety condition.
For any
and
, if
holds and
does not hold, then there exists some
satisfying
.
It can be easily proved by a straightforward case analysis. The safety condition simply ensures that the data stored in the heap memory has the expected forms.□
The preservation property on the safety condition also holds. It is later shown (Corollary 5) as a corollary of the bisimulation theorem (Theorem 4).
4.3 Bisimulation between Execution and SLDC Resolution
Now, we define the judgment, or relation,
, which links the world of the operational semantics and the world of SLDC resolution. We prove that the relation forms a bisimulation between execution in the operational semantics and SLDC resolution in our CHC representation (Theorem 4). Using this bisimulation, we complete the proof of Theorem 1, the soundness and completeness of our reduction. A key point is that, at the moment we release a mutable reference modeled as
, we specialize the logic variable
into the current target pattern
.
The judgment
, which translates a concrete configuration
into a logic configuration
, is defined as follows:

is designed as a resolutive configuration for our CHC representation of the program
. For simplicity, we assumed here that the arguments of the predicate
are in the order of
for each
. The variables
are fresh logic variables that are mutually distinct.The relation
forms a bisimulation between execution in the operational semantics and SLDC resolution in our CHC representation.
Assume that
holds. For any
satisfying
, there exists
such that
and
hold. Likewise, for any
satisfying
, there exists
such that
and
hold.
Taking a close look at each type of statements, we can find a correspondence between a transition on concrete configurations and the transition on resolutive configurations under our CHC representation. Therefore, we can choose
based on
and choose
based on
(we do not explicitly describe this choice here). The question is whether
really holds. Let
the abstract configuration associated with
. The property
can be broken into (i) whether the extraction judgment
holds, (ii) whether the safety condition on the logic variable summary
holds, and (iii) whether the safety condition on the extended memory footprint
holds. We can show
under the assumptions by some case analyses. Below, we give more detailed illustrations for some types of transitions.
Manipulation of Owning Pointers. Some transitions manipulate the target objects of some owning pointers. For example, the instruction
moves the memory sequences of
and
to allocate the two at one consecutive memory region. Also, the swap instruction
destructively can update the target object of an ownership pointer.
To handle such operations, the type system and the extraction judgments give an important guarantee: The manipulated memory cells should always be accessed with the active update permission. The safety judgment on the extended memory footprint
ensures that, when there is an active update access on an address, there is no other access on the address. Therefore, we can ensure that the transition updates only the expected part of the heap memory in the expected way and does not affect other unrelated memory cells. Note especially that the swap operation does not change the logic variable summary and the extended memory footprint.
Manipulation of Mutable References and Logic Variables. When a mutable reference is released, weakened (to an immutable reference), or subdivided by the transition, logic variables in the resolutive configuration and the abstract configuration are updated.
When a mutable reference modeled as a pattern
is released or weakened, the logic variable
is resolved into the pattern
. The lender of the target object of the mutable reference, which is still frozen under some lifetime in the type system, retrieves frozen update access to the object through extraction judgments. The safety on the logic variable summary provides an important guarantee: For each logic variable
concerned, there exists exactly one giver
and one taker
, which agree on the address and types (
and
).
When a mutable reference is subdivided, the situation is a bit more involved. For example, when we perform
on a mutable reference to a variant
,
is resolved into
with a newly taken logic variable
, and we get a new mutable reference
.
We can check that, after each type of manipulation on mutable references and logic variables,
holds.
Retyping. The retyping instruction
can change the type of an active data variable
from the original type
to a new type
, if
holds under the local lifetime context
. By induction over the type
and the memory extraction for
, we can prove the following properties, under the global lifetime context
, which extends the local lifetime context
: (i) Every update on the extended memory footprint has the following form: an item
turns into
for
satisfying
under the (global) lifetime context
. (ii) Every update on the logic variable summary has the following form: an item
turns into
for
and
satisfying
and
. Importantly, the type information in the logic variable summary remains unchanged up to type equivalence, because the mutable reference type
is invariant over the body type
.
Elimination of a Local Lifetime Variable. When a local lifetime
is eliminated with the instruction
, all the frozen variables in the data context tagged with
get reactivated. The type system ensures that there remains no reference associated with the lifetime
. Therefore, the extended memory footprint has no item of form
, which ensures the safety condition on the extended memory footprint after the lifetime elimination.□
Using this bisimulation, we can show preservation of the safety condition on concrete configurations (although this is not directly linked to the proof of Theorem 1).
For any
,
and
, if
and
hold, then
holds.
It follows from Theorem 4, because the judgment
is equivalent to
.□
Before completing the proof of Theorem 1, we show a few simple lemmas.
For any simple function
in a program
, for any concrete configuration
of form
, satisfying either
or
, the following equivalence holds:

It can be proved by straightforward induction.□
For any
and
, there exists at most one
such that
holds.
Clear from the definition.□
For any program
and any weak abstract configuration
, if the function
is simple, the label
is
or a label associated with a
statement, and
maps each variable to a value of the suitable sort, then there exists a concrete configuration
such that
holds.
By straightforward construction.□
Now, we complete the proof of Theorem 1, the soundness and completeness of our reduction.
We show each direction of the implication.
Necessity (
implies
). There exists a sequence of concrete configurations
such that the following judgments hold:

and
, the following judgments hold:

such that the following judgments hold:

. Because
holds, in the CHC representation
there is only one CHC whose head has
and the CHC has the form
. Thus,
holds. Therefore, by Lemma 2, we have
.Sufficiency (
implies
). By Lemma 2, there exists a sequence of resolutive configurations
such that the following properties hold:

, we can find that
is of the form
, where the statement at the label
is a
statement. By Lemma 8, we can construct a concrete configuration
such that
holds. By Theorem 4, we also have a sequence of concrete configurations
such that the following judgments hold:


holds.□5 IMPLEMENTATION AND EVALUATION
We implemented a verification tool for Rust programs based on our reduction, RustHorn, and conducted preliminary evaluation experiments with small benchmarks, where we successfully confirmed the effectiveness of our approach. In this section, we report on that.
5.1 Implementation of RustHorn
We implemented a prototype CHC-based verification tool RustHorn, which reduces Rust programs to CHCs by our method proposed in this article. It is available at https://github.com/hopv/rust-horn. It is written in Rust by
2,500 lines of code. The tool supports the core features of Rust, including recursions and recursive types.
RustHorn analyzes the MIR (Mid-level Intermediate Representation) [54] of a Rust program, which is provided by the Rust compiler, and then generates CHCs by applying our reduction. The use of MIR enables our tool to support a broad range of Rust programs, with various kinds of syntax sugar. (An obstacle is that the implementation depends on a nightly version of the Rust compiler, because the Rust compiler’s internal representation is unstable.) RustHorn relies on the Rust compiler’s borrow check and simply ignores the lifetime information, which is valid, thanks to the nature of our method.
We briefly explain here MIR and RustHorn’s algorithm. MIR models each Rust function as a set of simple instructions (called a statement) labeled with program points, like our calculus COR. In MIR, some sequentially executed instructions are packed into what is called a basic block. For efficiency, RustHorn introduces a predicate variable for each basic block rather than for each program point. RustHorn analyzes the set of local variables at the head of each basic block. Then for each basic block, it models the initial environment by symbolic values and performs a kind of symbolic execution to analyze the final environment of the block. In particular, in a MIR statement, we can directly access a (possibly deep) substructure of a local variable (called a place), which is not supported in COR but can easily be modeled in our reduction. Depending on the action taken at the end of the block (which is expressed by what is called a terminator), it adds some CHCs to the output. Before we jump to another basic block or return from the function, we clean up the local variables that will not be used anymore and add the equality constraint on the final target value of each mutable reference contained in the variables.
This algorithm performs a more advanced reduction than our formalized reduction presented in Section 3. We believe that our soundness and completeness proof presented in Section 2 and Section 4 justifies the core idea of this advanced reduction, but direct proof on the advanced reduction remains to be a challenge.
5.2 Benchmarks and Experiments
To measure the performance of RustHorn and the existing CHC-based verifier for C, SeaHorn [25], we conducted preliminary experiments using the benchmarks listed in Table 1. Each benchmark program has one assertion to be verified and is provided both in Rust and C. Most benchmark instances consist of a pair of safe and an unsafe programs that only differ from each other in the asserted property. We also wrote LOC (in Rust, skipping blank and comment lines) of each benchmark in the table. The column Loop? shows whether the verified program has loops (which include recursions, exclude vacuous loops like
| RustHorn | SeaHorn w/Spacer | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| Group | Instance | Safe? | LOC | Loop? | Mut? | w/Spacer | w/HoIce | as is | modified |
| simple | 01 | safe | 12 | yes | no | <0.1 | 0.1 | <0.1 | |
| 04-recursive | safe | 14 | yes | no | 0.5 | timeout | 0.8 | ||
| 05-recursive | unsafe | 26 | yes | no | <0.1 | <0.1 | <0.1 | ||
| 06-loop | safe | 10 | yes | no | timeout | 0.1 | timeout | ||
| hhk2008 | safe | 20 | yes | no | timeout | 47.9 | <0.1 | ||
| unique-scalar | unsafe | 9 | no | yes | <0.1 | 0.3 | <0.1 | ||
| bmc | 1 | safe | 46 | no | no | 0.2 | <0.1 | <0.1 | |
| unsafe | 0.2 | <0.1 | <0.1 | ||||||
| 2 | safe | 15 | yes | no | timeout | 0.1 | <0.1 | ||
| unsafe | <0.1 | 0.1 | <0.1 | ||||||
| 3 | safe | 35 | yes | no | 0.1 | <0.1 | <0.1 | ||
| unsafe | <0.1 | <0.1 | <0.1 | ||||||
| diamond-1 | safe | 55 | no | no | 0.1 | <0.1 | <0.1 | ||
| unsafe | <0.1 | <0.1 | <0.1 | ||||||
| diamond-2 | safe | 40 | no | no | 0.2 | <0.1 | <0.1 | ||
| unsafe | 0.1 | <0.1 | <0.1 | ||||||
| prusti | ackermann | safe | 17 | yes | no | <0.1 | 0.1 | <0.1 | |
| ackermann-same | safe | 26 | yes | no | timeout | timeout | timeout | ||
| compress | safe | 12 | no | yes | <0.1 | 0.1 | false alarm | ||
| borrows-align | safe | 14 | no | yes | <0.1 | 0.1 | <0.1 | ||
| account | safe | 18 | no | yes | <0.1 | 0.1 | <0.1 | ||
| unsafe | <0.1 | 0.2 | <0.1 | ||||||
| restore | safe | 14 | no | yes | <0.1 | 0.3 | false alarm | ||
| inc-max | base | safe | 15 | no | yes | <0.1 | 0.2 | false alarm | <0.1 |
| unsafe | <0.1 | 0.2 | <0.1 | <0.1 | |||||
| base3 | safe | 22 | no | yes | <0.1 | 0.2 | false alarm | ||
| unsafe | <0.1 | 0.2 | <0.1 | ||||||
| repeat | safe | 22 | yes | yes | 0.1 | timeout | false alarm | timeout | |
| unsafe | <0.1 | 0.5 | <0.1 | <0.1 | |||||
| repeat3 | safe | 29 | yes | yes | 0.2 | timeout | false alarm | ||
| unsafe | <0.1 | 1.4 | <0.1 | ||||||
| swap-dec | base | safe | 23 | yes | yes | 0.1 | 0.5 | false alarm | <0.1 |
| unsafe | 0.1 | timeout | <0.1 | 0.1 | |||||
| base3 | safe | 27 | yes | yes | 0.1 | timeout | false alarm | <0.1 | |
| unsafe | 0.4 | 14.2 | <0.1 | 0.1 | |||||
| exact | safe | 24 | yes | yes | 0.1 | 0.6 | false alarm | 0.2 | |
| unsafe | <0.1 | timeout | <0.1 | <0.1 | |||||
| exact3 | safe | 30 | yes | yes | timeout | timeout | false alarm | 0.2 | |
| unsafe | 0.1 | 2.2 | <0.1 | <0.1 | |||||
| swap2-dec | base | safe | 24 | yes | yes | 0.4 | 0.8 | false alarm | <0.1 |
| unsafe | 1.2 | timeout | <0.1 | 0.1 | |||||
| base3 | safe | 32 | yes | yes | 1.8 | timeout | false alarm | <0.1 | |
| unsafe | 67.0 | 39.0 | <0.1 | 0.2 | |||||
| exact | safe | 25 | yes | yes | 1.0 | 1.0 | false alarm | 0.2 | |
| unsafe | <0.1 | timeout | <0.1 | <0.1 | |||||
| exact3 | safe | 35 | yes | yes | timeout | timeout | false alarm | 1.3 | |
| unsafe | <0.1 | 6.2 | <0.1 | <0.1 | |||||
| just-rec | base | safe | 14 | yes | yes | <0.1 | 0.2 | <0.1 | |
| unsafe | <0.1 | 0.2 | <0.1 | ||||||
| linger-dec | base | safe | 15 | yes | yes | <0.1 | 0.2 | false alarm | |
| unsafe | <0.1 | 0.2 | <0.1 | ||||||
| base3 | safe | 29 | yes | yes | <0.1 | 0.3 | false alarm | ||
| unsafe | <0.1 | 31.9 | <0.1 | ||||||
| exact | safe | 16 | yes | yes | <0.1 | 0.3 | false alarm | ||
| unsafe | <0.1 | 0.3 | <0.1 | ||||||
| exact3 | safe | 30 | yes | yes | <0.1 | 0.4 | false alarm | ||
| unsafe | <0.1 | 1.3 | <0.1 | ||||||
| lists | append | safe | 27 | yes | yes | tool error | 0.3 | false alarm | |
| unsafe | tool error | 0.3 | 0.1 | ||||||
| inc-all | safe | 35 | yes | yes | tool error | 0.3 | false alarm | ||
| unsafe | tool error | 0.4 | <0.1 | ||||||
| inc-some | safe | 32 | yes | yes | tool error | 0.3 | timeout | ||
| unsafe | tool error | 0.6 | 0.1 | ||||||
| inc-some2 | safe | 34 | yes | yes | tool error | timeout | timeout | ||
| unsafe | tool error | 0.7 | 0.3 | ||||||
| trees | append | safe | 35 | yes | yes | tool error | 0.4 | false alarm | |
| unsafe | tool error | 0.3 | <0.1 | ||||||
| inc-all | safe | 36 | yes | yes | tool error | timeout | timeout | ||
| unsafe | tool error | 0.2 | 0.1 | ||||||
| inc-some | safe | 34 | yes | yes | tool error | 0.4 | timeout | ||
| unsafe | tool error | 0.7 | 0.1 | ||||||
| inc-some2 | safe | 36 | yes | yes | tool error | tool error | timeout | ||
| unsafe | tool error | 0.9 | timeout | ||||||
Table 1. Benchmarks and Experimental Results on RustHorn and SeaHorn, with Spacer and HoIce
We conducted experiments on a commodity laptop (2.6 GHz Intel Core i7 MacBook Pro with 16 GB RAM). First, we reduced each benchmark program into CHCs in the SMT-LIB 2 format using RustHorn and SeaHorn (version 10.0.0-rc0-86a31cf1) [25]. The time for the reduction was quite short (at most
0.3 second for each program). After that, we measured the time of CHC solving by Spacer [42] in Z3 (version 4.8.10) [75] and HoIce (version 1.8.3)14 [12, 13] for the generated CHCs. HoIce does not accept SeaHorn’s outputs, because SeaHorn uses a different format and employs arrays for pointers. We have also prepared modified versions of some of the CHCs generated by SeaHorn, obtained by adding constraints on address freshness to improve accuracy of the model and reduce false alarms. Still, we could not make the modified versions for the benchmarks in the groups
Below, we explain our benchmarks more in detail.
The benchmarks in the groups
Also, in light of (iii), we omitted large SeaHorn tests, such as
The benchmarks in the group
The benchmarks in the remaining seven groups were made by us featuring various use cases of mutable references. The benchmarks in the groups
The group
The group
The instances labeled
The groups
The safe program of the instance
The instance
Benchmarks in the group
5.3 Experimental Results
Table 1 shows the results of the experiments. The columns for RustHorn and SeaHorn show the time for verification (in seconds) in the case the verification was successful. In the case the verification failed, the columns show one of the following failure labels. The label timeout means timeout over the time limit of 180 seconds. The label false alarm means a report of unsafety for a safe program. The label tool error means an error of the backend CHC solver; Spacer was unstable for recursive types in general and HoIce was unstable in some situations.
RustHorn, combined with either Spacer or HoIce, successfully verified all programs that were successfully verified by SeaHorn (without our modification). Also, RustHorn successfully verified various interesting programs that SeaHorn could not verify. The verification time of RustHorn largely matched that of SeaHorn. Although the benchmark set used for the experiments is small and more or less contrived, we believe that the experimental results already indicate effectiveness of our verification method. Experiments on larger, more realistic benchmark programs are left to future work.
The combination of RustHorn and HoIce succeeded in verifying many programs with recursive data types (
SeaHorn, without our modification, issued false alarms for many programs in the last seven benchmark groups, from
For the benchmarks in the groups
6 RELATED WORK
CHC-based Verification of Pointer-Manipulating Programs. SeaHorn [25] 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 [26] are used to optimize the array representation of the heap memory, their approach suffers from some pointer use cases that our approach can handle, as is examined by our experiments reported in Section 5. Still, their approach is significant in the context of automated verification, given that many real-world pointer-manipulating programs do not fit within Rust’s permission control.
Another approach is taken by JayHorn [38, 39], which automatically verifies Java programs (possibly using object pointers) by reduction to CHCs. It represents store invariants using special predicates pull and push. Although this allows faster reasoning about the heap memory 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 Section 5.2. JayHorn reported “
Verification for Rust. Whereas we have presented the first CHC-based (fully automated) verification method specially designed for Rust, there are a number of studies on other types of verification for Rust.
RustBelt by Jung et al. [34] formally verified safety properties for Rust libraries with unsafe internal implementation, using manual reasoning in the higher-order separation logic Iris [35, 37], based upon higher-order concurrent separation logic, built on the Coq Proof Assistant [16]. They presented a formalized core of Rust,
, which affected the language design of our calculus COR. Thanks to the power of Iris, their verification method is highly extensible for various Rust libraries with unsafe code, including those with interior mutability. Still, they verify only the safety property and do not cover functional correctness. Also, the automation of the verification in their approach is not well discussed.
Ullrich [72] translated a subset of Rust into a purely functional programming language to manually verify functional correctness of some tricky Rust programs using the Lean Theorem Prover [17]. Although this method eliminates pointers to get simple models like our approach, the applicability of this method is quite limited, because it deals with mutable references by simple static tracking of addresses based on lenses [22]. This method thus does not support even basic use cases such as dynamic selection of mutable references (e.g.,
There are a series of studies [3, 19, 29] of methods of semi-automated verification of Rust programs using Viper [52], a verification platform based on separation logic with fractional permission. This approach can deal with advanced features such as unsafe code [29] and type traits [19] to some extent. In particular, Prusti by Astrauskas et al. [3] conducted semi-automated verification (manually providing pre/post-conditions and loop invariants) on many realistic examples. Also, they use special machinery called a pledge to model mutable borrows, which enables operations like
There are also approaches based on bounded model checking [4, 46, 70] for verification of Rust programs with unsafe code. Our reduction can be applied to bounded model checking, as discussed in Section 3.5.
Verification Using Permission/Ownership. The notion of permission/ownership has been applied to a wide range of verification. It has been used for detecting race conditions in concurrent programs [8, 69] and analyzing the safety of memory allocation [68]. Separation logic based on permission is also studied well [7, 37, 52]. A simple notion of permission has also been used in some verification tools [5, 15, 23]. However, existing studies on permission-based verification are mostly based on fractional or counting permission, which is quite different from Rust’s permission control.
Prophecy Variables. Our idea of considering a future value to represent a mutable reference is related to the notion of prophecy variables [1, 36, 73]. In particular, Jung et al. [36] presented a new program logic that supports prophecy variables on the separation logic Iris [37].
7 CONCLUSION
We proposed a novel method for CHC-based automated verification of Rust programs. The key idea is to model a mutable reference as a pair of the current target value and the target value at the end of the borrow. We formalized the method for a core language of Rust and proved its soundness and completeness. We implemented a prototype verification tool for a subset of Rust and confirmed the effectiveness of our approach through an experiment.
ACKNOWLEDGMENTS
We are grateful to the anonymous reviewers for insightful and helpful comments.
Footnotes
1 To be precise,
Footnoteint inC usually represents a 32-bit integer. However, in this article, we just consider unbounded integers for simplicity.2 Note that this system can be straightforwardly transformed into the following system of standard CHCs:
Footnote
3 Although some CHC-based verifiers use forward reduction, where the implication of each CHC goes in the direction of program execution, in this article, we use backward reduction, where the implication goes in the opposite direction.
Footnote4 We examined SeaHorn in our experiments, which are reported in Section 5.
Footnote5 This terminology is standard but a bit confusing, since the word “mutable/immutable” describes the property of the target object of the reference, rather than the property of the reference itself. Another terminology is “unique/shared reference,” which can be less confusing.
Footnote6 In the standard terminology of Rust, a lifetime often means a time range where a borrow is active. In this article, however, we use the term lifetime to refer to the time point when a borrow ends.
Footnote7 In Rust, we do not need to (and actually cannot) write annotations on local lifetimes, like
Footnote’l used above. The Rust compiler performs a very clever inference on local lifetimes.8 Strictly speaking, this property is broken by recently adopted (implicit) two-phase borrows [56, 65]. However, by shallow syntactical reordering, a program with two-phase borrows can be fit into usual borrow patterns.
Footnote9 When a local variable contains a mutable reference, the meaning of the “value” can be subtle because of the final target value in our model. Later in Section 4.2 and Section 4.3, we model each of the future target values as a syntactic variable in logic.
Footnote10 We actually do not need to support the case (v), because we can perform
Footnote
before
.11 We need this rule for the random value instruction
Footnote
in establishing the bisimulation of Theorem 4.12 We add this rule for the completeness lemma Lemma 2. Actually, we do not need this rule for resolution of a CHC representation
Footnote
.13 For simplicity, we assume that for each non-top stack frame we can uniquely determine the label of the function call statement performed for creating the frame, which allows us to determine
Footnote
. We can always satisfy this by inserting a fresh no-op labeled statement just after the function call.14 We used Z3 version 4.7.1 for the backend SMT solver of HoIce to deal well with recursive data types.
Footnote
- [1] . 1991. The existence of refinement mappings. Theor. Comput. Sci. 82, 2 (1991), 253–284.
DOI: DOI: DOI: https://doi.org/10.1016/0304-3975(91)90224-P Google ScholarDigital Library
- [2] . 2012. Lazy abstraction with interpolants for arrays. In Logic for Programming, Artificial Intelligence, and Reasoning - 18th International Conference, LPAR-18, Mérida, Venezuela, March 11–15, 2012. Proceedings (Lecture Notes in Computer Science), and (Eds.), Vol. 7180. Springer, 46–61.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-642-28717-6_7 Google ScholarCross Ref
- [3] . 2018. Leveraging Rust types for modular specification and verification.
DOI: DOI: DOI: https://doi.org/10.3929/ethz-b-000311092Google Scholar - [4] . 2018. Verifying Rust programs with SMACK. InAutomated Technology for Verification and Analysis - 16th International Symposium, ATVA 2018, Los Angeles, CA, USA, October 7–10, 2018, Proceedings.
Lecture Notes in Computer Science , Vol. 11138. Springer, 528–535.DOI: DOI: DOI: https://doi.org/10.1007/978-3-030-01090-4_32Google ScholarCross Ref
- [5] . 2011. Specification and verification: The Spec# experience. Commun. ACM 54, 6 (2011), 81–91.
DOI: DOI: DOI: https://doi.org/10.1145/1953122.1953145 Google ScholarDigital Library
- [6] . 2015. Horn clause solvers for program verification. In Fields of Logic and Computation II - Essays Dedicated to Yuri Gurevich on the Occasion of His 75th Birthday (Lecture Notes in Computer Science), , , , , and (Eds.), Vol. 9300. Springer, 24–51.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-319-23534-9_2Google Scholar - [7] . 2005. Permission accounting in separation logic. In Proceedings of the 32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, and (Eds.). ACM, 259–270.
DOI: DOI: DOI: https://doi.org/10.1145/1040305.1040327 Google ScholarCross Ref
- [8] . 2002. Ownership types for safe programming: Preventing data races and deadlocks. In Proceedings of the 2002 ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages and Applications, OOPSLA 2002, Seattle, Washington, USA, November 4–8, 2002, and (Eds.). ACM, 211–230.
DOI: DOI: DOI: https://doi.org/10.1145/582419.582440 Google ScholarCross Ref
- [9] . 2003. Checking interference with fractional permissions. In Static Analysis, 10th International Symposium, SAS 2003, San Diego, CA, USA, June 11–13, 2003, Proceedings (Lecture Notes in Computer Science), (Ed.), Vol. 2694. Springer, 55–72.
DOI: DOI: DOI: https://doi.org/10.1007/3-540-44898-5_4 Google ScholarCross Ref
- [10] . 2006. What’s decidable about arrays? In Verification, Model Checking, and Abstract Interpretation, 7th International Conference, VMCAI 2006, Charleston, SC, USA, January 8–10, 2006, Proceedings (Lecture Notes in Computer Science), and (Eds.), Vol. 3855. Springer, 427–442.
DOI: DOI: DOI: https://doi.org/10.1007/11609773_28 Google ScholarCross Ref
- [11] . 2018. Higher-order constrained horn clauses for verification. Proc. ACM Program. Lang. 2, POPL (2018), 11:1–11:28.
DOI: DOI: DOI: https://doi.org/10.1145/3158099 Google ScholarCross Ref
- [12] . 2018. ICE-based refinement type discovery for higher-order functional programs. In Tools and Algorithms for the Construction and Analysis of Systems - 24th International Conference, TACAS 2018, Held as Part of the European Joint Conferences on Theory and Practice of Software, ETAPS 2018, Thessaloniki, Greece, April 14–20, 2018, Proceedings, Part I (Lecture Notes in Computer Science), and (Eds.), Vol. 10805. Springer, 365–384.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-319-89960-2_20Google Scholar - [13] . 2018. HoIce: An ICE-based non-linear horn clause solver. In Programming Languages and Systems - 16th Asian Symposium, APLAS 2018, Wellington, New Zealand, December 2–6, 2018, Proceedings (Lecture Notes in Computer Science), (Ed.), Vol. 11275. Springer, 146–156.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-030-02768-1_8Google ScholarCross Ref
- [14] . 1998. Ownership types for flexible alias protection. In Proceedings of the 1998 ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages & Applications (OOPSLA’98), Vancouver, British Columbia, Canada, October 18–22, 1998, and (Eds.). ACM, 48–64.
DOI: DOI: DOI: https://doi.org/10.1145/286936.286947 Google ScholarCross Ref
- [15] . 2009. VCC: A practical system for verifying concurrent C. In Theorem Proving in Higher Order Logics, 22nd International Conference, TPHOLs 2009, Munich, Germany, August 17–20, 2009. Proceedings (Lecture Notes in Computer Science), , , , and (Eds.), Vol. 5674. Springer, 23–42.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-642-03359-9_2 Google ScholarCross Ref
- [16] . 2021. The Coq Proof Assistant. Retrieved from https://coq.inria.fr/.Google Scholar
- [17] . 2015. The Lean theorem prover (system description). In Automated Deduction - CADE-25 - 25th International Conference on Automated Deduction, Berlin, Germany, August 1–7, 2015, Proceedings (Lecture Notes in Computer Science), and (Eds.), Vol. 9195. Springer, 378–388.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-319-21401-6_26Google ScholarCross Ref
- [18] . 2020. Rewriting the Heart of Our Sync Engine - Dropbox. Retrieved from https://dropbox.tech/infrastructure/rewriting-the-heart-of-our-sync-engine.Google Scholar
- [19] . 2019. Verification of Rust Generics, Typestates, and Traits. Master’s thesis. ETH Zürich.Google Scholar
- [20] . 2017. Sampling invariants from frequency distributions. In 2017 Formal Methods in Computer Aided Design, FMCAD 2017, Vienna, Austria, October 2–6, 2017, and (Eds.). IEEE, 100–107.
DOI: DOI: DOI: https://doi.org/10.23919/FMCAD.2017.8102247 Google ScholarCross Ref
- [21] . 2019. Quantified invariants via syntax-guided synthesis. In Computer Aided Verification - 31st International Conference, CAV 2019, New York City, NY, USA, July 15–18, 2019, Proceedings, Part I (Lecture Notes in Computer Science), and (Eds.), Vol. 11561. Springer, 259–277.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-030-25540-4_14Google ScholarCross Ref
- [22] . 2007. Combinators for bidirectional tree transformations: A linguistic approach to the view-update problem. ACM Trans. Program. Lang. Syst. 29, 3 (2007), 17.
DOI: DOI: DOI: https://doi.org/10.1145/1232420.1232424 Google ScholarCross Ref
- [23] . 2016. Un système de types pragmatique pour la vérification déductive des programmes (A Pragmatic Type System for Deductive Verification). Ph.D. Dissertation. University of Paris-Saclay, France. Retrieved from https://tel.archives-ouvertes.fr/tel-01533090.Google Scholar
- [24] . 2012. Synthesizing software verifiers from proof rules. In ACM SIGPLAN Conference on Programming Language Design and Implementation, PLDI’12, Beijing, China - June 11-16, 2012, , , and (Eds.). ACM, 405–416.
DOI: DOI: DOI: https://doi.org/10.1145/2254064.2254112 Google ScholarCross Ref
- [25] . 2015. The SeaHorn verification framework. In Computer Aided Verification - 27th International Conference, CAV 2015, San Francisco, CA, USA, July 18–24, 2015, Proceedings, Part I (Lecture Notes in Computer Science), and (Eds.), Vol. 9206. Springer, 343–361.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-319-21690-4_20Google ScholarCross Ref
- [26] . 2017. A context-sensitive memory model for verification of C/C++ programs. In Static Analysis - 24th International Symposium, SAS 2017, New York, NY, USA, August 30–September 1, 2017, Proceedings (Lecture Notes in Computer Science), (Ed.), Vol. 10422. Springer, 148–168.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-319-66706-5_8Google ScholarCross Ref
- [27] . 2016. SMT-based verification of parameterized systems. In Proceedings of the 24th ACM SIGSOFT International Symposium on Foundations of Software Engineering, FSE 2016, Seattle, WA, USA, November 13–18, 2016, , , and (Eds.). ACM, 338–348.
DOI: DOI: DOI: https://doi.org/10.1145/2950290.2950330 Google ScholarCross Ref
- [28] . 2018. Quantifiers on demand. In Automated Technology for Verification and Analysis - 16th International Symposium, ATVA 2018, Los Angeles, CA, USA, October 7–10, 2018, Proceedings.
Lecture Notes in Computer Science , Vol. 11138. Springer, 248–266.DOI: DOI: DOI: https://doi.org/10.1007/978-3-030-01090-4_15Google ScholarCross Ref
- [29] . 2016. Rust2Viper: Building a Static Verifier for Rust. Master’s thesis. ETH Zürich.
DOI: DOI: DOI: https://doi.org/10.3929/ethz-a-010669150Google Scholar - [30] . 2017. Thread modularity at many levels: A pearl in compositional verification. In Proceedings of the 44th ACM SIGPLAN Symposium on Principles of Programming Languages, POPL 2017, Paris, France, January 18–20, 2017, and (Eds.). ACM, 473–485.
DOI: DOI: DOI: https://doi.org/10.1145/3009837 Google ScholarCross Ref
- [31] . 2018. The eldarica Horn solver. In 2018 Formal Methods in Computer Aided Design, FMCAD 2018, Austin, TX, USA, October 30–November 2, 2018, and (Eds.). IEEE, 1–7.
DOI: DOI: DOI: https://doi.org/10.23919/FMCAD.2018.8603013Google Scholar - [32] . 1951. On sentences which are true of direct unions of algebras. J. Symbol. Log. 16, 1 (1951), 14–21. Retrieved from http://www.jstor.org/stable/2268661.Google Scholar
Cross Ref
- [33] . 2002. Cyclone: A safe dialect of C. In Proceedings of the General Track: 2002 USENIX Annual Technical Conference, June 10–15, 2002, Monterey, California, USA, (Ed.). USENIX, 275–288. Retrieved from http://www.usenix.org/publications/library/proceedings/usenix02/jim.html. Google Scholar
Digital Library
- [34] . 2018. RustBelt: Securing the foundations of the Rust programming language. Proc. ACM Program. Lang. 2, POPL (2018), 66:1–66:34.
DOI: DOI: DOI: https://doi.org/10.1145/3158154 Google ScholarCross Ref
- [35] . 2018. Iris from the ground up: A modular foundation for higher-order concurrent separation logic. J. Funct. Program. 28 (2018), e20.
DOI: DOI: DOI: https://doi.org/10.1017/S0956796818000151Google ScholarCross Ref
- [36] . 2020. The future is ours: Prophecy variables in separation logic. Proc. ACM Program. Lang. 4, POPL (2020), 45:1–45:32.
DOI: DOI: DOI: https://doi.org/10.1145/3371113 Google ScholarCross Ref
- [37] . 2015. Iris: Monoids and invariants as an orthogonal basis for concurrent reasoning. In Proceedings of the 42nd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL 2015, Mumbai, India, January 15–17, 2015, and (Eds.). ACM, 637–650.
DOI: DOI: DOI: https://doi.org/10.1145/2676726.2676980 Google ScholarCross Ref
- [38] . 2017. Quantified heap invariants for object-oriented programs. In LPAR-21, 21st International Conference on Logic for Programming, Artificial Intelligence and Reasoning, Maun, Botswana, May 7–12, 2017 (EPiC Series in Computing), and (Eds.), Vol. 46. EasyChair, 368–384.Google Scholar
- [39] . 2016. JayHorn: A framework for verifying Java programs. In Computer Aided Verification - 28th International Conference, CAV 2016, Toronto, ON, Canada, July 17-23, 2016, Proceedings, Part I (Lecture Notes in Computer Science), and (Eds.), Vol. 9779. Springer, 352–358.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-319-41528-4_19Google ScholarCross Ref
- [40] . 2018. Zeus: Analyzing safety of smart contracts. In Proceedings of the 25th Annual Network and Distributed System Security Symposium. The Internet Society.Google Scholar
Cross Ref
- [41] . 2011. Predicate abstraction and CEGAR for higher-order model checking. In Proceedings of the 32nd ACM SIGPLAN Conference on Programming Language Design and Implementation, PLDI 2011, San Jose, CA, USA, June 4–8, 2011, and (Eds.). ACM, 222–233.
DOI: DOI: DOI: https://doi.org/10.1145/1993498.1993525 Google ScholarCross Ref
- [42] . 2014. SMT-based model checking for recursive programs. In Computer Aided Verification - 26th International Conference, CAV 2014, Held as Part of the Vienna Summer of Logic, VSL 2014, Vienna, Austria, July 18–22, 2014. Proceedings (Lecture Notes in Computer Science), and (Eds.), Vol. 8559. Springer, 17–34.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-319-08867-9_2 Google ScholarCross Ref
- [43] . 1974. Predicate logic as programming language. In Information Processing, Proceedings of the 6th IFIP Congress 1974, Stockholm, Sweden, August 5–10, 1974, (Ed.). North-Holland, 569–574.Google Scholar
- [44] . 2004. Constructing quantified invariants via predicate abstraction. In Verification, Model Checking, and Abstract Interpretation, 5th International Conference, VMCAI 2004, Venice, Italy, January 11–13, 2004, Proceedings (Lecture Notes in Computer Science), and (Eds.), Vol. 2937. Springer, 267–281.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-540-24622-0_22Google ScholarCross Ref
- [45] and (Eds.). 2018. Automated Technology for Verification and Analysis - 16th International Symposium, ATVA 2018, Los Angeles, CA, USA, October 7–10, 2018, Proceedings.
Lecture Notes in Computer Science , Vol. 11138. Springer.DOI: DOI: DOI: https://doi.org/10.1007/978-3-030-01090-4Google ScholarCross Ref
- [46] . 2018. No panic! Verification of Rust programs by symbolic execution. In Proceedings of the 16th IEEE International Conference on Industrial Informatics. IEEE, 108–114.
DOI: DOI: DOI: https://doi.org/10.1109/INDIN.2018.8471992Google ScholarCross Ref
- [47] . 2014. The Rust language. In Proceedings of the 2014 ACM SIGAda Annual Conference on High Integrity Language Technology, HILT 2014, Portland, Oregon, USA, October 18–21, 2014, and (Eds.). ACM, 103–104.
DOI: DOI: DOI: https://doi.org/10.1145/2663171.2663188 Google ScholarCross Ref
- [48] . 2021. Extensible Functional-Correctness Verification of Rust Programs by the Technique of Prophecy. Master’s thesis. University of Tokyo. Retrieved from http://www.kb.is.s.u-tokyo.ac.jp/yskm24t/papers/master-thesis.pdf.Google Scholar
- [49] . 2020. RustHorn: CHC-based verification for Rust programs. In Programming Languages and Systems - 29th European Symposium on Programming, ESOP 2020, Held as Part of the European Joint Conferences on Theory and Practice of Software, ETAPS 2020, Dublin, Ireland, April 25-30, 2020, Proceedings (Lecture Notes in Computer Science), (Ed.), Vol. 12075. Springer, 484–514.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-030-44914-8_18Google Scholar - [50] . 2021. Boogie: An Intermediate Verification Language. Retrieved from https://www.microsoft.com/en-us/research/project/boogie-an-intermediate-verification-language/.Google Scholar
- [51] . 2021. Rust language — Mozilla Research. Retrieved from https://research.mozilla.org/rust/.Google Scholar
- [52] . 2016. Viper: A verification infrastructure for permission-based reasoning. In Verification, Model Checking, and Abstract Interpretation - 17th International Conference, VMCAI 2016, St. Petersburg, FL, USA, January 17–19, 2016. Proceedings (Lecture Notes in Computer Science), and (Eds.), Vol. 9583. Springer, 41–62.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-662-49122-5_2 Google ScholarCross Ref
- [53] . 2019. Rust Case Study: Community Makes Rust an Easy Choice for npm. Retrieved from https://www.rust-lang.org/static/pdfs/Rust-npm-Whitepaper.pdf.Google Scholar
- [54] . 2021. The MIR (Mid-level IR). Retrieved from https://rustc-dev-guide.rust-lang.org/mir/index.html.Google Scholar
- [55] . 2021. Reference Cycles Can Leak Memory - The Rust Programming Language. Retrieved from https://doc.rust-lang.org/book/ch15-06-reference-cycles.html.Google Scholar
- [56] . 2021. RFC 2025: Nested Method Calls. Retrieved from https://rust-lang.github.io/rfcs/2025-nested-method-calls.html.Google Scholar
- [57] . 2021. RFC 2094: Non-lexical Lifetimes. Retrieved from https://rust-lang.github.io/rfcs/2094-nll.html.Google Scholar
- [58] . 2021. Rust Programming Language. Retrieved from https://www.rust-lang.org/.Google Scholar
- [59] . 2021. std::cell::RefCell - Rust. Retrieved from https://doc.rust-lang.org/std/cell/struct.RefCell.html.Google Scholar
- [60] . 2021. std::collections::HashMap - Rust. Retrieved from https://doc.rust-lang.org/std/collections/struct.HashMap.html.Google Scholar
- [61] . 2021. std::rc::Rc - Rust. Retrieved from https://doc.rust-lang.org/std/rc/struct.Rc.html.Google Scholar
- [62] . 2021. std::sync::Mutex - Rust. Retrieved from https://doc.rust-lang.org/std/sync/struct.Mutex.html.Google Scholar
- [63] . 2021. std::thread::spawn - Rust. Retrieved from https://doc.rust-lang.org/std/thread/fn.spawn.html.Google Scholar
- [64] . 2021. std::vec::Vec - Rust. Retrieved from https://doc.rust-lang.org/std/vec/struct.Vec.html.Google Scholar
- [65] . 2021. Two-phase borrows. Retrieved from https://rust-lang.github.io/rustc-guide/borrow_check/two_phase_borrows.html.Google Scholar
- [66] . 2019. Combining higher-order model checking with refinement type inference. In Proceedings of the 2019 ACM SIGPLAN Workshop on Partial Evaluation and Program Manipulation, [email protected] 2019, Cascais, Portugal, January 14–15, 2019, and (Eds.). ACM, 47–53.
DOI: DOI: DOI: https://doi.org/10.1145/3294032.3294081 Google ScholarCross Ref
- [67] . 2001. A decision procedure for an extensional theory of arrays. In Proceedings of the 16th Annual IEEE Symposium on Logic in Computer Science. IEEE Computer Society, 29–37.
DOI: DOI: DOI: https://doi.org/10.1109/LICS.2001.932480 Google ScholarCross Ref
- [68] . 2009. Fractional ownerships for safe memory deallocation. In Programming Languages and Systems, 7th Asian Symposium, APLAS 2009, Seoul, Korea, December 14–16, 2009. Proceedings (Lecture Notes in Computer Science), (Ed.), Vol. 5904. Springer, 128–143.
DOI: DOI: DOI: https://doi.org/10.1007/978-3-642-10672-9_11 Google ScholarCross Ref
- [69] . 2008. Checking race freedom via linear programming. In Proceedings of the ACM SIGPLAN 2008 Conference on Programming Language Design and Implementation, Tucson, AZ, USA, June 7–13, 2008, and (Eds.). ACM, 1–10.
DOI: DOI: DOI: https://doi.org/10.1145/1375581.1375583 Google ScholarCross Ref
- [70] . 2015. crust: A bounded verifier for Rust. In 30th IEEE/ACM International Conference on Automated Software Engineering, ASE 2015, Lincoln, NE, USA, November 9–13, 2015, , , and (Eds.). IEEE Computer Society, 75–80.
DOI: DOI: DOI: https://doi.org/10.1109/ASE.2015.77 Google ScholarCross Ref
- [71] . 2016. Electrolysis Reference. Retrieved from http://kha.github.io/electrolysis/.Google Scholar
- [72] . 2016. Simple Verification of Rust Programs via Functional Purification. Master’s thesis. Karlsruhe Institute of Technology.Google Scholar
- [73] . 2008. Modular Fine-grained Concurrency Verification. Ph.D. Dissertation. University of Cambridge, UK. Retrieved from http://ethos.bl.uk/OrderDetails.do?uin=uk.bl.ethos.612221.Google Scholar
- [74] . 1976. The semantics of predicate logic as a programming language. J. ACM 23, 4 (1976), 733–742.
DOI: DOI: DOI: https://doi.org/10.1145/321978.321991 Google ScholarDigital Library
- [75] . 2021. The Z3 Theorem Prover. Retrieved from https://github.com/Z3Prover/z3.Google Scholar
Index Terms
RustHorn: CHC-based Verification for Rust Programs
Recommendations
Aeneas: Rust verification by functional translation
We present Aeneas, a new verification toolchain for Rust programs based on a lightweight functional translation. We leverage Rust’s rich region-based type system to eliminate memory reasoning for a large class of Rust programs, as long as they do not ...
A Lightweight Formalism for Reference Lifetimes and Borrowing in Rust
Rust is a relatively new programming language that has gained significant traction since its v1.0 release in 2015. Rust aims to be a systems language that competes with C/C++. A claimed advantage of Rust is a strong focus on memory safety without ...
Leveraging rust types for modular specification and verification
Rust's type system ensures memory safety: well-typed Rust programs are guaranteed to not exhibit problems such as dangling pointers, data races, and unexpected side effects through aliased references. Ensuring correctness properties beyond memory safety,...

































































Comments