It has been a while since our last blog but here we are again! We recently gave a tutorial about SeaHorn as part of the Automated Formal Methods Workshop (AFM) and the Seventh Summer School on Formal Techniques (SSSFT) that took place on May 20th at Menlo College, Atherton (CA). In this post, we reproduce the most interesting parts of the tutorial.
In the first part of the tutorial, we described the basic usage of SeaHorn as well as how it can be combined with two interesting tools: crab-llvm, an abstract interpreter for LLVM programs, and sally, a model checker for infinite transition systems. In the second part, we shown a case study on how to prove memory safety using SeaHorn.
We started our tutorial with this simple C program test.true.c
:
The program test.true.c
is standard C except there are two special
annotations, both defined in the header file seahorn.h
which is part
of the SeaHorn distribution. The annotation assume(cond)
, where
cond
can be any C expression that evaluates to a Boolean, can be
used to either add extra assumptions about the environment or guide
SeaHorn during its search for inductive invariants. The annotation
sassert(cond)
is equivalent to C assert
. The reason why we use
sassert
rather than assert
is that sassert
is actually defined
as
This definition allows SeaHorn to reduce the validation of an
assertion to the problem of deciding whether the error state denoted
by __VERIFIER_error
is reachable or not.
Note that there is also in the program an external function nd
. In
SeaHorn any external function (denoted with C keyword extern
) that
returns a value of type ty
is modeled as a function that returns
non-deterministically a value of type ty
. This simple mechanism
allows SeaHorn to have code abstractions. Note that the external
function does not need to be named nd
. Any external function that
returns an integer will make the trick here.
Let us continue now by running SeaHorn on the program. The first thing that SeaHorn can do is to translate the C program into LLVM bitcode:
The option fe
(front-end) calls clang
to translate C into bitcode
and performs some preprocessing and optimization of the bitcode. You
can see the content of test.true.bc
by translating it into
human-readable bitcode format:
A better option is to use the SeaHorn utility inspect
as follows:
This command sequence converts first the bitcode to .dot
format and
then to a png
file. If you open the png
file then you should see
something like this:
Next, we can use SeaHorn to translate the LLVM bitcode into verification conditions. We use Horn Clauses as the language to represent verification conditions.
The option horn
outputs the verification conditions in
test.true.smt
. By default, this option uses a format that resembles
SMT-lib:
Once we have generated the verification conditions we can use SeaHorn
back-end solvers to try to solve them by adding the option --solve
:
The default solver used in SeaHorn
is Spacer which is a PDR-based
(PDR refers to Property-Directed Reachability, aka IC3) solver built
on the top of Z3. spacer
infers
inductive invariants over linear integer/real arithmetic and
quantified-free arrays from recursive, non-linear Horn
clauses. spacer
also computes summaries in presence of
functions. These are the invariants inferred by spacer
:
SeaHorn shows first that the query is unsat
meaning that the error
location (denoted by __VERIFIER_error
) cannot be reachable, and
therefore the program is safe!. In addition, SeaHorn outputs for each
basic block the inferred invariants. For instance, for the basic block
_bb
at main
(the loop) the invariant is the formula %j.0.i2 +
%k.0.i1 >= %_0 + 1
. With a bit of faith, you can convince yourself
that %j.0.i2
, %k.0.i1
, and %_0
correspond to the C variables
j
, k
, and n
.
For convenience, SeaHorn allows to write all the above commands in a single command:
Now, lets see a buggy version called test.false.c
:
Let us try to prove that it is safe:
SeaHorn outputs the following:
Here, the solver outputs sat
meaning that the error location is
reachable and therefore the program is unsafe. In addition, it outputs
the relations that were reachable during the unsafe trace found by the
solver. Although this can be helpful to understand why the program is
unsafe, it cannot be considered a valid counterexample.
We already describe in a previous post how to generate automatically executable test harnesses for failed properties. Let us execute these two commands:
The above commands produce an executable out
which can be executed
by simply typing:
You should see a printed message that indicates __VERIFIER_error
was
reached:
This shows that the error location is indeed reachable and therefore the counterexample found by the solve is an actual counterexample. More importantly, a key advantage of having an executable is that we can run now our favorite debugger and inspect why the program failed:
The reason for failure was that n
and k
can be set initially to
3
and 2
. Then, the program executes the loop three times and when
the exit of the loop is taken the value of k
is -1
.