Outline
- Background
- Transition systems
- Model checking
- Deductive Verification
Background
- Testing as dynamic verification
- Trying out the program on different inputs
- Actually running the program
- Static verification
- Analysed program is not run
- Reason on mathematical model of our program (states)
- There are also mixed approaches
- Automatic test generation from code structure
- Observers
What is correct?
- Intuitively, a mathematical proof has been found that given properties hold
- All preconditions are respected
- All postconditions are always true
- Different from testing
- All possible program inputs and scenarios considered
- However: usually assume correctness of compiler, OS, HW, ...
Verification vs Bug Finding
- Often tools/methods focus either on
- Systematically looking for bugs
- Verifying the absence of bugs
- First can usually not guarantee anything when no bugs are found
- Second usually fails when bugs are encountered
- Some tools/methods target both at the same time
Deductive Verification
- Oldest approach to verification (going back to Turing)
- Requires expertise + high effort
- Usually need to annotate programs (invariants, ...)
- Development philosophy ("Formal methods")
- Success stories:
- L4 kernel
- Paris Métro 14 (driverless)
Abstract interpretation
- Techniques based on fixed-point computation
- Good for "weaker" properties
- Absence of arithmetic overflows
- Absence of runtime exceptions
- Automatic, scalable, widely used by compilers
- Success stories
- Astrée could verify primary flight control system of Airbus A340, A380
Model Checking
- Techniques based on (systematic) state-space exploration
- Many different flavours
- Success stories
- Hardware verification
- UPPAAL
Heuristic bug finders
- Usually based on a combination of mentioned methods
- Mainly focussing on implicit specifications
- Sometimes difficult to only report genuine bugs
- Related to fuzzing
Transition systems
- A way of capturing the program states
- Mathemtically a tuple $(S, I, \rightarrow)$
- State space $S$
- Initial states $I \subseteq S$
- Transitions $\rightarrow \subseteq S \times S$
- Program as transition system
- $S = \text{ControlLocations} \times \text{VariableValuations}$
- $I = \{\text{InitialControlState}\} \times \{\text{InitialValues}\}$
- $\rightarrow = ...$
Safety for transition system
- Identify set $\text{Err} \subseteq S$ of error states
- System $(S, I, \rightarrow)$ is safe if there is no path
$s_0 \rightarrow s_1 \rightarrow ... \rightarrow s_n$ with $s_0 \in I$ and $s_n \in \text{Err}$
- Safety of program = unreachability in graph $(S, \rightarrow)$
- If no error state can be reached for any possible input, the program is safe
Example
bool a, b;
while(!a || !b) {
if (a)
b = true;
a = !a;
}
assert(b);
Explicit-state model checking
- Explicitly construct graph $(S, I, \rightarrow)$
- Check reachability of error states
- Example tools: Spin, Java Path Finder
- Problem: state-space explosion
- E.g., program with ten 32-bit integers has $\geq 2^{320}$ states
Possible solutions
- Symbolic Model Checking
- Represent graph $(S, I, \rightarrow)$ symbolically (usually using Binary Decision Diagrams (BDDs))
- Check for reachability of error states using fixed-point computations
- Bounded Model Checking
- Only analyse program up to some depth
- Good at finding bugs, not unreachability
- Today one of the most successful techniques
- Abstraction-based Model Checking
- Analyse a simplified abstraction of the transition system
- Refine if the abstraction is too coarse
- Particularly successful for software verification
Recap contracts
- Contracts define pre- and postconditions
- For function contracts:
- Precondition must be upheld by the caller of the function
- Postcondition must be guaranteed by the function
- Contracts can also be put on individual statements
- If precondition holds before executing statement $s$, then the postcondition must
hold after execution of $s$ has finished
- How can we apply this to C programs?
Background
- At each position (state) in a program we have a set of properties that are true
- With each statement that we execute these properties change
- Mathematical basis: Hoare logic
- An Axiomatic Basis for Computer Programming (Hoare, 1969)
- Describe a computation step as a Hoare triple $\{P\}\; C \:\{Q\}$
- Whenever property $P$ holds, then after executing $C$ (if $C$ terminates), $Q$ will hold
- Example: $\{\Phi\}\; \textbf{nop} \:\{\Phi\}$
Background
- Hoare logic gives us a formalism, but no algorithm
- Weakest precondition calculus
- Guarded commands, non-determinancy and formal derivation of programs (Dijkstra, 1975)
- Given the postcondition $Q$ and a computation $C$, we can mechanically derive a weakest precondition $P'$
- If $P \Rightarrow P'$ then we have proven the contract
- Example: For $\{P\}\; x := a \:\{x = 42\}$ the weakest precondition is $\{a = 42\}$
Example
/*
PRE: a > 0
POST: ret > 0
*/
int f(int a) {
int x = a;
x = 2 * x;
x = x + 2;
return x;
}
Example
/*
PRE: a > 0
POST: ret > 0
*/
int f(int a) {
int x = a;
x = 2 * x;
x = x + 2;
/* { x > 0 } */
return x;
}
Example
/*
PRE: a > 0
POST: ret > 0
*/
int f(int a) {
int x = a;
x = 2 * x;
/* { x > -2 } */
x = x + 2;
/* { x > 0 } */
return x;
}
Example
/*
PRE: a > 0
POST: ret > 0
*/
int f(int a) {
int x = a;
/* { x > -1 } */
x = 2 * x;
/* { x > -2 } */
x = x + 2;
/* { x > 0 } */
return x;
}
Example
/*
PRE: a > 0
POST: ret > 0
*/
int f(int a) {
/* { a > -1 } */
int x = a;
/* { x > -1 } */
x = 2 * x;
/* { x > -2 } */
x = x + 2;
/* { x > 0 } */
return x;
}
Example
/*
PRE: a > 0
POST: ret > 0
*/
int f(int a) {
/* { a > -1 } => is implied by PRE */
int x = a;
/* { x > -1 } */
x = 2 * x;
/* { x > -2 } */
x = x + 2;
/* { x > 0 } */
return x;
}
Inference rules
- Composition of statements
- $\{P\}\; C_1 \;\{R\} \land \{R\} \; C_2 \;\{Q\} \Rightarrow \{P\}\:C_1; C_2\;\{Q\}$
- Assignment
- $\{Q[x \leftarrow E]\}\; x := E \; \{Q\}$
- Conditional execution
- $\{P \land B\}\; C_1\;\{Q\} \land \{P \land \lnot B\}\; C_2 \; \{Q\} \Rightarrow \{P\}\; \text{if}\; B \;\text{then}\; C_1\; \text{else}\; C_2\; \{Q\}$
Example
/* { x >= 5 } */
if (B)
x = x - 2;
else
x = x - 4;
x = x * 2;
/* { x > 0 } */
Example
/* { x >= 5 } */
if (B)
x = x - 2;
else
x = x - 4;
/* { x * 2 > 0 } */
x = x * 2;
/* { x > 0 } */
Example
/* { x >= 5 } */
if (B)
x = x - 2;
else
/* { (x - 4) * 2 > 0 /\ !B } */
x = x - 4;
/* { x * 2 > 0 } */
x = x * 2;
/* { x > 0 } */
Example
/* { x >= 5 } */
if (B)
/* { (x - 2) * 2 > 0 /\ B } */
x = x - 2;
else
/* { (x - 4) * 2 > 0 /\ !B } */
x = x - 4;
/* { x * 2 > 0 } */
x = x * 2;
/* { x > 0 } */
Example
/* { x >= 5 } */
/* { (x - 2) * 2 > 0 /\ (x - 4) * 2 > 0 } */
if (B)
/* { (x - 2) * 2 > 0 /\ B } */
x = x - 2;
else
/* { (x - 4) * 2 > 0 /\ !B } */
x = x - 4;
/* { x * 2 > 0 } */
x = x / 2;
/* { x > 0 } */
Example
/* { x >= 5 } */
/* { x > 4 } */
if (B)
/* { (x - 2) * 2 > 0 /\ B } */
x = x - 2;
else
/* { (x - 4) * 2 > 0 /\ !B } */
x = x - 4;
/* { x * 2 > 0 } */
x = x / 2;
/* { x > 0 } */
Example
/* { x >= 5 } => { x > 4 } */
/* { x > 4 } */
if (B)
/* { (x - 2) * 2 > 0 /\ B } */
x = x - 2;
else
/* { (x - 4) * 2 > 0 /\ !B } */
x = x - 4;
/* { x * 2 > 0 } */
x = x / 2;
/* { x > 0 } */
Loops
- Loops are difficult
- We usually have to provide two things
- Invariants
- Properties that are true in every loop iteration
- Variant
- Something that changes with every loop iteration
- Needed for termination proof
- Loop variant needs to decrease monotonically
- More in Lab 4!
Frama-C
- Framework for modular analysis of C code
- Collection of tools for working with C projects
- Each feature is a separate plugin
- Code browsing (metrics, callgraph, scope & dataflow analysis)
- Code transformation (sparecode removal, slicing, constant folding)
- Specification generation (RTE, Aorai)
- Verification (weakest precondition, abstract interpretation)
- Highly recommended reading after lab 4:
- A Lesson on Verification of IoT Software with Frama-C (Blanchard et al, HPCS 2019)