Skip to content

Commit b410f4e

Browse files
btjcarolynzechremi-delmas-3000
authored
Add VeriFast CI (model-checking#239)
Adds a GitHub Actions workflow that downloads VeriFast and runs `verifast-proofs/check-verifast-proofs.mysh`. (`mysh` is a simple tool that ships with VeriFast for running simple scripts. It is optimized for running test suites (and used to run [VeriFast's test suite](https://github.com/verifast/verifast/blob/master/testsuite.mysh). Works on Linux, Mac, and Windows.) See model-checking#238 to see how this can be used to verify the `linked_list.rs` (Challenge 5) solution. Resolves model-checking#213 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses. --------- Co-authored-by: Carolyn Zech <[email protected]> Co-authored-by: Rémi Delmas <[email protected]>
1 parent 10424a1 commit b410f4e

File tree

21 files changed

+9759
-0
lines changed

21 files changed

+9759
-0
lines changed
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# This workflow runs some negative VeriFast test cases, to ensure
2+
# that VeriFast actually catches bugs.
3+
4+
name: VeriFast (negative)
5+
6+
on:
7+
workflow_dispatch:
8+
merge_group:
9+
pull_request:
10+
branches: [ main ]
11+
push:
12+
paths:
13+
- 'library/**'
14+
- '.github/workflows/verifast.yml'
15+
- 'verifast-proofs/**'
16+
17+
defaults:
18+
run:
19+
shell: bash
20+
21+
jobs:
22+
check-verifast-on-std:
23+
name: Verify std library
24+
runs-on: ubuntu-latest
25+
26+
steps:
27+
- name: Checkout Repository
28+
uses: actions/checkout@v4
29+
30+
- name: Install VeriFast
31+
run: |
32+
cd ~
33+
curl -OL https://github.com/verifast/verifast/releases/download/25.02/verifast-25.02-linux.tar.gz
34+
# https://github.com/verifast/verifast/attestations/4911733
35+
echo '5d5c87d11b3d735f44c3f0ca52aebc89e3c4d1119d98ef25188d07cb57ad65e8 verifast-25.02-linux.tar.gz' | shasum -a 256 -c
36+
tar xf verifast-25.02-linux.tar.gz
37+
38+
- name: Install the Rust toolchain used by VeriFast
39+
run: rustup toolchain install nightly-2024-11-23
40+
41+
- name: Run VeriFast Verification
42+
run: |
43+
export PATH=~/verifast-25.02/bin:$PATH
44+
cd verifast-proofs
45+
mysh check-verifast-proofs-negative.mysh

.github/workflows/verifast.yml

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: VeriFast
2+
3+
on:
4+
workflow_dispatch:
5+
merge_group:
6+
pull_request:
7+
branches: [ main ]
8+
push:
9+
paths:
10+
- 'library/**'
11+
- '.github/workflows/verifast.yml'
12+
- 'verifast-proofs/**'
13+
14+
defaults:
15+
run:
16+
shell: bash
17+
18+
jobs:
19+
check-verifast-on-std:
20+
name: Verify std library
21+
runs-on: ubuntu-latest
22+
23+
steps:
24+
- name: Checkout Repository
25+
uses: actions/checkout@v4
26+
27+
- name: Install VeriFast
28+
run: |
29+
cd ~
30+
curl -OL https://github.com/verifast/verifast/releases/download/25.02/verifast-25.02-linux.tar.gz
31+
# https://github.com/verifast/verifast/attestations/4911733
32+
echo '5d5c87d11b3d735f44c3f0ca52aebc89e3c4d1119d98ef25188d07cb57ad65e8 verifast-25.02-linux.tar.gz' | shasum -a 256 -c
33+
tar xf verifast-25.02-linux.tar.gz
34+
35+
- name: Install the Rust toolchain used by VeriFast
36+
run: rustup toolchain install nightly-2024-11-23
37+
38+
- name: Run VeriFast Verification
39+
run: |
40+
export PATH=~/verifast-25.02/bin:$PATH
41+
cd verifast-proofs
42+
mysh check-verifast-proofs.mysh

doc/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [Verification Tools](./tools.md)
1010
- [Kani](./tools/kani.md)
1111
- [GOTO Transcoder](./tools/goto-transcoder.md)
12+
- [VeriFast](./tools/verifast.md)
1213

1314
---
1415

doc/src/tools.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ please see the [Tool Application](general-rules.md#tool-applications) section.
1313
|---------------------|-------|
1414
| Kani Rust Verifier | [![Kani](https://github.com/model-checking/verify-rust-std/actions/workflows/kani.yml/badge.svg)](https://github.com/model-checking/verify-rust-std/actions/workflows/kani.yml) |
1515
| GOTO-Transcoder (ESBMC) | [![GOTO-Transcoder](https://github.com/model-checking/verify-rust-std/actions/workflows/goto-transcoder.yml/badge.svg)](https://github.com/model-checking/verify-rust-std/actions/workflows/goto-transcoder.yml) |
16+
| VeriFast for Rust | [![VeriFast](https://github.com/model-checking/verify-rust-std/actions/workflows/verifast.yml/badge.svg)](https://github.com/model-checking/verify-rust-std/actions/workflows/verifast.yml) |
1617

1718

1819

doc/src/tools/verifast.md

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# VeriFast for Rust
2+
3+
[VeriFast](https://github.com/verifast/verifast) is a tool for verifying the
4+
absence of [undefined
5+
behavior](https://doc.rust-lang.org/reference/behavior-considered-undefined.html)
6+
in single-threaded or multithreaded Rust programs that use `unsafe` blocks, and
7+
for verifying
8+
[soundness](https://doc.rust-lang.org/nomicon/working-with-unsafe.html) of Rust
9+
crates/modules that use `unsafe` blocks. VeriFast performs *modular
10+
verification*: it verifies one function at a time; during the verification of
11+
one function, if that function calls another function, VeriFast uses the
12+
callee's *specification*, not its implementation, to reason about the call.
13+
VeriFast verifies each function against its specification: it verifies that, if
14+
started in a state that satisfies the precondition, the function does not have
15+
undefined behavior and any state in which it returns satisfies the
16+
postcondition.
17+
18+
Besides requiring that the user annotate each function with a precondition and
19+
a postcondition, VeriFast also requires that the user annotate each loop with a
20+
loop invariant. This enables its modular symbolic execution algorithm to
21+
perform only a single symbolic execution of the loop's body to cover all
22+
possible real executions of the loop. Furthermore, the use of function
23+
specifications means a single symbolic execution of a function covers all
24+
possible real executions, even if the function is recursive. In summary, these
25+
annotations enable VeriFast to perform unbounded verification (i.e. of
26+
arbitrarily long, including infinitely long, executions) in finite time.
27+
28+
VeriFast function specifications and loop invariants are expressed in a form of
29+
[*separation logic*](https://en.wikipedia.org/wiki/Separation_logic), and it
30+
performs symbolic execution using a separation logic-based representation of
31+
memory. Separation logic addresses the problem of *aliasing*, which is that in
32+
programs involving pointers or references, different expressions can denote the
33+
same variable. By enabling assertions to express exclusive *ownership* of
34+
memory regions, separation logic enables concise specifications, proper
35+
information hiding, and efficient verification for pointer-manipulating
36+
programs.
37+
38+
## Verifying `unsafe` functions
39+
40+
Consider, for example, the function `Node::reverse_in_place` below that reverses the
41+
given linked list in-place and returns a pointer to the first node (which
42+
was the originally the last node).
43+
44+
```rust
45+
struct Node {
46+
next: *mut Node,
47+
}
48+
49+
/*@
50+
51+
pred Nodes(n: *mut Node; nodes: list<*mut Node>) =
52+
if n == 0 {
53+
nodes == nil
54+
} else {
55+
(*n).next |-> ?next &*& Nodes(next, ?nodes0) &*& nodes == cons(n, nodes0)
56+
};
57+
58+
@*/
59+
60+
impl Node {
61+
62+
unsafe fn reverse_in_place(mut n: *mut Node) -> *mut Node
63+
//@ req Nodes(n, ?nodes);
64+
//@ ens Nodes(result, reverse(nodes));
65+
//@ on_unwind_ens false;
66+
{
67+
let mut m = std::ptr::null_mut();
68+
loop {
69+
//@ inv Nodes(n, ?n_nodes) &*& Nodes(m, ?m_nodes) &*& reverse(nodes) == append(reverse(n_nodes), m_nodes);
70+
//@ open Nodes(n, _);
71+
if n.is_null() {
72+
return m;
73+
}
74+
let k = (*n).next;
75+
//@ append_assoc(reverse(tail(n_nodes)), [n], m_nodes);
76+
(*n).next = m;
77+
m = n;
78+
n = k;
79+
}
80+
}
81+
82+
}
83+
```
84+
85+
VeriFast interprets comments of the form `/*@ ... @*/` or `//@ ...` as VeriFast annotations. This example illustrates four types of annotations:
86+
- Three *specification clause annotations* specify the function's precondition, postcondition, and unwind postcondition, respectively. The function never unwinds, so its
87+
unwind postcondition is `false`.
88+
- The precondition and postcondition are specified in terms of the separation logic predicate `Nodes`, defined in a *ghost declaration annotation*. This predicate
89+
recursively defines the memory footprint of the linked list starting at a given node `n` and associates it with a mathematical list `nodes` of node locations.
90+
The separating conjunction `&*&` implies that the first node of the linked list is *separate* from the remainder of the linked list. It follows that mutating the first node does not affect
91+
the remainder of the linked list. The *variable pattern* `?next` binds logical variable `next` to the value of field `(*n).next`; its scope extends to the end of the assertion.
92+
If a logical variable is introduced in a precondition, its scope includes the postcondition.
93+
- At the start of the loop body, a *block annotation* specifies the loop invariant, expressing that `n` and `m` point to disjoint linked lists and expressing the relationship between their contents and that of the original linked list.
94+
- *Ghost command annotations* provide hints needed for the symbolic execution algorithm to succeed. `open Nodes(n, _)` unfolds the `Nodes` predicate application whose first argument equals `n`. `append_assoc` invokes a library *lemma* expressing the associativity of the `append` operation on mathematical lists.
95+
96+
The generic mathematical datatype `list` is defined in file [`list.rsspec`](https://github.com/verifast/verifast/blob/master/bin/rust/list.rsspec), part of VeriFast's *prelude*, as follows:
97+
```
98+
inductive list<t> = nil | cons(t, list<t>);
99+
```
100+
A list of `t` values is either empty, denoted by *constructor* `nil`, or nonempty, with first element (or *head*) `v` and remainder (or *tail*) `vs`, denoted by `cons(v, vs)`.
101+
102+
Mathematical functions `append` and `reverse` are defined in the same file as follows:
103+
```
104+
fix append<t>(xs: list<t>, ys: list<t>) -> list<t> {
105+
match xs {
106+
nil => ys,
107+
cons(x, xs0) => cons(x, append(xs0, ys))
108+
}
109+
}
110+
111+
fix reverse<t>(xs: list<t>) -> list<t> {
112+
match xs {
113+
nil => nil,
114+
cons(x, xs0) => append(reverse(xs0), cons(x, nil))
115+
}
116+
}
117+
```
118+
Lemma `append_assoc` is declared (but not proven) in the same file. Here is a proof:
119+
```
120+
lem append_assoc<t>(xs: list<t>, ys: list<t>, zs: list<t>)
121+
req true;
122+
ens append(append(xs, ys), zs) == append(xs, append(ys, zs));
123+
{
124+
match xs {
125+
nil => {}
126+
cons(x, xs0) => {
127+
append_assoc(xs0, ys, zs);
128+
}
129+
}
130+
}
131+
```
132+
A lemma is like a regular Rust function, except that it is declared inside an annotation. VeriFast checks that it has no side effects and that it terminates.
133+
134+
## Verifying safe abstractions
135+
136+
Consider the following broken implementation of [`std::mem::replace`](https://doc.rust-lang.org/std/mem/fn.replace.html):
137+
```rust
138+
fn replace<T>(dest: &mut T, src: T) -> T {
139+
unsafe {
140+
let result = (dest as *mut T).read();
141+
(dest as *mut T).write(src);
142+
(dest as *mut T).read() // should be `result`
143+
}
144+
}
145+
```
146+
The Rust compiler accepts it just fine, but VeriFast complains that it cannot prove that when this function returns, the ownership of the value pointed to by `dest` is *separate* from the ownership of the return value. If we replace the final line by `result`, VeriFast accepts the code.
147+
148+
For a function not marked as `unsafe`, VeriFast generates a specification expressing that the function is *semantically well-typed* per [RustBelt](https://research.ralfj.de/thesis.html)'s definition of what Rust's types mean in separation logic. If no specification clause annotations are provided for the function, VeriFast verifies the function against the generated specification; otherwise, VeriFast first verifies that the provided specification implies the generated one, and then verifies the function against the provided specification.
149+
150+
The generated specification for `replace` is as follows:
151+
```rust
152+
fn replace<T>(dest: &mut T, src: T) -> T
153+
//@ req thread_token(?_t) &*& *dest |-> ?dest0 &*& <T>.own(_t, dest0) &*& <T>.own(_t, src) &*& _t == currentThread;
154+
//@ ens thread_token(_t) &*& *dest |-> ?dest1 &*& <T>.own(_t, dest1) &*& <T>.own(_t, result);
155+
```
156+
`<T>.own(t, v)` expresses ownership of the T value `v` accessible to thread `t` (in case T is not [Send](https://doc.rust-lang.org/nomicon/send-and-sync.html)).
157+
`thread_token(t)` represents permission to open *nonatomic invariants* and *nonatomic borrows* at thread `t`; these contain resources shared in a non-thread-safe way at thread `t`.
158+
159+
For more information on how to verify safe abstractions in VeriFast, see the relevant [chapter](https://verifast.github.io/verifast/rust-reference/non-unsafe-funcs.html) in the VeriFast for Rust Reference and the [examples](https://github.com/verifast/verifast/tree/master/tests/rust/safe_abstraction) (in `tests/rust/safe_abstraction` in the VeriFast binary distributions). (See [`testsuite.mysh`](https://github.com/verifast/verifast/blob/master/tests/rust/testsuite.mysh) to learn the command line to use to verify a particular example.)
160+
161+
## Running VeriFast
162+
163+
To run VeriFast, download a binary distribution for your platform, either the
164+
[nightly build](https://github.com/verifast/verifast/releases/tag/nightly) or
165+
the [latest named
166+
release](https://github.com/verifast/verifast/releases/latest). Extract the
167+
archive to any folder on your computer. (On Macs, first [remove the quarantine
168+
bit](https://github.com/verifast/verifast?tab=readme-ov-file#binaries).) Then,
169+
either use the VeriFast IDE at `bin/vfide`, the command-line tool at
170+
`bin/verifast`, or the [VSCode
171+
extension](https://marketplace.visualstudio.com/items?itemName=VeriFast.verifast).
172+
In the IDE, open a file and press F5 to verify it. VeriFast will then either
173+
report "0 errors found" or show a debugger-like GUI that allows you to step
174+
through the failed symbolic execution path and inspect the symbolic state at
175+
each step. If verification succeeds, choose Show execution tree to see the tree
176+
of symbolic execution paths traversed for each function that was verified.
177+
178+
In the IDE, the Verify menu allows you to postpone dealing with certain
179+
complexities of the verification task. Specifically, you can tell VeriFast to
180+
ignore unwind paths, ignore arithmetic overflow, and treat shared reference
181+
creation like raw pointer creation (which ignores the complexities of Rust's
182+
[pointer aliasing
183+
rules](https://marketplace.visualstudio.com/items?itemName=VeriFast.verifast)).
184+
(Many of the other options are only relevant when verifying C programs and have
185+
no effect when verifying Rust programs.) All of these options can also be
186+
specified on the command line.

verifast-proofs/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.stripped.rs
2+
*.computed.diff

verifast-proofs/README.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# VeriFast proofs
2+
3+
This directory contains [VeriFast](../doc/src/tools/verifast.md) proofs for (currently a very, very small) part of the standard library.
4+
5+
VeriFast supports selecting the code to verify on a function-by-function basis. By default, when given a `.rs` file VeriFast will try to verify [semantic well-typedness](https://verifast.github.io/verifast/rust-reference/non-unsafe-funcs.html) of all non-`unsafe` functions in that file (and in any submodules), and will require that the user provide specifications for all `unsafe` functions, which it will then verify against those specifications. However, when given the `-skip_specless_fns` command-line flag, VeriFast will skip all functions for which the user did not provide a specification.
6+
7+
## Applying VeriFast
8+
9+
To verify a function in a file <code>library/<i>crate</i>/src/<i>mod</i>/<i>file</i>.rs</code>, proceed as follows:
10+
1. Copy that file to <code>verifast-proofs/<i>crate</i>/<i>mod</i>/<i>file</i>.rs/original/<i>file</i>.rs</code> as well as to <code>verifast-proofs/<i>crate</i>/<i>mod</i>/<i>file</i>.rs/verified/<i>file</i>.rs</code>.
11+
2. Create a file <code>verifast-proofs/<i>crate</i>/<i>mod</i>/<i>file</i>.rs/original/lib.rs</code> to serve as crate root for verification, and include <code><b>mod</b> <i>file</i>;</code>. (See the existing proofs for examples.) Copy it to <code>verifast-proofs/<i>crate</i>/<i>mod</i>/<i>file</i>.rs/verified/lib.rs</code>.
12+
2. Add a VeriFast specification to the function(s) you want to verify, and any other VeriFast annotations to make the proof go through, in <code>verifast-proofs/<i>crate</i>/<i>mod</i>/<i>file</i>.rs/verified/<i>file</i>.rs</code>. While doing so, you will need to change the code slightly so as to be able to insert ghost commands in the correct places.
13+
3. Add commands for checking your proof to `verifast-proofs/check-verifast-proofs.mysh`. This includes the following:
14+
1. A `verifast` invocation for checking the VeriFast proof.
15+
2. A `refinement-checker` invocation for checking that the code changes you made in the verified version do not change the meaning of the program. Specifically, this tool checks that the original code *refines* the verified code, i.e. that each behavior of a function in the original version is also a behavior of the corresponding function in the verified version. As a result, if the verified version has been verified to have no bad behaviors, the original version also has no bad behaviors.
16+
3. A `diff` invocation for checking that the version in `original` is identical to the original version in `library`.
17+
18+
### Example
19+
20+
Take the VeriFast proof of `linked_list.rs` as an example. The file structure is:
21+
22+
```
23+
linked_list.rs
24+
original
25+
lib.rs
26+
linked_list.rs
27+
linked_list
28+
tests.rs
29+
verified
30+
lib.rs
31+
linked_list.rs
32+
linked_list
33+
tests.rs
34+
```
35+
- The `lib.rs` files are the crate roots we created for verification.
36+
- The `linked_list.rs` files contain the `LinkedList` implementation code. `verified/linked_list.rs` adds VeriFast annotations and includes minor code changes with respect to the implementation in `original/linked_list.rs` when necessary to make it possible for annotations to be inserted in the right places and to make the proof go through.
37+
- Since the original `linked_list.rs` contains a `tests` module, we create empty `tests.rs` files.
38+
39+
We then:
40+
1. Run VeriFast to ensure the proof in `verified/linked_list.rs` goes through.
41+
2. Run the refinement checker to make sure that `verified/linked_list.rs` refines `original/linked_list.rs`. Since we made changes to `verified/linked_list.rs` to make the proof go through, we want to be sure that those changes did not affect the implementation behavior--i.e., we want to be sure that our proof of the verified version of the file actually allows us to make claims about the original version.
42+
3. Run the diff tool to ensure that `original/linked_list.rs` matches the file in `library/`, i.e., check that we're verifying the most recent version of the `LinkedList` implementation available in this repository.
43+
44+
Separating steps 2) and 3) lets us distinguish between CI failures caused by incorrect modifications to the verified files (failure in step 2) versus a proof that was once correct, but is just out of date (failure in step 3). Without the copy of the original file, when the `library/` files change, there would be no easy way to tell whether the VeriFast proofs are wrong or just stale.
45+
46+
The [VeriFast](../.github/workflows/verifast.yml) GitHub action will run `verifast-proofs/check-verifast-proofs.mysh`. Check that file to see which version of VeriFast is used.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// verifast_options{skip_specless_fns}
2+
3+
#![no_std]
4+
#![allow(internal_features)]
5+
#![allow(incomplete_features)]
6+
#![feature(allocator_api)]
7+
#![feature(staged_api)]
8+
#![feature(rustc_attrs)]
9+
#![feature(dropck_eyepatch)]
10+
#![feature(specialization)]
11+
#![feature(extend_one)]
12+
#![feature(exact_size_is_empty)]
13+
#![feature(hasher_prefixfree_extras)]
14+
#![feature(box_into_inner)]
15+
16+
#![stable(feature = "rust1", since = "1.0.0")]
17+
18+
extern crate alloc as std;
19+
20+
#[stable(feature = "rust1", since = "1.0.0")]
21+
pub use std::alloc as alloc;
22+
#[stable(feature = "rust1", since = "1.0.0")]
23+
pub use std::boxed as boxed;
24+
25+
trait SpecExtend<I> {
26+
fn spec_extend(&mut self, iter: I);
27+
}
28+
29+
pub mod linked_list;

0 commit comments

Comments
 (0)