Why3: trace but effects
Table of Contents
The task
Real world example. I was asked: will the application make some insecure call if some special setting was set in the configuration file? I was sure, it won't. I thought the application followed this policy and that this setting allowed you to prevent the execution of unsafe code. I was wrong. How to check it?
Why3 approach
Why3 has not effects system. But they can be modeled - with a ghost effect log. The idea is simple: effectful (or insecure, or the ones that interest you) functions will "log" when they are called. In some special "ghost log", then we can check the log - if it was changed then they were called. It's called "ghost" bcs it is not planned to exist in the real code (for example after extraction of C, Ocaml, etc from the model). The log can be very simple record with mutable fields: one per a function:
type trace = { mutable f1_calls: int }
Here we trace/log only one function: f1. I made it integer, bcs I wanted to be able to
check the fact of call many times: it any particular function (so, it is not just bool).
The whole code is:
1 module Call1
2 use int.Int
3
4 type trace = { mutable f1_calls: int }
5
6 let ghost trace : trace = { f1_calls = 0 }
7
8 predicate f1_was_called (before after: int) = after = before + 1
9 predicate f1_was_not_called (before after: int) = after = before
10
11 let ghost f1 () : unit
12 writes { trace.f1_calls }
13 ensures { f1_was_called (old trace.f1_calls) trace.f1_calls }
14 =
15 trace.f1_calls <- trace.f1_calls + 1
16
17 let f2 (a1: int) : int
18 ensures { a1 = 0 -> f1_was_not_called (old trace.f1_calls) trace.f1_calls }
19 ensures { a1 <> 0 -> f1_was_called (old trace.f1_calls) trace.f1_calls }
20 =
21 if a1 = 0 then 100
22 else begin
23 f1 ();
24 101
25 end
26
27 let main () : unit
28 requires { trace.f1_calls = 0 }
29 ensures { trace.f1_calls > 0 }
30 =
31 let _ = f2 2 in
32 ()
33
34 end
I added 2 predicates: f1_was_called and f1_was_not_called. They check the ghost and
mutable counter in the ghost trace (log).
The idea of this simple example is to demonstrate that you can check the fact that some
interesting function f1 was (or was not) called. See at main: we check exactly this with
its ensures {..}, we just need to provide some help to Why3, for example, that the
initial value of the counter was 0.
We simulate nested function calls with additional function f2. So, ensures {..} on 29
line tells - "the main has f1 effect". You can check the opposite as well (that it has
not f1 effect):
let main () : unit
requires { trace.f1_calls = 0 }
ensures { trace.f1_calls = 0 }
=
let _ = f2 0 in
()
Why did we need ensures {..} postconditions on f2 for example? IT does not look as a
black box, yes?
Why3 verification is modular.
When proving a caller of f2, Why3 uses the contract of f2, not its implementation!
So if we have:
let main () : unit
requires { trace.f1_calls = 0 }
ensures {
f1_was_called (old trace.f1_calls) trace.f1_calls
}
=
let _ = f2 1 in
()
and f2 has no relevant postcondition, Why3 has no reason to believe that calling f2 1
changes the counter!
Even though it can see the implementation while verifying f2 itself, that
implementation is not automatically propagated into callers.
To reason about f2, Why3 needs its contract, call it "a passport" - what is it? What
does it do? Well... f2 can be... a BLACK BOX! It can be "abstract", to be only a
contract! We introduce such a function not with let ... but with val ...! And it has
not a body.
See:
1 module Call1
2 use int.Int
3
4 type trace = { mutable f1_calls: int }
5
6 let ghost trace : trace = { f1_calls = 0 }
7
8 predicate f1_was_called (before after: int) = after = before + 1
9 predicate f1_was_not_called (before after: int) = after = before
10
11 let ghost f1 () : unit
12 writes { trace.f1_calls }
13 ensures { f1_was_called (old trace.f1_calls) trace.f1_calls }
14 =
15 trace.f1_calls <- trace.f1_calls + 1
16
17 val f2 (a1: int) : int
18 writes { trace.f1_calls }
19 ensures { result = if a1 = 0 then 100 else 101 }
20 ensures { a1 = 0 -> f1_was_not_called (old trace.f1_calls) trace.f1_calls }
21 ensures { a1 <> 0 -> f1_was_called (old trace.f1_calls) trace.f1_calls }
22
23 let main () : unit
24 requires { trace.f1_calls = 0 }
25 ensures { trace.f1_calls = 0 }
26 =
27 let _ = f2 0 in
28 ()
29
30 end
Look at f2 now: it specifies when f1 effect happens. Also it specifies (and it's
important to avoid a warning that trace actually is not changed) that f2 MAY CHANGE
trace.f1_calls.
OK. Now lets return to the original task: main embodies such a function that should be
verified about an effect, but we can do it with f2 too:
1 let f2 (a1: int) : int
2 writes { trace.f1_calls }
3 ensures { result = if a1 = 0 then 100 else 101 }
4 ensures { a1 = 0 -> f1_was_not_called (old trace.f1_calls) trace.f1_calls }
5 ensures { a1 <> 0 -> f1_was_called (old trace.f1_calls) trace.f1_calls }
6 = if a1 = 0 then begin
7 f1 (); (* DENIES THE CONTRACT ! *)
8 100
9 end
10 else begin
11 f1 ();
12 101
13 end
And this demonstrate the original task ideally! Look - we are sure that:
- if the setting (
a1) is 0 thenf1effect will not happen - else
f1effect will happen
this is our expectation. This is what we think about the function f2.
The result of our proving :
$ why3 prove Call1.mlw -P z3 File Call1.mlw: Goal f1'vc. Prover result is: Valid (0.01s, 38 steps). File Call1.mlw: Goal f2'vc. Prover result is: Unknown (sat) (0.00s, 258 steps). File Call1.mlw: Goal main'vc. Prover result is: Valid (0.00s, 40 steps).
and it happens bcs the body modeled the behavior of some real existing function denies its
contract - our expectation about the behavior of f2: when the setting a1 is 0 which
means "don't call the insecure function f1", the body actually calls it.
The whole model
We modeled our task as:
a1is our setting from the configuration filef1is our insecure functiontraceis ghost and it is used only for verification- verified functions can be represented abstractly
Semantic of predicates
If we mean "one or more calls" (to check that f1 effect happened somewhere in the depth
of nested calls), then the predicate could be:
predicate f1_was_called (before after: int) = after > before
Similar technique is used in many other Proof Assistants.