Lecture 9

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
    • 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

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

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

Thanks for today!