skip to main content
research-article
Open Access

RustHorn: CHC-based Verification for Rust Programs

Published:31 October 2021Publication History

Skip Abstract Section

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.

Skip 1INTRODUCTION Section

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 , mc91() returns 91 if the computation terminates, which is a kind of (partial) functional correctness of the function mc91.1 The verified property is equivalent to the satisfiability of the following system of CHCs (here, is sugar for ):23

The predicate means that mc91() returns (if it terminates). The first CHC in the system above describes the specification of mc91 and the second one describes the required property of mc91. We can verify the expected property by finding a solution to the system like below:
As observed in the example above, finding a solution to CHCs generated from a program with loops and recursions is strongly related to finding the invariant on loops and recursions.

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:

Here, 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 mc91p. The second argument of , representing the pointer argument r of mc91p, is an index for the memory-state arrays. So the assignment *r = n - 10 is modeled in the then part of the first CHC as , obtained by updating the th element of the memory-state array. In the else part, represents &s. This CHC system has a simple solution:
which can be found by some array-supporting CHC solvers including Spacer [42] with the support of arrays by the underlying SMT solvers [10, 67].

However, the array-based approach has some shortcomings. Let us consider, for example, the following innocent-looking code (here, rand() is a non-deterministic function that can return any integer value):

Depending on the return value of rand(), just_rec(ma) either (i) immediately returns true or (ii) recursively calls itself and checks whether the target of ma remains unchanged through the recursive call. Since the target object of ma is not modified through the call of just_rec, the return value a0 == *ma is always true. A tricky point is that the function can modify the memory by newly allocating the data of b.

Suppose we wish to verify that just_rec never returns false. The array-based reduction generates a system of CHCs like the following:

Here, we have omitted the allocation for a0 for simplicity. We use 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.

The resulting CHC system now has a solution, but it involves a quantifier.
Here, we need a quantified invariant , 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 just_rec, SeaHorn generates CHCs without arrays, by successfully analyzing that no effective destructive update happens, although SeaHorn usually uses the array-based reduction. Still, such analyses are usually more or less ad-hoc and can easily fail for advanced pointer uses.4 Existing verifiers like SeaHorn target programming languages such as C/C++ and Java, which do not restrict aliasing of pointers, which causes the difficulties in program verification.

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 a and b during the execution of inc_max(5, 3). In inc_max, on line 5, the permission to update the integer object of a is borrowed by a newly taken pointer, or mutable reference, &a, and we similarly perform borrowing on b. For both borrows, the deadline is set to the end of the inner block (} in line 6). Until the deadline, the two references have the update permission on the integer objects, whereas the lenders a and b temporarily lose all the permission. Now the function take_max is called with arguments &a and &b. The function take_max takes two integer mutable references ma and mb and returns the one with the larger target value. An interesting point is that the returned address is determined by a dynamic condition. After the call, we get a mutable reference mc, which points to the integer object of either a or b. The reference mc owns the update permission until the end of the inner block; this property is inherited from ma and mb. In line 6, mc increments the integer object by *mc += 1. Just after the deadline (in line 7), a and b retrieve the update permission to their integer objects and thus can read from them. Here, we check if a != b holds. The property we want to verify on this program is that inc_max returns true for any inputs a and b. It holds because, by incrementing the larger side of a and b, the difference between a and b increases by one.

Fig. 1.

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 take_max/inc_max program, then we would get CHCs of the following form:

The problem is, we do not know how to represent the values of a and b after the deadline of the borrows. There is no way to fill the parts ? in the second CHC. So, we need a better way to model mutable references.

The key idea of our method is to represent a mutable reference ma as a pair consisting of the values of the target object of *ma at two time points — the current value 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:

The mutable reference ma is now represented as , and similarly for mb and mc. In the then part of the first CHC, we have the constraint , because now we throw away mb and thus the final target value of mb is now set to the current target value . The constraint corresponds to return ma in the program. The same reasoning applies to the else part of the first CHC. In the second CHC, the mutable reference mc is modeled as the pair . After incrementing the value of mc (expressed by ), the borrowed update permission of mc is released, which is expressed by . Now, the final check a != b is simply modeled as , because the new values of a and b are available as 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 inc_max(5, 3) (as in Figure 1), the pointers ma and mb passed to take_max are modeled as 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 just_rec discussed in Section 1.1 into the following system of pretty simple CHCs.

This CHC system has a very simple solution , 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 take_max/inc_max program discussed earlier. In Rust, the program is written as follows: To aid understanding, we added some ghost annotations in cyan.

The type i32 represents a (32-bit) integer. The type &’a mut i32 represents a mutable reference to an integer that is governed under the lifetime ’a, which represents the deadline of a borrow.6 In Rust, the permission of each pointer is expressed in the type. The function take_max takes two integer mutable references of some lifetime ’a and returns an integer mutable reference of the lifetime ’a (the function is parametrized over ’a). In the function inc_max, we perform borrowing. The time point at the end of the inner scope is named ’l here. We mutably borrow the integer variables a and b under this lifetime ’l, and pass them to the function take_max. The output mc has the type &’l mut i32.7

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 take_max, we jump from T0 to either T1 or T2 depending on the condition *ma >= *mb. In the function inc_max, the function call let mc = take_max(&mut a, &mut b); is decomposed into three instructions, namely, those at I0, I1 and I2. For convenience of explanation, we set a program point I4 at the end of the inner scope.

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 inc_max with the type context assigned to each program point.

The variable a temporarily loses the permission on its integer object until the deadline of the borrow ’l, which we say that a is frozen under the lifetime ’l. A similar thing applies to b. The type context has the information about which variables are frozen under which lifetime. (In the notation used above, a:[’l] i32 means that a is typed i32 but frozen under ’l.) When we move from I4 to I5, the lifetime ’l comes and thus the variables a and b retrieve the permission.

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 &mut i32 like ma is modeled as a pair of the current and final integer values, whereas an integer variable like a, which is essentially a pointer to an integer object, is modeled simply as its target value. In our formalized reduction, for each program point, we introduce a predicate variable and generate a CHC that models the instruction at the point. For example, the function take_max is reduced to the following CHCs, where three predicate variables , , and represent the program points T0, T1, and T2:

The predicate variable for each program point models the relation between the values of the local variables in that point and the return value of the function the point belongs to. For example, the predicate variable models the relation between the values of ma and mb () and the return value of take_max (). At T1, we release a mutable reference mb, which is modeled as . To ensure that what we took as the final value agrees with the actual final value, we add the constraint here. The function inc_max is reduced to the following CHCs:
At I0, we borrow a and obtain a mutable reference ma. Here, we take a fresh variable 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 ma as . Now, we can simply model a as here, because the type system ensures that a cannot be accessed, or is frozen, until the deadline of the borrow ’l. At I3, we perform a destructive update, incrementing the target integer of mc. Here, letting be the value of mc at I3, we set mc’s value at I4 to . At I5, we can access a and b now because the lifetime ’l is over. We can simply use the first argument of I_5 for the value of a here, because it was set to the final target value when we performed the borrow at I0.

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.

Skip 2FORMALIZATION OF RUST: CALCULUS OF OWNERSHIP AND REFERENCE Section

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:

We also use a meta-variable 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 main function in Rust and C/C++).

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 Box<T>. It can freely update, read, and release its target object. As informally explained in Section 1.2, a mutable or immutable reference is a pointer that targets an object owned by some owning pointer and has the update or read permission to the object under until some lifetime. We use lifetime variables 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 Box<T> (or simply T). The types of a mutable reference and an immutable reference are and , which correspond to &’a mut T and &’a T in Rust.

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.

Remark 1

(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 get_default in “Problem Case #3” of Reference [57].

Example 1

(Program).

The Rust program with take_max and inc_max presented in Section 1.2 is modeled as follows in this calculus:

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 :

In short, 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:

The initial whole context at 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:

For any program , 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.

Remark 2

(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:

The configuration consists of stack frames and a heap memory. Each stack frame is accompanied by , 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:

Although we do not define the type size for a type variable , 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.

Example 2

(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.

In the stack frames each variable just has the address data. Integer objects are all stored in the heap memory.

Skip 3OUR REDUCTION FROM RUST PROGRAMS TO CHCS Section

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.

We abbreviate 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:

Although the definition is partial on , 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:

Finally, the validity of a CHC system 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):

Here, 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:

Here, 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 swap_dec).

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 .

Example 3

(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: .

The essence of this CHC system is the same as what we informally presented in Section 1.2. Note that here we use pattern matching to eliminate equalities, unlike the informal description.

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.

A weak abstract configuration is similar to a concrete configuration in the operational semantics, but maps each variable to a value and gets rid of the heap memory. The configuration has only one stack frame, since we target only the initial and final states of a function call. We also introduce some auxiliary notions. An access mode 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:

Using this, we introduce the judgment 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:

Theorem 1 (Soundness and Completeness of Our Reduction).

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.

Outline of the Proof

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.

Example 4.

Let us consider the following Rust program, which is a variant of just_rec in Section 1.1.

Unlike just_rec, the function linger_dec can modify the local variable of an arbitrarily deep ancestor. Each recursive call to linger_dec can introduce a new lifetime for mb, so arbitrarily many layers of lifetimes can be yielded.

The Rust program above can be expressed in COR as follows:

For brevity, we admitted here the following features: the non-deterministic branching statement (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 linger_dec never returns false. If we use, like 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:

This can be solved by many CHC solvers, since it has a very simple solution like below.

Example 5.

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 List<T>, which is a common recursive data type. The function take_some takes a mutable reference to an integer list and returns a mutable reference to some element of the list. The function sum calculates the sum of the elements of a list. The function inc_some increments some element of the input list using a mutable reference and checks that the sum of the elements of the list has increased by 1.

The Rust program above can be expressed in COR as follows:

Here, 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 inc_some never returns false. Our method reduces this verification problem into the following system of CHCs:

Here, 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 take_some. In the function take_some the mutable reference mla can be subdivided into mutable references to the head and tail of the list, which is expressed in the first CHC by the constraint .

We can give this CHC system a very simple solution, using an auxiliary recursive function defined by and .

The validity of the solution can be checked without induction about ; 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 inc_some, HoIce found the recursive function 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 a’ = a and cut off execution branches that do not pass the check.

For example, take_max/inc_max in Section 1.2 and Example 1 can be reduced into the following OCaml program:

Here, the bindings let a’ = Random.int(0) and let b’ = Random.int(0) take the future target values with random guesses, and the assumption checks assume (b’ = b), assume (a’ = a) and assume (c’ = c + 1) model the check of the random guesses. The original problem “Does inc_max never return false?” on the Rust program is reduced to the problem “Does main never fail at the assertion?” on the OCaml representation above. Notably, MoCHi [41], a higher-order model checker for OCaml, successfully verified the safety property for the OCaml representation above. It also successfully and instantly verified a similar OCaml representation of the Rust program of linger_dec presented at Example 4.

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 Vec<T> [64] can simply be represented as a functional array. In particular, we can model Vec::index_mut(self: &mut Vec<T>, idx: usize) -> &mut T, a function that takes out a mutable reference to some element of a vector out of a mutable reference to a vector. Also, we can support mutable iterators on a vector. Similarly, we can also support data structures like a hash map HashMap<K, V> [60]. We can also support some concurrency libraries like thread::spawn [63].

However, Rust libraries such as RefCell<T> [59] and Mutex<T> [62] impose challenges to our method, because they introduce shared mutable states (or more technically, interior mutability). A naive approach is to pass around the global memory state for such data types. Here, let us discuss how to support RefCell<T>, which is a memory cell that attains dynamic permission control by reference counting and allows us to build data structures with circular references. We can model each instance of RefCell as an index and pass around the global array that maps each index of a RefCell<T> instance to a pair of the body value and the reference counter. To take a mutable or immutable reference from RefCell, we check and update the counter and take out the value from the array. At the time we take a mutable reference from a RefCell<T>, the body value in the global array should be updated into . This precisely models RefCell, but handling indices and the global array can be costly. We can also think of separating the array into smaller parts by methods such as region-based type systems and pointer analysis.

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 RefCell [59] and reference-counting garbage collection by Rc [61], we can cause a memory leak by isolating some cycle. When a leaked object has a mutable reference, we can fail at determining the final target value of it, which makes our method incomplete. Still, we do not think this is a major problem, because our method is still sound and in general program behaviors with memory leaks are very hard to verify any way.

Skip 4PROOF OF THE SOUNDNESS AND COMPLETENESS OF OUR REDUCTION Section

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)

should have one or more pattern formulas on the left side. Let . Take from any CHC whose head formula unifies with . Namely, is of the form and unifies with . Take the most general unifier on and , such that holds for each . Here, and are finite maps from variables to patterns. Now, we have a pre-configuration .

(2)

Now, we calculate and specialize until we remove all operators and all variables that appear only once (which we call orphaned variables) in . By that, we obtain a configuration out of the pre-configuration . Then, we judge that holds.

More precisely, the calculation and specialization process repeats the following operations (the order of the operations can actually be freely chosen).

  • We replace a term of the form with the integer taken by .

  • An orphaned variable in the pre-configuration is replaced with any value of the suitable sort.11

  • When in the pre-configuration there occurs a term of form or for some variable , we globally replace with any integer .12

Lemma 2 (Completeness of SLDC Resolution).

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:

Here, “ unifies to ” means that replacing variables in with some values we obtain .

Proof.

Similar to the proof of completeness of SLD resolution [43].□

Example 6

(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.

In the third line (), 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:

They correspond to a concrete configuration 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:

A logic variable summary records how logic variables are used in extracting patterns from a concrete configuration. An extended memory footprint is a finite multiset of items of the form described below:
The activeness 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:

It is an extension of the judgment 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:

It is an extension of the judgment 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 :

For each logic variable 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 :

This judgment is an extension of , 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.

Proposition 3 (Safety on A Concrete Configuration Ensures Progress).

For any and , if holds and does not hold, then there exists some satisfying .

Proof.

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:

Here, 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.

Theorem 4 (Bisimulation between Execution and SLDC Resolution under 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.

Proof.

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).

Corollary 5 (Safety on A Concrete Configuration is Preserved by Transition).

For any , and , if and hold, then holds.

Proof.

It follows from Theorem 4, because the judgment is equivalent to .□

Before completing the proof of Theorem 1, we show a few simple lemmas.

Lemma 6 (Equivalence Between the Basic and Extended Extraction-Examination Judgments).

For any simple function in a program , for any concrete configuration of form , satisfying either or , the following equivalence holds:

Proof.

It can be proved by straightforward induction.□

Lemma 7 (Uniqueness on the Basic Extraction-Examination Judgments).

For any and , there exists at most one such that holds.

Proof.

Clear from the definition.□

Lemma 8 (Construction of A Concrete Configuration from A Weak Abstract Configuration).

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.

Proof.

By straightforward construction.□

Now, we complete the proof of Theorem 1, the soundness and completeness of our reduction.

Proof of Theorem 1

We show each direction of the implication.

Necessity ( implies ). There exists a sequence of concrete configurations such that the following judgments hold:

By Lemma 6, by setting and , the following judgments hold:
By Theorem 4, we have a sequence of resolutive configuration such that the following judgments hold:
By Lemma 6 and Lemma 7, we have . 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:

By the definition of the CHC representation , 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:
Thus, we also have the following judgments:
Therefore, holds.□

Skip 5IMPLEMENTATION AND EVALUATION Section

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 !loop !). The column Mut? shows whether the verified program uses mutable references. The benchmarks and experimental results are available at https://doi.org/10.5281/zenodo.4710723.

Table 1.
RustHornSeaHorn w/Spacer
GroupInstanceSafe?LOCLoop?Mut?w/Spacerw/HoIceas ismodified
simple01safe12yesno<0.10.1<0.1
04-recursivesafe14yesno0.5timeout0.8
05-recursiveunsafe26yesno<0.1<0.1<0.1
06-loopsafe10yesnotimeout0.1timeout
hhk2008safe20yesnotimeout47.9<0.1
unique-scalarunsafe9noyes<0.10.3<0.1
bmc1safe46nono0.2<0.1<0.1
unsafe0.2<0.1<0.1
2safe15yesnotimeout0.1<0.1
unsafe<0.10.1<0.1
3safe35yesno0.1<0.1<0.1
unsafe<0.1<0.1<0.1
diamond-1safe55nono0.1<0.1<0.1
unsafe<0.1<0.1<0.1
diamond-2safe40nono0.2<0.1<0.1
unsafe0.1<0.1<0.1
prustiackermannsafe17yesno<0.10.1<0.1
ackermann-samesafe26yesnotimeouttimeouttimeout
compresssafe12noyes<0.10.1false alarm
borrows-alignsafe14noyes<0.10.1<0.1
accountsafe18noyes<0.10.1<0.1
unsafe<0.10.2<0.1
restoresafe14noyes<0.10.3false alarm
inc-maxbasesafe15noyes<0.10.2false alarm<0.1
unsafe<0.10.2<0.1<0.1
base3safe22noyes<0.10.2false alarm
unsafe<0.10.2<0.1
repeatsafe22yesyes0.1timeoutfalse alarmtimeout
unsafe<0.10.5<0.1<0.1
repeat3safe29yesyes0.2timeoutfalse alarm
unsafe<0.11.4<0.1
swap-decbasesafe23yesyes0.10.5false alarm<0.1
unsafe0.1timeout<0.10.1
base3safe27yesyes0.1timeoutfalse alarm<0.1
unsafe0.414.2<0.10.1
exactsafe24yesyes0.10.6false alarm0.2
unsafe<0.1timeout<0.1<0.1
exact3safe30yesyestimeouttimeoutfalse alarm0.2
unsafe0.12.2<0.1<0.1
swap2-decbasesafe24yesyes0.40.8false alarm<0.1
unsafe1.2timeout<0.10.1
base3safe32yesyes1.8timeoutfalse alarm<0.1
unsafe67.039.0<0.10.2
exactsafe25yesyes1.01.0false alarm0.2
unsafe<0.1timeout<0.1<0.1
exact3safe35yesyestimeouttimeoutfalse alarm1.3
unsafe<0.16.2<0.1<0.1
just-recbasesafe14yesyes<0.10.2<0.1
unsafe<0.10.2<0.1
linger-decbasesafe15yesyes<0.10.2false alarm
unsafe<0.10.2<0.1
base3safe29yesyes<0.10.3false alarm
unsafe<0.131.9<0.1
exactsafe16yesyes<0.10.3false alarm
unsafe<0.10.3<0.1
exact3safe30yesyes<0.10.4false alarm
unsafe<0.11.3<0.1
listsappendsafe27yesyestool error0.3false alarm
unsafetool error0.30.1
inc-allsafe35yesyestool error0.3false alarm
unsafetool error0.4<0.1
inc-somesafe32yesyestool error0.3timeout
unsafetool error0.60.1
inc-some2safe34yesyestool errortimeouttimeout
unsafetool error0.70.3
treesappendsafe35yesyestool error0.4false alarm
unsafetool error0.3<0.1
inc-allsafe36yesyestool errortimeouttimeout
unsafetool error0.20.1
inc-somesafe34yesyestool error0.4timeout
unsafetool error0.70.1
inc-some2safe36yesyestool errortool errortimeout
unsafetool error0.9timeout

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 linger-dec, lists and trees, because address freshness check is quite hard to model. For inc-max/base3 and inc-max/repeat3, we could not make the modified versions, because SeaHorn wrongly omitted all the memory manipulation in the CHC outputs for them, probably by inaccurate pointer analyses.

Below, we explain our benchmarks more in detail.

The benchmarks in the groups simple and bmc were taken from those of SeaHorn ( https://github.com/seahorn/seahorn/tree/master/test). They are originally provided in C and the Rust versions were written by us. The benchmarks in SeaHorn were chosen based upon the following criteria: They (i) consist only of features supported by the core of Rust (covered by RustHorn), (ii) follow Rust’s permission discipline, and (iii) are small enough to be amenable for manual translation from C to Rust. We omitted SeaHorn tests that use arrays (such as simple/02_array) and function pointers (such as devirt/devirt_02), in light of (i). For an example of (ii), the following SeaHorn test dsa/test-1 was not included, because here the two pointers a and b can simultaneously point at the same object y with update permission:

Also, in light of (iii), we omitted large SeaHorn tests, such as bmc/cdaudio_simpl1. The following benchmark simple/hhk2008 is a SeaHorn test that was adopted in our experiments. The key challenge of this program is to find out an invariant on the while loop.

The benchmarks in the group prusti were taken from tests of Prusti [3], a semi-automated verification tool for Rust (available at https://github.com/viperproject/prusti-dev). We chose several small, interesting benchmarks from Prusti’s tests. For example, restore features a mutable reference to a randomly chosen object.

The benchmarks in the remaining seven groups were made by us featuring various use cases of mutable references. The benchmarks in the groups inc-max, just-rec, and linger-dec are based on the examples in Section 1 and Section 3.4.

The group swap-dec consists of benchmark programs that perform repeated and involved updates via mutable references to a mutable reference to an integer. For example, below is the safe program of the instance swap-dec/base, with some details modified for readability. The verified property is that the function test always returns true whenever it terminates.

The group swap2-dec is an advanced variant of swap-dec. It features a mutable reference to a mutable reference to a mutable reference to an integer. For example, the safe program of swap2-dec/base is as follows (may_swap is the same as above):

The instances labeled repeat in the group inc-max repeat the operation of the function inc_max n times, for some random number n. The instances labeled exact in the groups swap-dec, swap2-dec, and linger-dec not only observe the decrease but also check the amount of the decrease. For example, in the safe program of swap-dec/exact, the last check is a0 >= a && a0 - a <= 2 * n, which has the condition a0 - a <= 2 * n unlike swap-dec/base.

The groups lists and trees feature destructive updates on recursive data structures (singly linked lists and binary trees) via mutable references. The instance lists/inc-some has appeared as Example 5. The safe program of the instance lists/append is as follows:

The safe program of the instance lists/inc-all is as follows (List and sum are the same as above):

The instance lists/inc-some2 is an advanced variant of inc-some of Example 5. Its program with the safe property is presented below (List and sum are the same as above). The function test takes mutable references to some two elements of the input list and performs increment on them.

Benchmarks in the group trees are analogous to those in the group lists but designed for binary trees instead of lists.

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 (lists and trees), including lists/inc-some and trees/inc-some. Still, it failed at some benchmarks such as lists/inc-some2 and trees/inc-all. This is because HoIce, unlike Spacer, can find models defined with primitive recursive functions for recursive data types, as discussed in Example 5 in Section 3.4.

SeaHorn, without our modification, issued false alarms for many programs in the last seven benchmark groups, from inc-max to trees. This is due to SeaHorn’s imprecise modeling of pointers and memory states, where freshness of pointer addresses is not fully specified. For our modified CHC outputs of SeaHorn, false alarms were not observed but verification timed out for one benchmark (although one timeout was observed), but we could not make modified versions for many of the benchmarks. For the last four groups from just-rec to trees, unboundedly many memory cells can be allocated, which imposes a fundamental challenge for the array-based reduction, as discussed in Section 1.1. SeaHorn succeeded in verification for just-rec by analyzing absence of effective destructive updates and generating CHCs without arrays, but for all other benchmarks of the safe property SeaHorn failed because of imprecise representation. RustHorn succeeded in verifying most benchmark programs in these groups.

For the benchmarks in the groups swap-dec and swap2-dec, RustHorn performed somewhat inefficiently compared to SeaHorn with our modification. This is presumably because the benchmarks in the groups feature nested mutable references, which are modeled as a big value in our reduction. The group swap-dec features a mutable reference to a mutable reference to an integer, which is modeled as a pair of pairs of integers, which has four integers in total. The group swap2-dec features a mutable reference to a mutable reference to a mutable reference to an integer (threefold!), which is modeled as a value that has eight integers in total. For SeaHorn, the benchmarks in swap-dec and swap2-dec are quite easy because only a limited number of addresses (up to six addresses) are used in each program. This indicates a disadvantage of our approach compared to the array-based approach. Still, we believe that in real-world Rust programs we do not use such nested mutable references so often.

Skip 6RELATED WORK Section

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 “UNKNOWN” (instead of “SAFE” or “UNSAFE”) for even simple programs such as the programs of the instance unique-scalar in simple and the instance basic in inc-max.

Verification for Rust. Whereas we have presented the first CHC-based (fully automated) verification method specially designed for Rust, 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., take_max in Section 1.2) [71], which can be easily handled by our method. However, our approach covers arbitrary pointer operations supported in the safe core of Rust, as discussed in Section 3.

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 Vec::index_mut. Still, this approach does not support some basic operations on mutable references, such as split of mutable references, unlike our RustHorn. We suppose that Viper’s reasoning based on fractional permission does not naturally match Rust’s lifetime-based permission control. However, our reduction of RustHorn is specially designed for Rust. As we discussed in Section 3.5, our reduction of Rust programs to CHCs can be applied to semi-automated verification where users can declare pre/post-conditions and loop invariants. This extension of our approach can work more nicely than their Viper-based approaches for a wide class of Rust programs.

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].

Skip 7CONCLUSION Section

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.

Skip ACKNOWLEDGMENTS Section

ACKNOWLEDGMENTS

We are grateful to the anonymous reviewers for insightful and helpful comments.

Footnotes

  1. 1 To be precise, int in C usually represents a 32-bit integer. However, in this article, we just consider unbounded integers for simplicity.

    Footnote
  2. 2 Note that this system can be straightforwardly transformed into the following system of standard CHCs:

    Footnote
  3. 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.

    Footnote
  4. 4 We examined SeaHorn in our experiments, which are reported in Section 5.

    Footnote
  5. 5 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.

    Footnote
  6. 6 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.

    Footnote
  7. 7 In Rust, we do not need to (and actually cannot) write annotations on local lifetimes, like ’l used above. The Rust compiler performs a very clever inference on local lifetimes.

    Footnote
  8. 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.

    Footnote
  9. 9 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.

    Footnote
  10. 10 We actually do not need to support the case (v), because we can perform before .

    Footnote
  11. 11 We need this rule for the random value instruction in establishing the bisimulation of Theorem 4.

    Footnote
  12. 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. 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 . We can always satisfy this by inserting a fresh no-op labeled statement just after the function call.

    Footnote
  14. 14 We used Z3 version 4.7.1 for the backend SMT solver of HoIce to deal well with recursive data types.

    Footnote

REFERENCES

  1. [1] Abadi Martín and Lamport Leslie. 1991. The existence of refinement mappings. Theor. Comput. Sci. 82, 2 (1991), 253284. DOI:DOI: DOI: https://doi.org/10.1016/0304-3975(91)90224-P Google ScholarGoogle ScholarDigital LibraryDigital Library
  2. [2] Alberti Francesco, Bruttomesso Roberto, Ghilardi Silvio, Ranise Silvio, and Sharygina Natasha. 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), Bjørner Nikolaj and Voronkov Andrei (Eds.), Vol. 7180. Springer, 4661. DOI:DOI: DOI: https://doi.org/10.1007/978-3-642-28717-6_7 Google ScholarGoogle ScholarCross RefCross Ref
  3. [3] Astrauskas Vytautas, Müller Peter, Poli Federico, and Summers Alexander J.. 2018. Leveraging Rust types for modular specification and verification. DOI:DOI: DOI: https://doi.org/10.3929/ethz-b-000311092Google ScholarGoogle Scholar
  4. [4] Baranowski Marek S., He Shaobo, and Rakamaric Zvonimir. 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, 528535. DOI:DOI: DOI: https://doi.org/10.1007/978-3-030-01090-4_32Google ScholarGoogle ScholarCross RefCross Ref
  5. [5] Barnett Mike, Fähndrich Manuel, Leino K. Rustan M., Müller Peter, Schulte Wolfram, and Venter Herman. 2011. Specification and verification: The Spec# experience. Commun. ACM 54, 6 (2011), 8191. DOI:DOI: DOI: https://doi.org/10.1145/1953122.1953145 Google ScholarGoogle ScholarDigital LibraryDigital Library
  6. [6] Bjørner Nikolaj, Gurfinkel Arie, McMillan Kenneth L., and Rybalchenko Andrey. 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), Beklemishev Lev D., Blass Andreas, Dershowitz Nachum, Finkbeiner Bernd, and Schulte Wolfram (Eds.), Vol. 9300. Springer, 2451. DOI:DOI: DOI: https://doi.org/10.1007/978-3-319-23534-9_2Google ScholarGoogle Scholar
  7. [7] Bornat Richard, Calcagno Cristiano, O’Hearn Peter W., and Parkinson Matthew J.. 2005. Permission accounting in separation logic. In Proceedings of the 32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, Palsberg Jens and Abadi Martín (Eds.). ACM, 259270. DOI:DOI: DOI: https://doi.org/10.1145/1040305.1040327 Google ScholarGoogle ScholarCross RefCross Ref
  8. [8] Boyapati Chandrasekhar, Lee Robert, and Rinard Martin C.. 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, Ibrahim Mamdouh and Matsuoka Satoshi (Eds.). ACM, 211230. DOI:DOI: DOI: https://doi.org/10.1145/582419.582440 Google ScholarGoogle ScholarCross RefCross Ref
  9. [9] Boyland John. 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), Cousot Radhia (Ed.), Vol. 2694. Springer, 5572. DOI:DOI: DOI: https://doi.org/10.1007/3-540-44898-5_4 Google ScholarGoogle ScholarCross RefCross Ref
  10. [10] Bradley Aaron R., Manna Zohar, and Sipma Henny B.. 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), Emerson E. Allen and Namjoshi Kedar S. (Eds.), Vol. 3855. Springer, 427442. DOI:DOI: DOI: https://doi.org/10.1007/11609773_28 Google ScholarGoogle ScholarCross RefCross Ref
  11. [11] Burn Toby Cathcart, Ong C.-H. Luke, and Ramsay Steven J.. 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 ScholarGoogle ScholarCross RefCross Ref
  12. [12] Champion Adrien, Chiba Tomoya, Kobayashi Naoki, and Sato Ryosuke. 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), Beyer Dirk and Huisman Marieke (Eds.), Vol. 10805. Springer, 365384. DOI:DOI: DOI: https://doi.org/10.1007/978-3-319-89960-2_20Google ScholarGoogle Scholar
  13. [13] Champion Adrien, Kobayashi Naoki, and Sato Ryosuke. 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), Ryu Sukyoung (Ed.), Vol. 11275. Springer, 146156. DOI:DOI: DOI: https://doi.org/10.1007/978-3-030-02768-1_8Google ScholarGoogle ScholarCross RefCross Ref
  14. [14] Clarke David G., Potter John, and Noble James. 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, Freeman-Benson Bjørn N. and Chambers Craig (Eds.). ACM, 4864. DOI:DOI: DOI: https://doi.org/10.1145/286936.286947 Google ScholarGoogle ScholarCross RefCross Ref
  15. [15] Cohen Ernie, Dahlweid Markus, Hillebrand Mark A., Leinenbach Dirk, Moskal Michal, Santen Thomas, Schulte Wolfram, and Tobies Stephan. 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), Berghofer Stefan, Nipkow Tobias, Urban Christian, and Wenzel Makarius (Eds.), Vol. 5674. Springer, 2342. DOI:DOI: DOI: https://doi.org/10.1007/978-3-642-03359-9_2 Google ScholarGoogle ScholarCross RefCross Ref
  16. [16] Team Coq. 2021. The Coq Proof Assistant. Retrieved from https://coq.inria.fr/.Google ScholarGoogle Scholar
  17. [17] Moura Leonardo Mendonça de, Kong Soonho, Avigad Jeremy, Doorn Floris van, and Raumer Jakob von. 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), Felty Amy P. and Middeldorp Aart (Eds.), Vol. 9195. Springer, 378388. DOI:DOI: DOI: https://doi.org/10.1007/978-3-319-21401-6_26Google ScholarGoogle ScholarCross RefCross Ref
  18. [18] Dropbox. 2020. Rewriting the Heart of Our Sync Engine - Dropbox. Retrieved from https://dropbox.tech/infrastructure/rewriting-the-heart-of-our-sync-engine.Google ScholarGoogle Scholar
  19. [19] Erdin Matthias. 2019. Verification of Rust Generics, Typestates, and Traits. Master’s thesis. ETH Zürich.Google ScholarGoogle Scholar
  20. [20] Fedyukovich Grigory, Kaufman Samuel J., and Bodík Rastislav. 2017. Sampling invariants from frequency distributions. In 2017 Formal Methods in Computer Aided Design, FMCAD 2017, Vienna, Austria, October 2–6, 2017, Stewart Daryl and Weissenbacher Georg (Eds.). IEEE, 100107. DOI:DOI: DOI: https://doi.org/10.23919/FMCAD.2017.8102247 Google ScholarGoogle ScholarCross RefCross Ref
  21. [21] Fedyukovich Grigory, Prabhu Sumanth, Madhukar Kumar, and Gupta Aarti. 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), Dillig Isil and Tasiran Serdar (Eds.), Vol. 11561. Springer, 259277. DOI:DOI: DOI: https://doi.org/10.1007/978-3-030-25540-4_14Google ScholarGoogle ScholarCross RefCross Ref
  22. [22] Foster J. Nathan, Greenwald Michael B., Moore Jonathan T., Pierce Benjamin C., and Schmitt Alan. 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 ScholarGoogle ScholarCross RefCross Ref
  23. [23] Gondelman Léon. 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 ScholarGoogle Scholar
  24. [24] Grebenshchikov Sergey, Lopes Nuno P., Popeea Corneliu, and Rybalchenko Andrey. 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, Vitek Jan, Lin Haibo, and Tip Frank (Eds.). ACM, 405416. DOI:DOI: DOI: https://doi.org/10.1145/2254064.2254112 Google ScholarGoogle ScholarCross RefCross Ref
  25. [25] Gurfinkel Arie, Kahsai Temesghen, Komuravelli Anvesh, and Navas Jorge A.. 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), Kroening Daniel and Pasareanu Corina S. (Eds.), Vol. 9206. Springer, 343361. DOI:DOI: DOI: https://doi.org/10.1007/978-3-319-21690-4_20Google ScholarGoogle ScholarCross RefCross Ref
  26. [26] Gurfinkel Arie and Navas Jorge A.. 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), Ranzato Francesco (Ed.), Vol. 10422. Springer, 148168. DOI:DOI: DOI: https://doi.org/10.1007/978-3-319-66706-5_8Google ScholarGoogle ScholarCross RefCross Ref
  27. [27] Gurfinkel Arie, Shoham Sharon, and Meshman Yuri. 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, Zimmermann Thomas, Cleland-Huang Jane, and Su Zhendong (Eds.). ACM, 338348. DOI:DOI: DOI: https://doi.org/10.1145/2950290.2950330 Google ScholarGoogle ScholarCross RefCross Ref
  28. [28] Gurfinkel Arie, Shoham Sharon, and Vizel Yakir. 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, 248266. DOI:DOI: DOI: https://doi.org/10.1007/978-3-030-01090-4_15Google ScholarGoogle ScholarCross RefCross Ref
  29. [29] Hahn Florian. 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 ScholarGoogle Scholar
  30. [30] Hoenicke Jochen, Majumdar Rupak, and Podelski Andreas. 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, Castagna Giuseppe and Gordon Andrew D. (Eds.). ACM, 473485. DOI:DOI: DOI: https://doi.org/10.1145/3009837 Google ScholarGoogle ScholarCross RefCross Ref
  31. [31] Hojjat Hossein and Rümmer Philipp. 2018. The eldarica Horn solver. In 2018 Formal Methods in Computer Aided Design, FMCAD 2018, Austin, TX, USA, October 30–November 2, 2018, Bjørner Nikolaj and Gurfinkel Arie (Eds.). IEEE, 17. DOI:DOI: DOI: https://doi.org/10.23919/FMCAD.2018.8603013Google ScholarGoogle Scholar
  32. [32] Horn Alfred. 1951. On sentences which are true of direct unions of algebras. J. Symbol. Log. 16, 1 (1951), 1421. Retrieved from http://www.jstor.org/stable/2268661.Google ScholarGoogle ScholarCross RefCross Ref
  33. [33] Jim Trevor, Morrisett J. Gregory, Grossman Dan, Hicks Michael W., Cheney James, and Wang Yanling. 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, Ellis Carla Schlatter (Ed.). USENIX, 275288. Retrieved from http://www.usenix.org/publications/library/proceedings/usenix02/jim.html. Google ScholarGoogle ScholarDigital LibraryDigital Library
  34. [34] Jung Ralf, Jourdan Jacques-Henri, Krebbers Robbert, and Dreyer Derek. 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 ScholarGoogle ScholarCross RefCross Ref
  35. [35] Jung Ralf, Krebbers Robbert, Jourdan Jacques-Henri, Bizjak Ales, Birkedal Lars, and Dreyer Derek. 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 ScholarGoogle ScholarCross RefCross Ref
  36. [36] Jung Ralf, Lepigre Rodolphe, Parthasarathy Gaurav, Rapoport Marianna, Timany Amin, Dreyer Derek, and Jacobs Bart. 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 ScholarGoogle ScholarCross RefCross Ref
  37. [37] Jung Ralf, Swasey David, Sieczkowski Filip, Svendsen Kasper, Turon Aaron, Birkedal Lars, and Dreyer Derek. 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, Rajamani Sriram K. and Walker David (Eds.). ACM, 637650. DOI:DOI: DOI: https://doi.org/10.1145/2676726.2676980 Google ScholarGoogle ScholarCross RefCross Ref
  38. [38] Kahsai Temesghen, Kersten Rody, Rümmer Philipp, and Schäf Martin. 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), Eiter Thomas and Sands David (Eds.), Vol. 46. EasyChair, 368384.Google ScholarGoogle Scholar
  39. [39] Kahsai Temesghen, Rümmer Philipp, Sanchez Huascar, and Schäf Martin. 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), Chaudhuri Swarat and Farzan Azadeh (Eds.), Vol. 9779. Springer, 352358. DOI:DOI: DOI: https://doi.org/10.1007/978-3-319-41528-4_19Google ScholarGoogle ScholarCross RefCross Ref
  40. [40] Kalra Sukrit, Goel Seep, Dhawan Mohan, and Sharma Subodh. 2018. Zeus: Analyzing safety of smart contracts. In Proceedings of the 25th Annual Network and Distributed System Security Symposium. The Internet Society.Google ScholarGoogle ScholarCross RefCross Ref
  41. [41] Kobayashi Naoki, Sato Ryosuke, and Unno Hiroshi. 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, Hall Mary W. and Padua David A. (Eds.). ACM, 222233. DOI:DOI: DOI: https://doi.org/10.1145/1993498.1993525 Google ScholarGoogle ScholarCross RefCross Ref
  42. [42] Komuravelli Anvesh, Gurfinkel Arie, and Chaki Sagar. 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), Biere Armin and Bloem Roderick (Eds.), Vol. 8559. Springer, 1734. DOI:DOI: DOI: https://doi.org/10.1007/978-3-319-08867-9_2 Google ScholarGoogle ScholarCross RefCross Ref
  43. [43] Kowalski Robert A.. 1974. Predicate logic as programming language. In Information Processing, Proceedings of the 6th IFIP Congress 1974, Stockholm, Sweden, August 5–10, 1974, Rosenfeld Jack L. (Ed.). North-Holland, 569574.Google ScholarGoogle Scholar
  44. [44] Lahiri Shuvendu K. and Bryant Randal E.. 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), Steffen Bernhard and Levi Giorgio (Eds.), Vol. 2937. Springer, 267281. DOI:DOI: DOI: https://doi.org/10.1007/978-3-540-24622-0_22Google ScholarGoogle ScholarCross RefCross Ref
  45. [45] Lahiri Shuvendu K. and Wang Chao (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 ScholarGoogle ScholarCross RefCross Ref
  46. [46] Lindner Marcus, Aparicius Jorge, and Lindgren Per. 2018. No panic! Verification of Rust programs by symbolic execution. In Proceedings of the 16th IEEE International Conference on Industrial Informatics. IEEE, 108114. DOI:DOI: DOI: https://doi.org/10.1109/INDIN.2018.8471992Google ScholarGoogle ScholarCross RefCross Ref
  47. [47] Matsakis Nicholas D. and II Felix S. Klock,. 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, Feldman Michael and Taft S. Tucker (Eds.). ACM, 103104. DOI:DOI: DOI: https://doi.org/10.1145/2663171.2663188 Google ScholarGoogle ScholarCross RefCross Ref
  48. [48] Matsushita Yusuke. 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 ScholarGoogle Scholar
  49. [49] Matsushita Yusuke, Tsukada Takeshi, and Kobayashi Naoki. 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), Müller Peter (Ed.), Vol. 12075. Springer, 484514. DOI:DOI: DOI: https://doi.org/10.1007/978-3-030-44914-8_18Google ScholarGoogle Scholar
  50. [50] Microsoft. 2021. Boogie: An Intermediate Verification Language. Retrieved from https://www.microsoft.com/en-us/research/project/boogie-an-intermediate-verification-language/.Google ScholarGoogle Scholar
  51. [51] Mozilla. 2021. Rust language — Mozilla Research. Retrieved from https://research.mozilla.org/rust/.Google ScholarGoogle Scholar
  52. [52] Müller Peter, Schwerhoff Malte, and Summers Alexander J.. 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), Jobstmann Barbara and Leino K. Rustan M. (Eds.), Vol. 9583. Springer, 4162. DOI:DOI: DOI: https://doi.org/10.1007/978-3-662-49122-5_2 Google ScholarGoogle ScholarCross RefCross Ref
  53. [53] npm. 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 ScholarGoogle Scholar
  54. [54] Community Rust. 2021. The MIR (Mid-level IR). Retrieved from https://rustc-dev-guide.rust-lang.org/mir/index.html.Google ScholarGoogle Scholar
  55. [55] Community Rust. 2021. Reference Cycles Can Leak Memory - The Rust Programming Language. Retrieved from https://doc.rust-lang.org/book/ch15-06-reference-cycles.html.Google ScholarGoogle Scholar
  56. [56] Community Rust. 2021. RFC 2025: Nested Method Calls. Retrieved from https://rust-lang.github.io/rfcs/2025-nested-method-calls.html.Google ScholarGoogle Scholar
  57. [57] Community Rust. 2021. RFC 2094: Non-lexical Lifetimes. Retrieved from https://rust-lang.github.io/rfcs/2094-nll.html.Google ScholarGoogle Scholar
  58. [58] Community Rust. 2021. Rust Programming Language. Retrieved from https://www.rust-lang.org/.Google ScholarGoogle Scholar
  59. [59] Community Rust. 2021. std::cell::RefCell - Rust. Retrieved from https://doc.rust-lang.org/std/cell/struct.RefCell.html.Google ScholarGoogle Scholar
  60. [60] Community Rust. 2021. std::collections::HashMap - Rust. Retrieved from https://doc.rust-lang.org/std/collections/struct.HashMap.html.Google ScholarGoogle Scholar
  61. [61] Community Rust. 2021. std::rc::Rc - Rust. Retrieved from https://doc.rust-lang.org/std/rc/struct.Rc.html.Google ScholarGoogle Scholar
  62. [62] Community Rust. 2021. std::sync::Mutex - Rust. Retrieved from https://doc.rust-lang.org/std/sync/struct.Mutex.html.Google ScholarGoogle Scholar
  63. [63] Community Rust. 2021. std::thread::spawn - Rust. Retrieved from https://doc.rust-lang.org/std/thread/fn.spawn.html.Google ScholarGoogle Scholar
  64. [64] Community Rust. 2021. std::vec::Vec - Rust. Retrieved from https://doc.rust-lang.org/std/vec/struct.Vec.html.Google ScholarGoogle Scholar
  65. [65] Community Rust. 2021. Two-phase borrows. Retrieved from https://rust-lang.github.io/rustc-guide/borrow_check/two_phase_borrows.html.Google ScholarGoogle Scholar
  66. [66] Sato Ryosuke, Iwayama Naoki, and Kobayashi Naoki. 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, Hermenegildo Manuel V. and Igarashi Atsushi (Eds.). ACM, 4753. DOI:DOI: DOI: https://doi.org/10.1145/3294032.3294081 Google ScholarGoogle ScholarCross RefCross Ref
  67. [67] Stump Aaron, Barrett Clark W., Dill David L., and Levitt Jeremy R.. 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, 2937. DOI:DOI: DOI: https://doi.org/10.1109/LICS.2001.932480 Google ScholarGoogle ScholarCross RefCross Ref
  68. [68] Suenaga Kohei and Kobayashi Naoki. 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), Hu Zhenjiang (Ed.), Vol. 5904. Springer, 128143. DOI:DOI: DOI: https://doi.org/10.1007/978-3-642-10672-9_11 Google ScholarGoogle ScholarCross RefCross Ref
  69. [69] Terauchi Tachio. 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, Gupta Rajiv and Amarasinghe Saman P. (Eds.). ACM, 110. DOI:DOI: DOI: https://doi.org/10.1145/1375581.1375583 Google ScholarGoogle ScholarCross RefCross Ref
  70. [70] Toman John, Pernsteiner Stuart, and Torlak Emina. 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, Cohen Myra B., Grunske Lars, and Whalen Michael (Eds.). IEEE Computer Society, 7580. DOI:DOI: DOI: https://doi.org/10.1109/ASE.2015.77 Google ScholarGoogle ScholarCross RefCross Ref
  71. [71] Ullrich Sebastian. 2016. Electrolysis Reference. Retrieved from http://kha.github.io/electrolysis/.Google ScholarGoogle Scholar
  72. [72] Ullrich Sebastian. 2016. Simple Verification of Rust Programs via Functional Purification. Master’s thesis. Karlsruhe Institute of Technology.Google ScholarGoogle Scholar
  73. [73] Vafeiadis Viktor. 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 ScholarGoogle Scholar
  74. [74] Emden Maarten H. van and Kowalski Robert A.. 1976. The semantics of predicate logic as a programming language. J. ACM 23, 4 (1976), 733742. DOI:DOI: DOI: https://doi.org/10.1145/321978.321991 Google ScholarGoogle ScholarDigital LibraryDigital Library
  75. [75] Team Z3. 2021. The Z3 Theorem Prover. Retrieved from https://github.com/Z3Prover/z3.Google ScholarGoogle Scholar

Index Terms

  1. RustHorn: CHC-based Verification for Rust Programs

      Recommendations

      Comments

      Login options

      Check if you have access through your login credentials or your institution to get full access on this article.

      Sign in

      Full Access

      • Published in

        cover image ACM Transactions on Programming Languages and Systems
        ACM Transactions on Programming Languages and Systems  Volume 43, Issue 4
        December 2021
        272 pages
        ISSN:0164-0925
        EISSN:1558-4593
        DOI:10.1145/3492431
        Issue’s Table of Contents

        Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from [email protected].

        Publisher

        Association for Computing Machinery

        New York, NY, United States

        Publication History

        • Published: 31 October 2021
        • Revised: 1 April 2021
        • Accepted: 1 April 2021
        • Received: 1 May 2020
        Published in toplas Volume 43, Issue 4

        Permissions

        Request permissions about this article.

        Request Permissions

        Check for updates

        Qualifiers

        • research-article
        • Refereed

      PDF Format

      View or Download as a PDF file.

      PDF

      eReader

      View online with eReader.

      eReader

      HTML Format

      View this article in HTML Format .

      View HTML Format
      About Cookies On This Site

      We use cookies to ensure that we give you the best experience on our website.

      Learn more

      Got it!