Lecture 8

Paul Fiterau

Based on slides by Nikolaus Huber

Verification

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
    • E.g., symbolic execution generates a test case for each program path

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

Techniques

Deductive Verification

  • Oldest approach to verification (going back to Turing)
  • Requires expertise + high effort
    • Usually need to annotate programs (invariants, ...)
    • Common development philosophy (e.g., used in B-method)
  • Success stories:
    • seL4 L4 microkernel (~8000 LoC)
    • The cost of the proof is higher, in total about 20 py.
      seL4: Formal Verification of an OS Kernel

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 (e.g., for optimizations)
  • 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

Example - Clang

		
			void f(void) {
			   int * x = malloc(10 * sizeof(int));
			}            
			int main(void) {
			   f();
			   return 0;
			}
	
	
		
			clang --analyze main.c
		
	
		
		main.c:5:11: warning: Value stored to 'x' during its initialization is never read [deadcode.DeadStores]
    5 |      int* x = malloc(10 * sizeof(int));
      |           ^   ~~~~~~~~~~~~~~~~~~~~~~~~
		main.c:6:3: warning: Potential leak of memory pointed to by 'x' [unix.Malloc]
    6 |   }
	
	
	

Transition systems

  • A way of capturing the program states
  • Mathematically 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., prog. with ten 32-bit integers has $\geq 2^{320}$ states
  • Several solutions exist
    • E.g., based on state space abstraction, or bounding the number of steps in state exploration
    • Check videos of prior course offering (link on Studium)

Deductive 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 - Hoare Logic

  • 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 - Weakest Precondition Calculus

  • 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 > 0 } */
			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; 
			/* { 2 * x > -2 } */
			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; 
		}
	

WP calculus - revisit

  • For a computation $C$ and a postcondition $Q$, WP calculus provides us with $wp$ s.t.:
    $\{wp(C, Q)\}\; C\; \{Q\}$ is a valid Hoare triple
  • $wp$ is computed mostly automatically
  • This allows us to verify our function $fun$ by:
    1. Computing $wp(fun, Post)$
    2. Checking that $Pre \rightarrow wp(fun, Post)$

WP for different computations

  • Assignment
  • $wp(x := E, Q) = Q[x \leftarrow E]$
  • Sequence of statements
  • $wp(S_1;S_2, Q) = wp(S_1, wp(S_2, Q))$
  • Conditional execution
  • $wp(if\; B\; then\; S_1\; else\; S_2, Q) = \\ (B \rightarrow wp(S_1, Q)) \wedge (\neg B \rightarrow wp(S_2, Q)) = \\(B \wedge wp(S_1, Q)) \vee (\neg B \wedge wp(S_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 /\ B) /\ ((x - 4) * 2 > 0 /\ !B)} */ 
		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 /\ B) /\ (x > 4 /\ !B) } */ 
		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 /\ B) /\ (x > 4 /\ !B) } */
		/* { (x > 2 /\ B) /\ (x > 4 /\ !B) } */ 
		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)

Thanks for today!