K. Rustan M. Leino Microsoft Research, Redmond, WA, USA 19 July 2007 Invited talk, CADE-21 Bremen, Germany
Mike Barnett Nikolaj Bjørner Evan Chang Ernie Cohen Ádám Darvas Leo de Moura Manuel Fähndrich Bart Jacobs Francesco Logozzo Ronald Middelkoop Rosemary Monahan Peter Müller Ralf Sasse Wolfram Schulte Jan Smans Herman Venter Angela Wallenburg Burkhart Wolff
Problem Building and maintaining programs that are correct Approach Specifications record design decisions bridge intent and code Tools amplify human effort manage details find inconsistencies ensure quality
Hoare, Joshi, Leavens, Misra, Naumann, Shankar, Woodcock, et al. “We envision a world in which computer programs are always the most reliable component of any system or device that contains them” [Hoare & Misra]
Spec# language Object-oriented.NET language Superset of C#, adding: more types specifications (pre- and postconditions, etc.) Usage rules (methodology) Checking: Static type checking Run-time checking Static verification (optional)
Sound modular verification Focus on automation, not full functional correctness specifications No termination verification No verification of temporal properties
static verifier (Boogie) MSIL (“bytecode”) SMT solver V.C. generator Inference engine Translator verification condition “correct” or list of errors Spec# compiler Spec# BoogiePL
C HAVOC and VerifiedC C HAVOC and VerifiedC Eiffel …? Java bytecode + BML Spec# Z3 Simplify Zap 2 SMT Lib Fx7 SMT Lib Fx7 Isabelle/ HOL …? BoogiePL abstract interpreter predicate abstraction? termination detector? …?
EUF, arithmetic, quantifiers Counterexamples Fast, for both valid and invalid formulas
Non-chronological backtracking Avoid “exponential splitting” Incremental matching -- Leonardo de Moura, on Z3 for Boogie
type const function axiom var procedure implementation
x := E a[ i ] := E havoc x assert E assume E ; call P() if while break label: goto A, B
class C : object { int x; C( ) { … } virtual int M(int n) { … } static void Main() { C c = new C( ); c.x = 12; int y = c.M(5); } } class C : object { int x; C( ) { … } virtual int M(int n) { … } static void Main() { C c = new C( ); c.x = 12; int y = c.M(5); } }
// class types const unique System.Object: name; const unique C: name; axiom C <: System.Object; function typeof(ref) returns (name); // fields type field; const unique C.x: field; const unique allocated: field; // the heap var Heap: [ref, field] int; class C : object { int x; class C : object { int x;
// method declarations procedure C..ctor(this: ref); requires this != null && typeof(this) <: C; modifies Heap; procedure C.M(this: ref, n: int) returns (result: int); requires this != null && typeof(this) <: C; modifies Heap; procedure C.Main(); modifies Heap; C() { … } virtual int M(int n) static void Main() C() { … } virtual int M(int n) static void Main()
// method implementations implementation C.Main() { var c: ref, y: int; havoc c; assume c != null; assume Heap[c, allocated] == 0; assume typeof(c) == C; Heap[c, allocated] := 1; call C..ctor(c); assert c != null; Heap[c, C.x] := 12; call y := C.M(c, 5); } C c = new C(); c.x = 12; int y = c.M(5);
0.Cut back edges 1.Remove assignments 2.(Recursively) apply wp (with some form of memoization)
class C { int f; object g; } var f: [ref] int; var g: [ref] ref; type Field; type Value; var Heap: [ref, Field] Value; function V2I(Value) returns (int); function I2V (int) returns (Value); axiom ( v: Value I2V(V2I(v)) == v); axiom ( i: int V2I(I2V(i)) == i); type Field ; var Heap: . [ref, Field ] ; For C: select(Memory, Region, Offset, Length) store(Memory, Region, Offset, Length, Value) x = c.f; x := f [ c ]; x = c.f; x := V2I(Heap[ c, f ]); x = c.f; x := Heap[ c, f ];
public string NextChunk() modifies this.*; ensures result.Length <= ChunkSize; public string NextChunk() modifies this.*; ensures result.Length <= ChunkSize;
procedure Chunker.NextChunk(this: ref where $IsNotNull(this, Chunker)) returns ($result: ref where $IsNotNull($result, System.String)); // in-parameter: target object free requires $Heap[this, $allocated]; requires ($Heap[this, $ownerFrame] == $PeerGroupPlaceholder || !($Heap[$Heap[this, $ownerRef], $inv] $Heap[$pc, $inv] == $typeof($pc) && $Heap[$pc, $localinv] == $typeof($pc)); // out-parameter: return value free ensures $Heap[$result, $allocated]; ensures ($Heap[$result, $ownerFrame] == $PeerGroupPlaceholder || !($Heap[$Heap[$result, $ownerRef], $inv] $Heap[$pc, $inv] == $typeof($pc) && $Heap[$pc, $localinv] == $typeof($pc)); // user-declared postconditions ensures $StringLength($result) <= $Heap[this, Chunker.ChunkSize]; // frame condition modifies $Heap; free ensures (forall $o: ref, $f: name :: { $Heap[$o, $f] } $f != $inv && $f != $localinv && $f != $FirstConsistentOwner && (!IsStaticField($f) || !IsDirectlyModifiableField($f)) && $o != null && old($Heap)[$o, $allocated] && (old($Heap)[$o, $ownerFrame] == $PeerGroupPlaceholder || !(old($Heap)[old($Heap)[$o, $ownerRef], $inv] old($Heap)[$o, $f] == $Heap[$o, $f]); // boilerplate free requires $BeingConstructed == null; free ensures (forall $o: ref :: { $Heap[$o, $localinv] } { $Heap[$o, $inv] } $o != null && !old($Heap)[$o, $allocated] && $Heap[$o, $allocated] ==> $Heap[$o, $inv] == $typeof($o) && $Heap[$o, $localinv] == $typeof($o)); free ensures (forall $o: ref :: { $Heap[$o, $FirstConsistentOwner] } old($Heap)[old($Heap)[$o, $FirstConsistentOwner], $exposeVersion] == $Heap[old($Heap)[$o, $FirstConsistentOwner], $exposeVersion] ==> old($Heap)[$o, $FirstConsistentOwner] == $Heap[$o, $FirstConsistentOwner]); free ensures (forall $o: ref :: { $Heap[$o, $localinv] } { $Heap[$o, $inv] } old($Heap)[$o, $allocated] ==> old($Heap)[$o, $inv] == $Heap[$o, $inv] && old($Heap)[$o, $localinv] == $Heap[$o, $localinv]); free ensures (forall $o: ref :: { $Heap[$o, $allocated] } old($Heap)[$o, $allocated] ==> $Heap[$o, $allocated]) && (forall $ot: ref :: { $Heap[$ot, $ownerFrame] } { $Heap[$ot, $ownerRef] } old($Heap)[$ot, $allocated] && old($Heap)[$ot, $ownerFrame] != $PeerGroupPlaceholder ==> old($Heap)[$ot, $ownerRef] == $Heap[$ot, $ownerRef] && old($Heap)[$ot, $ownerFrame] == $Heap[$ot, $ownerFrame]) && old($Heap)[$BeingConstructed, $NonNullFieldsAreInitialized] == $Heap[$BeingConstructed, $NonNullFieldsAreInitialized];
Instantiation via e-graph matching A matching pattern (trigger) is a set of terms that together mention all the bound variables, and none of which is just a bound variable by itself Examples: ( x { f(x) } 0 ≤ f(x)) ( x,y { g(x,y) } f(x) < g(x,y))
( x,y { f(x), f(y) } x ≤ y f(x) ≤ f(y)) ( x { f(x) } x ≠ null f(x) ≤ f(next(x))) ( x { f(next(x)) } x ≠ null f(x) ≤ f(next(x))) ( x,y { f(x), f(y) } f(x) = f(y) x = y) ( x { f(x) } f Inv(f(x)) = x) ( x { f(x+1) } f(x) ≤ f(x+1)) ( x,y,z { x*(y+z) } x*(y+z) = x*y + x*z) ( x,y { P(x,y) } x = y P(x,y) = 10) ( x { P(x,x) } P(x,x) = 10)
Introduce IsHeap(h) to mean “h is a well-formed heap” class C { T f; … } ( h,o IsHeap(h) o ≠ null h[o,f ] = null typeof(o) <: T ) ( h,o IsHeap(h) o ≠ null h[o,allocated] h[o,f ] = null h[h[o,f ], allocated] ) Since the heap uses the select/store axioms, it cannot be enforced by the BoogiePL type system
The encoding needs to introduce assumptions that IsHeap holds o.f = E; Heap[o,f ] := E; assume IsHeap(Heap); IsHeap(Heap) is a free pre- and postcondition of every method free requires IsHeap(Heap); modifies Heap; free ensures IsHeap(Heap);
Mutable Consistent Committed c: Chunker StringBuilder ownership sb expose (c) { … } Valid
Mutable Committed c: Chunker StringBuilder ownership expose (c) { … } sb Consistent Valid
Mutable Committed c: Chunker StringBuilder ownership expose (c) { … } sb Consistent Valid
Mutable Committed c: Chunker StringBuilder ownership expose (c) { … } sb Consistent Valid
class C { int x; int y; invariant x ≤ y; … } ( h, o IsHeap(h) x ≠ null h[o,allocated] typeof(o) <: C Valid(o, h) h[o,x] ≤ h[o,y] )
o is peer consistent in h (h[o, owner] = Valid(h[o, owner])) ( p h[p, owner] = h[o, owner] Valid(p,h)) Peer consistency is the default precondition of all parameters of all methods Needs to be proved (all over!)
void M( ) modifies this.x, this.y; { … } modifies Heap; ensures ( o, f Heap[o,f] = old(Heap)[o,f] (o = this f = x) (o = this f = y) old(Heap)[o, allocated] // or o is committed in old(Heap) old(Heap[o,owner] Valid(Heap[o,owner], Heap)) )
Types find errors in translation Some types are required by some provers (e.g., SMT Lib)
type R;// type representing records type Field ; function Get: . R Field ; function Set: . R Field R; axiom ( ( r: R, f: Field , g: Field , x: f = g Get(Set(r, f, x), g) = x )); axiom ( , ( r: R, f: Field , g: Field , x: f g Get(Set(r, f, x), g) = Get(r, g) )); Type error
class C { int x; bool y; void M() modifies this.x, this.y; { … } const x: Field int; const y: Field bool; procedure M(this: ref); modifies Heap; ensures ( ( o: ref, f: Field Heap[o,f] = old(Heap)[o,f] (o = this f = x) (o = this f = y) … ) Type errors
How complicated does the type system need to be? parametric polymorphism? subtyping? type (disequality) constraints? guarded types? How is it translated into provers with less advanced type systems? Performance penalty (ironically) Does it really help the prover that much? [Meng & Paulson] [Couchot & Lescuyer]
Labeled subformulas Useful for: pin-pointing location and type error, and determining execution traces leading to error Models Can be used to print better error messages Proving existentials Needed to prove that a function is well- defined Matching not up to the task (there is nothing to match against)
Download Spec# and Boogie from here Spec# system, Boogie, BoogiePL To verify, use an intermediate language Separates concerns Promotes sharing in verification community Front ends for multiple languages Multiple theorem provers Shared benchmarks Quantifiers are both crucial and convenient Reduce the need for other theories Triggers are important Need to be carefully designed Include in competitions. SMT Comp? What’s a good type system for prover input? Hardest part in designing VCs: programming methodology that Fits common programming idioms and Can be handled well by automatic prover Education