Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VeriFast CI #239

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/verifast.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: VeriFast
feliperodri marked this conversation as resolved.
Show resolved Hide resolved

on:
workflow_dispatch:
merge_group:
pull_request:
branches: [ main ]
push:
paths:
- 'library/**'
- '.github/workflows/verifast.yml'
- 'verifast-proofs/**'

defaults:
run:
shell: bash

jobs:
check-verifast-on-std:
name: Verify std library
runs-on: ubuntu-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Install VeriFast
run: |
cd ~
curl -OL https://github.com/verifast/verifast/releases/download/24.12/verifast-24.12-linux.tar.gz
# https://github.com/verifast/verifast/attestations/3689894
echo '51bebf990f31666abcd3675000e7714ef79b417390e930953ef25383e8d59421 verifast-24.12-linux.tar.gz' | shasum -a 256 -c
tar xf verifast-24.12-linux.tar.gz

- name: Install the Rust toolchain used by VeriFast
run: rustup toolchain install nightly-2024-11-23

- name: Run VeriFast Verification
run: |
export PATH=~/verifast-24.12/bin:$PATH
cd verifast-proofs
mysh check-verifast-proofs.mysh
1 change: 1 addition & 0 deletions doc/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- [Verification Tools](./tools.md)
- [Kani](./tools/kani.md)
- [VeriFast](./tools/verifast.md)

---

Expand Down
1 change: 1 addition & 0 deletions doc/src/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ please see the [Tool Application](general-rules.md#tool-applications) section.
|---------------------|-------|
| 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) |
| 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) |
| 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) |



Expand Down
186 changes: 186 additions & 0 deletions doc/src/tools/verifast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# VeriFast for Rust

[VeriFast](https://github.com/verifast/verifast) is a tool for verifying the
absence of [undefined
behavior](https://doc.rust-lang.org/reference/behavior-considered-undefined.html)
in single-threaded or multithreaded Rust programs that use `unsafe` blocks, and
for verifying
[soundness](https://doc.rust-lang.org/nomicon/working-with-unsafe.html) of Rust
crates/modules that use `unsafe` blocks. VeriFast performs *modular
verification*: it verifies one function at a time; during the verification of
one function, if that function calls another function, VeriFast uses the
callee's *specification*, not its implementation, to reason about the call.
VeriFast verifies each function against its specification: it verifies that, if
started in a state that satisfies the precondition, the function does not have
undefined behavior and any state in which it returns satisfies the
postcondition.

Besides requiring that the user annotate each function with a precondition and
a postcondition, VeriFast also requires that the user annotate each loop with a
loop invariant. This enables its modular symbolic execution algorithm to
perform only a single symbolic execution of the loop's body to cover all
possible real executions of the loop. Furthermore, the use of function
specifications means a single symbolic execution of a function covers all
possible real executions, even if the function is recursive. In summary, these
annotations enable VeriFast to perform unbounded verification (i.e. of
arbitrarily long, including infinitely long, executions) in finite time.

VeriFast function specifications and loop invariants are expressed in a form of
[*separation logic*](https://en.wikipedia.org/wiki/Separation_logic), and it
performs symbolic execution using a separation logic-based representation of
memory. Separation logic addresses the problem of *aliasing*, which is that in
programs involving pointers or references, different expressions can denote the
same variable. By enabling assertions to express exclusive *ownership* of
memory regions, separation logic enables concise specifications, proper
information hiding, and efficient verification for pointer-manipulating
programs.

## Verifying `unsafe` functions

Consider, for example, the function `Node::reverse` below that reverses the
given linked list in-place and returns a pointer to the first node (which
was the originally the last node).

```rust
struct Node {
next: *mut Node,
}

/*@

pred Nodes(n: *mut Node; nodes: list<*mut Node>) =
if n == 0 {
nodes == nil
} else {
(*n).next |-> ?next &*& Nodes(next, ?nodes0) &*& nodes == cons(n, nodes0)
};

@*/

impl Node {

unsafe fn reverse(mut n: *mut Node) -> *mut Node
//@ req Nodes(n, ?nodes);
//@ ens Nodes(result, reverse(nodes));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to use a different name for the mathematical list reverse function and the rust reverse method ? This could be confusing for some novice users (myself included). By the way does ghost code live in a separate namespace ? I'm guessing not since you can refer to Rust function parameters in the spec. How does overloading work between Rust code and verifast ghost code for function symbols ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real functions and lemmas share the same namespace. No two functions can have the same name. But in this example the real function is called Node::reverse and the lemma is called reverse, so there's no clash.

//@ on_unwind_ens false;
{
let mut m = std::ptr::null_mut();
loop {
//@ inv Nodes(n, ?n_nodes) &*& Nodes(m, ?m_nodes) &*& reverse(nodes) == append(reverse(n_nodes), m_nodes);
//@ open Nodes(n, _);
if n.is_null() {
return m;
}
let k = (*n).next;
//@ append_assoc(reverse(tail(n_nodes)), [n], m_nodes);
(*n).next = m;
m = n;
n = k;
}
}

}
```

VeriFast interprets comments of the form `/*@ ... @*/` or `//@ ...` as VeriFast annotations. This example illustrates four types of annotations:
- Three *specification clause annotations* specify the function's precondition, postcondition, and unwind postcondition, respectively. The function never unwinds, so its
unwind postcondition is `false`.
- The precondition and postcondition are specified in terms of the separation logic predicate `Nodes`, defined in a *ghost declaration annotation*. This predicate
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.
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
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.
If a logical variable is introduced in a precondition, its scope includes the postcondition.
- 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.
- *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.

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:
```
inductive list<t> = nil | cons(t, list<t>);
```
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)`.

Mathematical functions `append` and `reverse` are defined in the same file as follows:
```
fix append<t>(xs: list<t>, ys: list<t>) -> list<t> {
match xs {
nil => ys,
cons(x, xs0) => cons(x, append(xs0, ys))
}
}

fix reverse<t>(xs: list<t>) -> list<t> {
match xs {
nil => nil,
cons(x, xs0) => append(reverse(xs0), cons(x, nil))
}
}
```
Lemma `append_assoc` is declared (but not proven) in the same file. Here is a proof:
```
lem append_assoc<t>(xs: list<t>, ys: list<t>, zs: list<t>)
req true;
ens append(append(xs, ys), zs) == append(xs, append(ys, zs));
{
match xs {
nil => {}
cons(x, xs0) => {
append_assoc(xs0, ys, zs);
}
}
}
```
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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we say that the general methodology/mental model for proving some Rust code implementing some data structure would be to: first, formalize the data structure layout, its invariants and functions in in Verifast's own separation logic language, and then prove that the Rust code and the abstract specification compute the same thing by adding annotations explaining how both compute in lockstep (you are essentially weaving the spec and the program's executions together) ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not quite how I think of it. The specification (the req clause and the ens clause) is not computational/imperative; it's declarative. The lemmas are imperative but they are proofs of their specification; I think of them as mathematical lemmas stating that the precondition implies the postcondition.

## Verifying safe abstractions

Consider the following broken implementation of [`std::mem::replace`](https://doc.rust-lang.org/std/mem/fn.replace.html):
```rust
fn replace<T>(dest: &mut T, src: T) -> T {
unsafe {
let result = (dest as *mut T).read();
(dest as *mut T).write(src);
(dest as *mut T).read() // should be `result`
}
}
```
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.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precision the "generated specification" is the default specification derived automatically by Verifast, according to RustBelts separation logic model of rust types, from the function signature.


The generated specification for `replace` is as follows:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this the default specification ? Or automatically generated ?

```rust
fn replace<T>(dest: &mut T, src: T) -> T
//@ req thread_token(?_t) &*& *dest |-> ?dest0 &*& <T>.own(_t, dest0) &*& <T>.own(_t, src) &*& _t == currentThread;
//@ ens thread_token(_t) &*& *dest |-> ?dest1 &*& <T>.own(_t, dest1) &*& <T>.own(_t, result);
```
`<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)).
`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`.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the concept of opening predicates has been explained so far in this doc, a link to a definition would be useful.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence is not about opening predicates, it's about opening invariants and borrows; I wanted to briefly motivate thread_token but I'm not sure it makes sense to try and explain those concepts here.


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

## Running VeriFast

To run VeriFast, download a binary distribution for your platform, either the
[nightly build](https://github.com/verifast/verifast/releases/tag/nightly) or
the [latest named
release](https://github.com/verifast/verifast/releases/latest). Extract the
archive to any folder on your computer. (On Macs, first [remove the quarantine
bit](https://github.com/verifast/verifast?tab=readme-ov-file#binaries).) Then,
either use the VeriFast IDE at `bin/vfide`, the command-line tool at
`bin/verifast`, or the [VSCode
extension](https://marketplace.visualstudio.com/items?itemName=VeriFast.verifast).
In the IDE, open a file and press F5 to verify it. VeriFast will then either
report "0 errors found" or show a debugger-like GUI that allows you to step
through the failed symbolic execution path and inspect the symbolic state at
each step. If verification succeeds, choose Show execution tree to see the tree
of symbolic execution paths traversed for each function that was verified.

In the IDE, the Verify menu allows you to postpone dealing with certain
complexities of the verification task. Specifically, you can tell VeriFast to
ignore unwind paths, ignore arithmetic overflow, and treat shared reference
creation like raw pointer creation (which ignores the complexities of Rust's
[pointer aliasing
rules](https://marketplace.visualstudio.com/items?itemName=VeriFast.verifast)).
(Many of the other options are only relevant when verifying C programs and have
no effect when verifying Rust programs.) All of these options can also be
specified on the command line.
2 changes: 2 additions & 0 deletions verifast-proofs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.stripped.rs
*.computed.diff

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a README file to this folder that describes the intended puprpose and organisation guidelines ?
From side discussions it seems that the folder will contain copies of standard library files extended with verifast ghost code, and that check-verifast-proofs.mysh will run all the diff logic and verifast analyses, but this needs to be explained here

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would really be awsome to have an actual example of a working proof as part of this PR too, so that we can check the action is working.

Empty file.
Loading