Lazy Abstraction with Interpolants

Similar documents
Ufo: A Framework for Abstraction- and Interpolation-Based Software Verification

Interpolation-based Software Verification with Wolverine

Software Model Checking. Xiangyu Zhang

Having a BLAST with SLAM

F-Soft: Software Verification Platform

Having a BLAST with SLAM

Having a BLAST with SLAM

CS 510/13. Predicate Abstraction

Software Model Checking with Abstraction Refinement

Having a BLAST with SLAM

Using Counterexample Analysis to Minimize the Number of Predicates for Predicate Abstraction

Tree Interpolation in Vampire

Scalable Program Verification by Lazy Abstraction

Regression Verification - a practical way to verify programs

Abstraction Refinement for Quantified Array Assertions

Predicate Refinement Heuristics in Program Verification with CEGAR

Static Program Analysis

Proof Pearl: The Termination Analysis of Terminator

Predicate Abstraction of ANSI C Programs using SAT Λ

TRACER: A Symbolic Execution Tool for Verification

Counterexample Guided Abstraction Refinement in Blast

Unbounded Symbolic Execution for Program Verification

DIPARTIMENTO DI INGEGNERIA E SCIENZA DELL INFORMAZIONE Povo Trento (Italy), Via Sommarive 14

Predicate Abstraction with Adjustable-Block Encoding

An Eclipse Plug-in for Model Checking

Software Model Checking. From Programs to Kripke Structures

Sliced Path Prefixes: An Effective Method to Enable Refinement Selection

Algorithms for Software Model Checking: Predicate Abstraction vs. IMPACT

Oracle-Guided Heap Interpolant Synthesis

Model Checking: Back and Forth Between Hardware and Software

Software Model Checking via Large-Block Encoding

Verifying Multithreaded Software with Impact

Model Checking with Abstract State Matching

Inductive Invariant Generation via Abductive Inference

Lectures 20, 21: Axiomatic Semantics

Subsumer-First: Steering Symbolic Reachability Analysis

The software model checker BLAST

6. Hoare Logic and Weakest Preconditions

Abstract Interpretation

Learning Summaries of Recursive Functions

Abstractions from Proofs

An Annotated Language

Approximating Predicate Images for Bit-Vector Logic

Lecture 6: Arithmetic and Threshold Circuits

Counterexamples with Loops for Predicate Abstraction

More on Verification and Model Checking

1. true / false By a compiler we mean a program that translates to code that will run natively on some machine.

On Reasoning About Finite Sets in Software Model Checking

COMPUTABILITY THEORY AND RECURSIVELY ENUMERABLE SETS

Over-Approximating Boolean Programs with Unbounded Thread Creation

Binary Decision Diagrams

Trees. 3. (Minimally Connected) G is connected and deleting any of its edges gives rise to a disconnected graph.

SYNERGY : A New Algorithm for Property Checking

Logic-Flow Analysis of Higher-Order Programs

The Encoding Complexity of Network Coding

No model may be available. Software Abstractions. Recap on Model Checking. Model Checking for SW Verif. More on the big picture. Abst -> MC -> Refine

Handling Loops in Bounded Model Checking of C Programs via k-induction

Lecture 1 Contracts : Principles of Imperative Computation (Fall 2018) Frank Pfenning

Program Analysis and Constraint Programming

Cover Page. The handle holds various files of this Leiden University dissertation

Automatic Abstraction without Counterexamples

Lecture 2: Symbolic Model Checking With SAT

Foundations of AI. 9. Predicate Logic. Syntax and Semantics, Normal Forms, Herbrand Expansion, Resolution

Small Formulas for Large Programs: On-line Constraint Simplification In Scalable Static Analysis

Definition: A context-free grammar (CFG) is a 4- tuple. variables = nonterminals, terminals, rules = productions,,

Unifying and extending hybrid tractable classes of CSPs

LOGIC AND DISCRETE MATHEMATICS

Duet: Static Analysis for Unbounded Parallelism

Counter-Example Guided Program Verification

Efficient Memoization for Dynamic Programming with Ad-Hoc Constraints

Double Header. Two Lectures. Flying Boxes. Some Key Players: Model Checking Software Model Checking SLAM and BLAST

Automated Debugging with Error Invariants

Proofs from Tests. Nels E. Beckman, Aditya V. Nori, Sriram K. Rajamani, Robert J. Simmons, SaiDeep Tetali, Aditya V. Thakur

Lecture 1 Contracts. 1 A Mysterious Program : Principles of Imperative Computation (Spring 2018) Frank Pfenning

BDD-Based Software Model Checking with CPAchecker

A MODEL CHECKER FOR JAVA BYTECODE, WITH NOVEL APPLICATIONS. A Dissertation Presented to The Academic Faculty. Burak Sahin

Complete Instantiation of Quantified Formulas in Satisfiability Modulo Theories. ACSys Seminar

Towards a Logical Reconstruction of Relational Database Theory

Verifying Concurrent Programs

Edmund Clarke Computer Science Dept., Carnegie Mellon University Pittsburgh, PA, USA

On Reasoning about Finite Sets in Software Checking

From OCL to Propositional and First-order Logic: Part I

An algorithm for Performance Analysis of Single-Source Acyclic graphs

Formal Systems II: Applications

Handout 9: Imperative Programs and State

Proc. ISoLA 2018, c Springer

Model Checking I Binary Decision Diagrams

Timed Automata: Semantics, Algorithms and Tools

Tracing the executions of concurrent programs

Indexed Predicate Discovery for Unbounded System Verification

Ivy: Safety Verification by Interactive Generalization

Equivalence Checking of C Programs by Locally Performing Symbolic Simulation on Dependence Graphs

Computer Science Technical Report

Introduction to Axiomatic Semantics

Hardware versus software

A proof-producing CSP solver: A proof supplement

KRATOS A Software Model Checker for SystemC

Abstract Counterexample-based Refinement for Powerset Domains

DPLL(Γ+T): a new style of reasoning for program checking

Principles of AI Planning. Principles of AI Planning. 7.1 How to obtain a heuristic. 7.2 Relaxed planning tasks. 7.1 How to obtain a heuristic

Transcription:

Lazy Abstraction with Interpolants Kenneth L. McMillan Cadence Berkeley Labs Abstract. We describe a model checker for infinite-state sequential programs, based on Craig interpolation and the lazy abstraction paradigm. On device driver benchmarks, we observe a speedup of up to two orders of magnitude relative to a similar tool using predicate abstraction. 1 Introduction Craig interpolants derived from proofs have been shown to provide an efficient method of image approximation in finite-state symbolic model checking [10]. In this paper, we extend the interpolation-based model checking approach from finite- to infinite-state systems, in particular to the verification of sequential programs. The approach applies an interpolating prover [11] in the lazy abstraction paradigm [7]. Instead of iteratively refining an abstraction, lazy abstraction refines the abstract model on demand, as it is constructed. Up to now, this refinement has been based on predicate abstraction [12]. Here, we refine the abstraction using interpolants derived from refuting program paths. This avoids the high cost of computing the predicate image (or abstract post ) operator, yielding a substantial performance improvement. To illustrate the algorithm, we will use the simple C fragment of Figure 1 (borrowed from [7]). We model the functions lock and unlock by setting and resetting a variable L representing the state of the lock. We would like to prove that L is always zero on entry to lock. A control-flow graph for the function is shown in the figure. We have initialized L to zero and added a transition to an error state when lock is called and L is non-zero. Our algorithm unwinds the control-flow graph of the program into a tree. Each vertex in the tree corresponds to a program control location, and is labeled with a fact about the program variables that is true at that point in the execution of the program. Each vertex is initially labeled True. When we reach a vertex corresponding to the error location, we strengthen the facts along the path to that vertex, so as to prove the error vertex unreachable. For example, suppose we first expand the path that branches to the error location on entering the loop (Figure 2a). We wish to label the error vertex False, thus proving it unreachable. This is done by generating an interpolant for the path to the error state. An interpolant for a path is a sequence of formulas assigned to the vertices, such that each formula implies the next after executing the intervening program operation, and such that the initial vertex is labeled True and the final vertex False. Existence of an interpolant implies T. Ball and R.B. Jones (Eds.): CAV 2006, LNCS 4144, pp. 123 136, 2006. c Springer-Verlag Berlin Heidelberg 2006

124 K.L. McMillan do{ lock(); old = new; if(*){ unlock; new++; } } while (new!= old); ERR [L!=0] L=0; new++ L=0 L=1; old=new [new==old] [new!=old] (a) program fragment (b) control-flow graph Fig. 1. A simple example program T 0 L=0 T [L!=0] T 2 1 ERR (a) first error 4 [new!=old] T T 6 [L!=0] 5 T ERR (b) second error 0 T L=0 F [L!=0] L=0 2 1 ERR L=1; old=new 3 L=0; T new++ 0 T L=0 F [L!=0] L=0 2 1 ERR L=1; old=new 3 old=new L=0; new++ old=new L=0 4 8 [new!=old] [new==old] [new!=old] F [L!=0] [L!=0] F 6 5 L=0 7 T F 9 10 ERR ERR (c) termination Fig. 2. Stages of the unwinding (vertex labels in italics) that the final (error) vertex is unreachable. An interpolant can be derived from a refutation of the path generated by a theorem prover [11,6]. In Figure 2a, an interpolant would be: True,L = 0,False. In Figure 2b, we have strengthened the labeling on the error path with this interpolant (ruling out the error) and backtracked to explore the non-error branch. We pass through the loop, calling lock and unlock, then return to the top, taking the error branch again. In this case, our interpolant labels vertices 4 and 5 with L = 0 (again labeling the error vertex False). Notice that vertices 5 and 1 correspond to the same location (the top of the loop) and that the label of vertex 5 implies the label of vertex 1. We say that vertex 1 covers vertex 5, and we cease expanding descendants of the covered vertex. However, if vertex 1 were to be strengthened in the future, it might cease to cover vertex 5, and we would have to continue expanding it. Figure 2c shows the remainder of the unwinding, indicating coverings with dotted lines. We backtrack, expanding the path that falls out of the loop, and then the path that skips the call to unlock. In the latter case, we again reach an error state, strengthening the path. This labels vertex 9 with False, thus it is also covered by vertex 1. At this point all unexpanded states are covered, so the procedure terminates. At termination, the disjunction of the labels for a given location is an invariant for that location. Notice also that the labels use the atomic predicates L =0andold = new, but are not the strongest facts expressible using those predicates (as we would obtain with predicate abstraction).

Lazy Abstraction with Interpolants 125 Rather, they are just strong enough to allow us to label the error vertices False. Notice that we could also strengthen a path by computing strongest postconditions or weakest preconditions along the path (these are, in fact, the strongest and weakest interpolants respectively). However, by deriving interpolants from proofs, we exploit the prover s ability to focus on relevant facts, and thus avoid deducing irrelevant information that could complicate the analysis, or even lead to divergence. Related Work. The most closely related technique is predicate abstraction [12]. This is implemented using the lazy paradigm in the Blast model checker [7], and in a number of software model checkers [2,4,3] using a counterexamplebased refinement loop. The advantage of the present method over predicate abstraction is that it avoids computing the abstract post operator. That is, in predicate abstraction, computing the set of successors of a set of abstract states requires an exponential number of calls to a decision procedure in the worst case. Because of this, weak approximations are typically used, such as the Cartesian or Boolean Programs approximations [1], with the associated need for refinement in case of failures. Even with approximations, computing the abstract post operator (or abstract transition relation) is still the dominant cost. By contrast, the present method requires just one call to a decision procedure for each error vertex reached, and one for each covering test. The method is also closely related to the interpolation-based model checking method of [10]. That work only treated finite-state systems. In principle the method could be generalized to infinite-state programs, however it would require applying a decision procedure to an unfolding of the entire program up to some depth k. This would almost certainly be impractical. Using the lazy abstraction method, we only apply the decision procedure to individual program paths leading to error locations, greatly reducing the burden on the prover. Outline of the Paper. In section 2, we will formalize the lazy interpolationbased model checking procedure, proving some results about soundness and termination. Then in section 3, we describe an implementation of the procedure in a software model checking tool called Impact, and compare the performance of this tool to the lazy predicate abstraction approach implemented in Blast. Experiments using a small set of device driver benchmarks show a performance improvement of one to two orders of magnitude using the new method. Finally in section 4, we conclude and consider some future directions for research. 2 Lazy Interpolant-Based Model Checking Throughout this paper, we will use standard first-order logic (FOL) and the notation L(Σ) to denote the set of well-formed formulas (wff s) of FOL over a vocabulary Σ of non-logical symbols. For a given formula or set of formulas φ, we will use L(φ) todenotethewff s over the vocabulary of φ. For every non-logical symbol s, we presume the existence of a unique symbol s (that is, s with one prime added). We think of s with n primes added

126 K.L. McMillan as representing the value of s at n time units in the future. For any formula or term φ, we will use the notation φ n to denote the addition of n primes to every symbol in φ (meaning φ at n time units in the future). For any set Σ of symbols, let Σ denote {s s Σ} and Σ n denote {s n s Σ}. Modeling Programs. We use FOL formulas to characterize programs. To this end, let S, the state vocabulary, be a set of individual variables and uninterpreted n-ary functional and propositional constants. A state formula is a formula in L(S) (which may also include various interpreted symbols, such as = and +). A transition formula is a formula in L(S S ). For our purposes, a program is a tuple (Λ, Δ, l i,l f ), where Λ is a finite set of program locations, Δ is a set of actions, l i Λ is the initial location and l f Λ is the error location. An action is a triple (l, T, m), where l, m Λ are respectively the entry and exit locations of the action, and T is a transition formula. A path π of a program is a sequence of transitions of the form (l 0,T 0,l 1 ) (l 1,T 1,l 2 ) (l n 1,T n 1,l n ). The path is an error path when l 0 = l i and l n = l f. The unfolding U(π) ofpathπ is the sequence of formulas T 0 0,...,T n n 1,that is, the sequence of transition formulas T 0...T n 1,witheachT i shifted i time units into the future. We will say that path π is feasible when U(π) is consistent. We can think of amodelof U(π) as a concrete program execution, assigning a value to every program variable at every time 0...n. A program is said to be safe when every error path of the program is infeasible. An inductive invariant of a program is amapi : Λ L(S), such that I(l i ) True and for every action (l, T, m) Δ, I(l) T implies I(m).Asafety invariant of a program is an inductive invariant such that I(l f ) False. Existence of a safety invariant of a program implies that the program is safe. To simplify presentation of the algorithms, we will assume that every location has at least one outgoing action. This can be made true without affecting program safety by adding self-loops. Interpolants from Proofs. Given a pair of formulas (A, B), such that A B is inconsistent, an interpolant for (A, B) isaformulaâ with the following properties: A implies Â,  B is unsatisfiable, and  L(A) L(B). The Craig interpolation lemma [5] states that an interpolant always exists for inconsistent formulas in FOL. To handle program paths, we generalize this idea to sequences of formulas. That is, given a sequence of formulas Γ = A 1,...,A n, we say that Â0,... A ˆ n is an interpolant for Γ when Â0 = True and A ˆ n = False and, for all 1 i n,  i 1 A i implies Âi and for all 1 i<n,  i (L(A 1...A i ) L(A i+1...a n )).

Lazy Abstraction with Interpolants 127 That is, the i-th element of the interpolant is a formula over the common vocabulary the prefix A 0...A i and the suffix A i+1...a n, and each interpolant implies the next, with A i.ifγ is quantifier-free, we can derive a quantifier-free interpolant for Γ from a refutation of Γ, in certain interpreted theories [11]. Program Unwindings. We now give a definition of a program unwinding, and an algorithm to construct a complete unwinding using interpolants. For two vertices v and w of a tree, we will write w v when w isaproperancestorofv. Definition 1. An unwinding of a program A = (Λ, Δ, l i,l f ) is a quadruple (V,E,M v,m e ),where(v,e) is a directed tree rooted at ɛ, M v : V Λ is the vertex map, and M e : E Δ is the edge map, such that: M v (ɛ) =l i for every non-leaf vertex v V, for every action (M v (v),t,m) Δ, there exists an edge (v, w) E such that M v (w) =m and M e (v, w) =T. Definition 2. A labeled unwinding of a program A =(Λ, Δ, l i,l f ) is a triple (U, ψ, ), where U=(V,E,M v,m e ) is an unwinding of A ψ : V L(S) is called the vertex labeling, and V V is called the covering relation. A vertex v V is said to be covered iff there exists (w, x) such that w v. The unwinding is said to be safe iff, for all v V, M v (v) =l f implies ψ(v) False.It is complete iff every leaf v V is covered. Definition 3. A labeled unwinding (U, ψ, ) of a program A =(Λ, Δ, l i,l f ), where U = (V,E,M v,m e ),issaidtobewell-labeled iff: ψ(ɛ) True, and for every edge (v, w) E, ψ(v) M e (v, w) implies ψ(w),and for all (v, w), ψ(v) ψ(w), andw is not covered. Notice that, if a vertex is covered, all its descendants are also covered. Moreover, we do not allow a covered vertex to cover another vertex. To see why, consider the unwinding of Figure 3. Here, vertex y covers x, but is itself covered, since its ancestor v is covered by w. This might seem acceptable, since any states reachable from y should be reachable from w through its descendant z. However, this is not the case. Because the vertex labels are approximate, it may be that ψ(y) ψ(z). Thus, z may not reach all states reachable from x. Theorem 1. If there exists a safe, complete, well-labeled unwinding of program A, thena is safe. Proof. Let U be the set of uncovered vertices, and let function M map location l to {ψ(v) M v (v) =l, v U}. M is a safety invariant for A. We now describe a semi-algorithm for building a complete, safe, well-labeled unwinding of a program. The algorithm terminates if the program is unsafe,

128 K.L. McMillan p p w v p T T z y x Fig. 3. Example showing why covered vertices must not cover others global variables: V aset,e V V, V V and ψ : V wff procedure Expand(v V ): if v is an uncovered leaf then for all actions (M v(v),t,m) Δ add a new vertex w to V and a new edge (v, w) toe; set M v(w) m and ψ(w) True; set M e(v, w) T procedure Refine(v V ): if M v(v) =l f and ψ(v) False then let π =(v 0,T 0,v 1) (v n 1,T n 1,v n) be the unique path from ɛ to v if U(π) has an interpolant Â0,...,Ân then for i =0...n: let φ = Â i i if ψ(v i) = φ then remove all pairs (,v i)from set ψ(v i) ψ(v i) φ else abort (program is unsafe) procedure Cover(v, w V ): if v is uncovered and M v(v) =M v(w) andv w then if ψ(v) = ψ(w) then add (v, w) to ; delete all (x, y), s.t.v y; Fig. 4. Three basic unwinding steps but may not terminate if it is safe (which is expected, since program safety is undecidable). We first outline a non-deterministic procedure with three basic steps: Expand, which generates the successors of a leaf vertex, Refine, which refines the labels along a path, labeling an error vertex False, andcover, which expands the covering relation. These steps are shown in Figure 4. The interpolant in Refine can be generated from a refutation of U(π), by the method of [11]. Each of the three steps preserves well-labeledness of the unwinding. In Refine, the first two well-labeledness conditions are guaranteed by the properties of interpolants (i.e., Â 0 = True and each interpolant formula implies the next). When we strengthen ψ(v), we remove all arcs (,v) inthe covering relation, since a vertex covered by v may no longer be covered after strengthening v. InCover, ifavertexv becomes covered, then all descendants of v are also covered. This means that any existing covering arcs (x, y) where v y must be removed to maintain well-labeledness. If Refine succeeds, then ψ(v) mustbefalse (since A ˆ n is always False). Thus, to make the unwinding safe, we have only to apply Refine to every error vertex. Finally, when none of the three steps can produce any change, the unwinding is both safe and complete, so we know the original program is safe.

Lazy Abstraction with Interpolants 129 procedure Close(v V ): for all w V s.t. w v and M v(w) =M v(v): Cover(v, w) recursive procedure DFS(v V ): Close(v) if v is uncovered then if M v(v) =l f then Refine(v); for all w v: Close(w) Expand(v); for all children w of v: DFS(w) procedure Unwind: set V {ɛ}, E, ψ(ɛ) True, while there exists an uncovered leaf v V : for all w V s.t. w v: Close(w); DFS(v) Fig. 5. DFS unwinding strategy To build a well-labeled unwinding, we now have only to choose a strategy for applying the three unwinding rules. The most difficult question is when to apply Cover. Covering one vertex can result in uncovering others. Thus, applying Cover non-deterministically may not terminate. To avoid this possibility, we define a total order on the vertices. This order must respect the ancestor relation. That is, if v w then v w. For example, we could define by a pre-order traversal of the tree, or by numbering the vertices in order of creation. We then restrict Cover to pairs (v, w) such that w v. Now suppose that in adding a covering arc (v, w), we remove (x, y), where v y. Then by transitivity, we must have v x. Thus, covering a vertex v can only result in uncovering vertices greater than v. This implies that we cannot apply Cover infinitely. We will say that a vertex v is closed if either it is covered, or no arc (v, w) can be added to (while maintaining well-labeledness). The procedure Close of Figure 5 closes a vertex. We would like to guarantee that when a vertex is expanded, all of its ancestors are closed, thus we do not expand a vertex that could be covered instead. We could, of course, call Close on all the ancestors of avertexv before expanding it. This would be costly, however. A more efficient strategy is shown in Figure 5. The procedure Unwind locates an uncovered leaf, then performs a local depth-first search around that leaf. During the search, it maintains the invariant that all ancestors of the currently visited leaf vertex v are closed. Moreover, all the vertices on the DFS stack are children of ancestors of v. Thus, when we pop a vertex off of the stack, we have only to call Close on the new vertex to re-establish the invariant. After calling Refine on an error vertex, the procedure calls Close on all of the ancestors v. This can be improved somewhat by only re-closing those vertices that were actually strengthened by Refine. Theorem 2. If procedure Unwind terminates without aborting on program A, then A is safe. Proof. Since only the operations Expand, Refine and Cover alter the unwinding, and these preserve well-labeledness, the resulting unwinding is well-labeled.

130 K.L. McMillan Further, since all error vertices are refined, the unwinding is safe. Since the procedure terminates only when there are no uncovered leaves, the final unwinding is complete. Thus, by Theorem 1, program A is safe. Termination. Due to decidability considerations, we do not expect the unwinding to terminate in all cases. However, in the finite-state case, or in general when the language L(S) has bounded ascending chains, we can show termination. A finite ascending chain is a sequence of formulas φ 0,φ 1,...,φ n such that for all 0 i<j n, φ j φ i. We will say that a language L is k-bounded, for integer k, if all ascending chains in L have length at most k. For example, the Boolean formulas over n variables are 2 n + 1-bounded. Theorem 3. If L(S) is k-bounded, then procedure Unwind terminates or aborts. Proof. Procedure DFS maintains the invariant that all ancestors of v are closed. Thus, there are no x w v such that M v (x) =M v (w) andψ(w) ψ(x) (else w would not be closed). Thus, for any location l, the formulas φ(w) where M v (w) =l and w v form an ascending chain. Since L(S) isk-bounded, it follows that the path from ɛ to v contains at most Λ k vertices. Thus the depth of the tree is bounded. As argued above, Cover cannot continue to coververtices infinitely. Thus, in the main loop, always eventually Close fails to cover a new vertex, or the loop terminates. In the former case, vertex v remains uncovered, andisthusexpandedinproceduredfs. However, we cannot expand vertices infinitely, since the tree depth is bounded. Thus, the loop must terminate (or abort in Refine). A Weak Notion of Completeness. In general, the FO formulas over a given vocabulary S have infinite ascending chains. Thus, the above termination result is not generally applicable. However, by restricting the language of the interpolants, we can force termination (perhaps without deciding safety). That is, given a language L, an L-restricted interpolant for a sequence Γ is an interpolant for Γ in which all formulas are contained in L. Techniques for computing L-restricted interpolants are described in [9]. Given a language L, letus define an unwinding procedure Unwind(L) that differs from Unwind only in that interpolant in procedure Refine is replaced by L-restricted interpolant. If language L is k-bounded, then Unwind(L) must terminate or abort. Moreover, in [9] it is shown that if program A has an inductive invariant expressible in L, then every error path of A has an L-restricted interpolant. Thus Unwind(L) cannot abort, and must terminate proving safety. We can use this idea to create a procedure that is complete in the limited sense that it eventually verifies all programs that have inductive invariants expressible as quantifier-free formulas in a suitable FO theory. That is, we define an infinite chain of k-bounded, quantifier-free restriction languages L 0 L 1, such that every formula is contained in some L k. 1 If a program has a quanti- 1 Quantifier-freeness is required so that the entailment tests in Refine and Close are decidable. Otherwise completeness is relative to an oracle for the theory.

Lazy Abstraction with Interpolants 131 procedure ForceCover(v, w V ) let x be the nearest common ancestor of v and w let π =(v 0,T 0,v 1) (v n 1,T n 1,v n) be the unique path from x to v let Γ = ψ(x) U(π) ψ(w) n if Γ has an interpolant Â0,...,Ân+2 then for i =0...n: let φ = Â i i+1 if ψ(v i) = φ then remove all pairs (,v i)from set ψ(v i) ψ(v i) φ Fig. 6. Procedure to force covering of one vertex by another fier free safety invariant in the theory, then it has an invariant in some L k.we start with L 0 and each time Unwind(L i )aborts,wemoveontol i+1.whenwe reach L k,theunwind(l k ) must terminate. Thus, our approach is complete in the limited sense that it verifies (eventually) any program with a quantifier-free safety invariant in the theory (this is precisely the set of programs that we can verify with predicate abstraction if we can guess the right atomic predicates). Of course, in practice we must choose the restriction languages L k carefully, so that termination occurs for a small value of k. Forced Covering. To speed convergence of the unwinding procedure, we can use interpolant-based refinement to force a vertex v to be covered by some other vertex w. We will call this a forced covering. Suppose that v and w have nearest common ancestor x in the unwinding. We construct the characteristic formula for the path from x to v, asserting ψ(x) at the beginning, and ψ(w) at the end. If this is infeasible (meaning ψ(w) must hold at v) we strengthen all the vertices on the path from x to v by the corresponding interpolant formulas. Thus, we ensure that w covers v. This procedure is depicted in Figure 6. Clearly, attempting all possible forced coverings could be costly. In practice, before expanding a vertex we attempt a forced covering by a few recently generated vertices representing the same program location. This substantially reduces the part of the unwinding that we must explore. Other Optimizations. As in other work using interpolants [6,8], we generate the characteristic formula of a path in static single-assignment (SSA) form. That is, we create a new instance of a program variable only when that variable is modified. This eliminates a large number of constraints of the form x i+1 = x i that occur when a variable is unmodified by a program statement.when refining a program path, we also use a simple slicing (or cone-of-influence reduction) to remove from the program path any assignments that cannot affect the feasibility of the path. Slicing typically removes a large fraction of the assignments in the path, especially initializations of global variables that are not referenced. It should be noted, however, that slicing can affect completeness, since it is possible that a variable that is not referenced is nonetheless necessary to express an inductive invariant (it might even be an auxiliary variable added by the user for this purpose). In practice, however, this has not been observed to occur, and slicing yields a substantial performance improvement.

132 K.L. McMillan Finally, in the Refine and Cover steps, we must test whether one formula entails another, using a decision procedure. Since the same test tends to occur many times, it pays to memoize the decision procedure calls. 3 Experiments The lazy interpolation-based unwinding procedure is implemented in a software model checking tool called Impact 2 (carrying on the tradition of violent acronyms for software model checkers). In this section, we compare the interpolant-based method of Impact with the predicate abstraction approach of Blast. The benchmarks we use are device drivers from the Microsoft Windows DDK,writteninC.Theywereusedastest cases in [6]. Each driver is providedwithatestharness(i.e., a main program that calls the driver functions appropriately in a non-deterministic manner) and is instrumented with auxiliary variables and safety assertions that test whether whether certain rules are obeyed in calling the kernel API functions. 3 All six of the example programs are safe. To check the implementation of Impact, however, we inserted three errors into each example program. Impact detected all 18 errors, each in at most a few seconds. Performance data are reported only for the safe versions. Impact is based on the interpolating prover of [9]. This prover supports a first-order theory with equality, uninterpreted function symbols, and integer difference-bound arithmetic (i.e., predicates of the form x y c, x c or x c, wherec is a constant). It also supports first-order arrays, with interpreted select and store functions. Support for full linear arithmetic is also possible, but currently not for integer models. To handle C programs, we first reduce them to Simple Goto Programs (SGP s). These are programs containing only conditional goto statements, assignments and assertions, and whose only data types are unbounded integers and arrays of unbounded integers. Pointers and records are eliminated by this translation, and function calls are in-lined. This reduction was done using a modified version of the SATABS infrastructure [4]. Unfortunately, space does not permit a description of the translation process here. Once a C program has been translated to a simple goto program, we can model it formally in the logic of the prover. The logic contains operations on arrays, as well as limited arithmetic. We model the unsupported integer operations (such as the bit-wise operators) with uninterpreted functions (thus we may fail to prove safety if it depends on properties of these operators). An assertion in the program is modeled by a conditional branch to the error state l f. Transitions in the model correspond to basic blocks in the goto program. Having modeled the program, we can then verify safety using procedure Unwind(L), where L is the restriction language for interpolation. We use the same sequence of restriction languages L k as in [9]. This restricts the constants in arithmetic formulas to fall in a certain finite set that depends on k, and also restricts depth of function 2 Interpolating software Model checker without Predicate abstraction. 3 Benchmarks available from the author.

Lazy Abstraction with Interpolants 133 Table 1. Performance statistics on device driver benchmarks source SGP Blast Impact Blast Blast Impact Blast Impact name loc loc time(s) time(s) speedup preds post(s) interp(s) vtcs vtcs kbfiltr 12K 2.3K 26.3 3.15 8.3 25 23.3 2.2 1651 744 diskperf 14K 3.9K 102 20.0 5.1 84 92.2 19.3 3232 3885 cdaudio 44k 6.3K 310 19.1 16.2 108 265 11.9 5253 3257 floppy 18K 8.7K 455 17.8 25.6 105 404 16.9 9573 2518 parclass 138K 8.8K 5511 26.2 210 162 5302 22.9 8612 3720 parport 61K 13K 8084 37.1 218 224 7965 31.0 63.5K 12.7K symbol nesting as a function of k. In fact, all of the example programs can be verified with restriction language L 0. For comparison to predicate abstraction approach, we use the Blast software model checker [7]. This tool is in some ways a good comparison, since it is also based on the lazy abstraction paradigm (using predicate abstraction instead of interpolation to refine paths). In addition, it uses the same interpolating prover to generate atomic predicates that Impact uses for path refinement. Thus in principle both tools should be able to construct the same class of safety invariants. On the other hand, the implementations are independent, so observed performance differences may be due in part to implementation efficiencies. In principle the closest comparison could be obtained by running both programs on the same SGP. However, as it turns out the performance of Blast was significantly better when run on the original C source code. This may be because the elimination of pointers prevented the use of some pointer-based optimizations in Blast. For this reason, we present performance numbers for Blast as run on the original source code. We use the standard Blast option that assigns to each new vertex all of the predicates that have been used for program locations in the same function scope. This tends to increase the number of predicates at each vertex, but reduces the number of refinements needed, thus yielding better performance. 4 Table 1 compares the run time performance of Blast and Impact on the six device driver examples. The first three columns show the name of the example, the number of textual lines in the source code, and the number of lines in the SGP. The last probably provides a better representation of the code size, since the source code contains much white space and many redundant declarations. The next two columns provide the run times for Blast and Impact. Bothare run on a 3GHz Intel Xeon processor. These times represent only the model checking process, and do not include time for parsing or translation to an SGP. The next column shows the speedup of Impact relative to Blast. Forthesmall examples, Impact has about an order of magnitude advantage, which increases to two orders of magnitude for the large examples. The explanation for the performance difference may lie in the fact that the abstract post computation becomes increasingly expensive as the programs get 4 The Blast options used were -msvc -nofp -dfs -tproj -cldepth 1 -predh 6 -scope -nolattice -clock.

134 K.L. McMillan Table 2. Performance statistics for revised Blast source SGP Blast Impact Blast Blast Impact Blast Impact name loc loc time(s) time(s) speedup preds post(s) interp(s) vtcs vtcs kbfiltr 12K 2.3K 11.9 3.15 3.8 38 6.6 2.2 1009 744 diskperf 14K 3.9K 117 20.0 5.9 119 49.8 19.3 1855 3885 cdaudio 44k 6.3K 202 19.1 10.6 180 114 11.9 3400 3257 floppy 18K 8.7K 164 17.8 9.2 154 77.9 16.9 2856 2518 parclass 138K 8.8K 463 26.2 17.7 242 175 22.9 5003 3720 parport 61K 13K 324 37.1 8.7 280 156 31.0 10.4K 12.7K larger and the number of predicates increases. The table shows some run-time statistics that bear this out. Columns 7 9 show the number of atomic predicates used by Blast, the amount of time spent by Blast in the predicate image computation, and the amount of time spent by Impact in computing interpolants for path refinement. It is clear that avoiding the predicate image computation provides a significant advantage. The last two columns of the table show the number of vertices in the final unwinding for both Blast and Impact. Blast expands more vertices (though not enough to fully account for the performance difference). This may be because the predicate images computed by Blast are stronger than necessary. Thus Blast distinguishes states that need not be distinguished, resulting in a larger unwinding. After this paper was originally submitted, Ranjit Jhala improved the performance of Blast by making it less lazy. In this version, each new vertex in the unwinding is assigned all the predicates seen thus far for the same program location, or if there are none, then predicates of its parent. This slightly eager approach greatly reduces the number of refinement steps. The reduction in refienements makes it practical to use only the predicates from the same location, rather than the same function scope, which reduces the number of predicates per vertex and thus speeds the predicate image computation substantially. Table 2 shows comparison data for this new version. 5 The performance gap between Blast and Impact is now considerably smaller (only one order of magnitude). It could be that computing some state information in an eager manner would reduce the number of refinement steps of Impact as well. We leave this question for future research. 4 Conclusion We have described a method that uses interpolation rather than predicate abstraction in the lazy abstraction paradigm. This avoids the most costly operation of predicate abstraction, the abstract image computation. In contrast to the interpolation-based model checking method of [10], it avoids constructing and refuting an unfolding of the entire program. Instead, the interpolating prover is 5 Blast options for this experiment were -msvc -nofp -craig 2 -scope -cldepth 1 -bfs except for cdaudio, which also required -clock. No single set of options was able to verify all the examples.

Lazy Abstraction with Interpolants 135 applied only to individual program paths, greatly lessening the burden on the prover. This makes it possible to apply the interpolation-based approach to the verification of infinite-state sequential programs. For a small collection of device driver examples, a run-time improvement of one to two orders of magnitude was obtained, relative to the lazy predicate abstraction approach. Although a greater variety of examples is clearly needed to study the trade-offs between the two methods, the experiments show that the interpolation method has the potential to provide a substantial performance improvement. There are several potentially interesting topics for future research. Consider, for example, the following simple C program fragment: for(i = 0; i < n; i++) x[i] = 0; for(i = 0; i < n; i++) assert(x[i] == 0); A safety invariant of this program requires a universal quantifier over the index of the array. Thus, predicate abstraction methods that use atomic predicates cannot verify this program. However, in [11] it is shown that an interpolating prover can be used to generate interpolants with quantifiers. This opens the possibility of generating quantified inductive invariants with the present method. There are several challenges involved in this. First the decision procedure must handle quantified formulas. Since the validity of quantified formulas is undecidable, we must have heuristics to instantiate quantifiers. Second, we must somehow prevent the number of quantifiers in the interpolants from increasing without bound. Although these problems remain to be solved, using a naïve approach to quantifier instantiation it is possible to verify simple programs like the above. Thus, it may be possible to use the method to verify properties that depend, for example, on the contents of arrays. It also seems possible that the interpolation approach can be made to scale better by using function summaries, in an approach that might be called summaries on demand. If we refute a program path that contains the expansion of a procedure call, we can derive an interpolant that is an over-approximation of the transition relation of the procedure (in the same way that transition relation approximations are derived in [8]). This approximation can be used as an abstraction (summary) of the procedure. When an error path is found not to be refutable, it might be refined by expanding one or more summarized functions, which would strengthen the summaries of the expanded functions. Thus, there seems to be scope for both enriching the class of properties that can be verified, and for improving the performance of the method on large programs. Acknowledgments. Thanks to Daniel Kröning for providing the SATABS tool infrastructure used in this work, to Ranjit Jhala for help with Blast, andto the anonymous reviewers for useful comments and corrections. References 1. T. Ball, A. Podelski, and S. K. Rajamani. Boolean and Cartesian abstraction for model checking C programs. In T. Margaria and W. Yi, editors, TACAS, volume 2031 of LNCS, pages 268 283. Springer, 2001.

136 K.L. McMillan 2. T. Ball and S. K. Rajamani. The SLAM project: debugging system software via static analysis. In POPL, pages 1 3, 2002. 3. S. Chaki, E. M. Clarke, A. Groce, S. Jha, and H. Veith. Modular verification of software components in C. In ICSE, pages 385 395. IEEE Computer Society, 2003. 4. E. Clarke, D. Kroening, N. Sharygina, and K. Yorav. SATABS: SAT-based predicate abstraction for ANSI-C. In TACAS, volume 3440 of LNCS, pages 570 574. Springer Verlag, 2005. 5. W. Craig. Three uses of the Herbrand-Gentzen theorem in relating model theory and proof theory. J. Symbolic Logic, 22(3):269 285, 1957. 6. T. A. Henzinger, R. Jhala, R. Majumdar, and K. L. McMillan. Abstractions from proofs. In POPL, pages 232 244, 2004. 7. T. A. Henzinger, R. Jhala, R. Majumdar, and G. Sutre. Lazy abstraction. In POPL, pages 58 70, 2002. 8. R. Jhala and K. L. McMillan. Interpolant-based transition relation approximation. In K. Etessami and S. K. Rajamani, editors, CAV, volume 3576 of LNCS, pages 39 51. Springer, 2005. 9. R. Jhala and K. L. McMillan. A practical and complete approach to predicate refinement. In H. Hermanns and J. Palsberg, editors, TACAS, volume 3920 of LNCS, pages 459 473. Springer, 2006. 10. K. L. McMillan. Interpolation and SAT-based model checking. In CAV, pages 1 13, 2003. 11. Kenneth L. McMillan. An interpolating theorem prover. Theor. Comput. Sci., 345(1):101 121, 2005. 12. H. Saïdi and S. Graf. Construction of abstract state graphs with PVS. In CAV, pages 72 83, 1997.