A simple sequential reasoning approach for sound modular

  • Slides: 36
Download presentation
A simple sequential reasoning approach for sound modular verification of mainstream multithreaded programs Wolfram

A simple sequential reasoning approach for sound modular verification of mainstream multithreaded programs Wolfram Schulte & Bart Jacobs Microsoft Research & K. U. Leuven TV 2006

Agenda Background - Wolfram • Spec#: A verifying compiler(*) Foreground - Bart • Verification

Agenda Background - Wolfram • Spec#: A verifying compiler(*) Foreground - Bart • Verification of Data Race Freedom • Verification of Invariants • Verification of Deadlock Freedom _____ 2

A Verifying Compiler A verifying compiler uses automated. . reasoning to check the correctness

A Verifying Compiler A verifying compiler uses automated. . reasoning to check the correctness of the program that it compiles. “ Correctness is specified by types, assertions, . . and other redundant annotations that accompany the program. ” [Hoare, 2004] 3

The Spec# Verifying Compiler • As source language we use C# • As specifications

The Spec# Verifying Compiler • As source language we use C# • As specifications we use method contracts, invariants, and also class, field and type annotations • As program logic we use Dijkstra’s weakest preconditions • For automatic verification we use type 4

Spec#: Research Challenge How to verify object invariants in the presence of • Callbacks

Spec#: Research Challenge How to verify object invariants in the presence of • Callbacks • Aliasing • Multi-threading 5

Example: Callbacks and Invariants class Subject{ Observer obs; int state=1; invariant state !=0; Subject(Observer

Example: Callbacks and Invariants class Subject{ Observer obs; int state=1; invariant state !=0; Subject(Observer o){ this. obs = o; o. sub = this; } void Update(int y) { state = 0; obs. Notify(); state = y; } int Get() { return Program. N/state; } class Observer{ Subject sub; int cache; void Notify() { cache = sub. Get(); } } class Program{ static int N = 100; static void Main() { Observer o = new Observer(); Subject s = new Subject(o); s. Update(5); } 6

Example: Callbacks and Invariants Subject s Observer o Invariant holds Update(x) Invariant might be

Example: Callbacks and Invariants Subject s Observer o Invariant holds Update(x) Invariant might be broken Invariant holds Invariant might be broken Execution within Get() callback Update Invariant holds Notify() Time 7

Callbacks and Invariants: Allow invariants temporarily to be broken Design • Objects can be

Callbacks and Invariants: Allow invariants temporarily to be broken Design • Objects can be valid or mutable • Mutable objects need not satisfy their invariants • Mutability is changed by special source commands – unpack(o) to make o mutable – pack(o) to re-establish invariant and make o 8

Example: Invariants and Aliasing class Subject{ Observer obs; int state=1; invariant state!=0; class Observer{

Example: Invariants and Aliasing class Subject{ Observer obs; int state=1; invariant state!=0; class Observer{ Subject sub; int cache; invariant sub!=null cache== Program. N/sub. state; Subject(Observer o){ this. obs = o; o. sub = this; } void Notify() { cache = sub. Get(); } } void Update(int y) { unpack(o); state = y; pack(o); obs. Notify(); } int Get() { return Program. N/state; } class Program{ static int N = 100; static void Main() { Observer o = new Observer(); Subject s = new Subject(o); s. Update(5); } 9

Example: Invariants and Aliasing Update(5) : Subject invariant state!=0 state ==10 5 inv =

Example: Invariants and Aliasing Update(5) : Subject invariant state!=0 state ==10 5 inv = Get() obs Notify() o: Observer cache==100/sub. state cache == 10 20 sub Valid Mutable 10

Invariants and Aliasing: Use corresponding proof / design patterns Design When an object is

Invariants and Aliasing: Use corresponding proof / design patterns Design When an object is mutated, then all of its dependents must be mutable Two kinds of dependencies: • Visibility for peer structures – An object whose field is mentioned in an invariant needs to know its dependents • Ownership for hierarchical abstraction – An object does not need to know its owner 11

A Simple Sequential Reasoning Approach for Sound Modular Verification of Mainstream Multithreaded Programs 12

A Simple Sequential Reasoning Approach for Sound Modular Verification of Mainstream Multithreaded Programs 12

Demo: Verifying a Chat Server 13

Demo: Verifying a Chat Server 13

Outline of the talk • Data race prevention • Invariants and ownership trees •

Outline of the talk • Data race prevention • Invariants and ownership trees • Deadlock prevention 14

Multithreading Multiple threads running in parallel, reading and writing shared data A data race

Multithreading Multiple threads running in parallel, reading and writing shared data A data race occurs when a shared variable is written by one thread and concurrently read or written by another thread How to guarantee that there are no data races? class Counter { int dangerous; void Inc() { int tmp = dangerous; dangerous = tmp + 1; } } Counter ct = new Counter(); new Thread(ct. Inc). Start(); // What is the value of // ct. dangerous after both // threads have terminated? 15

Mutexes: Avoiding Races • Mutual exclusion for shared objects is provided via locks •

Mutexes: Avoiding Races • Mutual exclusion for shared objects is provided via locks • Locks can be obtained using a lock block. A thread may enter a lock (o) block only if no other thread is executing inside a lock (o) block; else, the thread waits • When a thread holds a lock on object o, C#/Java 16

Program Method for Avoiding Races Our program rules enforce that a thread t can

Program Method for Avoiding Races Our program rules enforce that a thread t can only access a field of object o if o is either thread local or t has locked o We model accessibility using access sets: • A thread’s access set consists of all objects it has created but not shared yet or whose lock it holds. • Threads are only allowed to access fields 17 of objects in their corresponding access set

Annotations Needed to Avoid Races • Threads have access sets – t. A is

Annotations Needed to Avoid Races • Threads have access sets – t. A is a new ghost field in each thread t describing the set of accessible objects • Objects can be shared – o. shared is a new boolean ghost field in each object o – share(o) is a new operation that shares an unshared o • Fields can be declared to be shared – Shared fields can only be assigned shared 18

Object Life Cycle and Access Sets new share unshared acquire free locked release shared

Object Life Cycle and Access Sets new share unshared acquire free locked release shared 1. 2. 3. 4. A new o is unshared, and added to tid. A. An unshared o can be made accessible for other threads by sharing it; o is taken out of tid. A A shared o can be exclusively acquired by locking it; when locking succeeds o is added to tid. A A locked o can be released for others by unlocking it; o is taken out of tid. A. 19

Verification via Access Sets Tr[[o = new C(); ]] = … o. shared: =

Verification via Access Sets Tr[[o = new C(); ]] = … o. shared: = false; tid. A[o]: = true Tr[[x = o. f; ]] = … assert tid. A[o]; x : =o. f; Tr[[o. f = x; ]] = … assert tid. A[o]; if (f is declared shared) assert x. shared; o. f : =x; Tr[[share(o)]] = … assert tid. A[o]; assert ! o. shared; o. shared : =true; tid. A[o] : =false; Tr[[lock (o) S ]] = … assert ! tid. A[o]; assert o. shared; havoc o. *; tid. A[o]: =true; Tr[[S]]; tid. A[o]: = false 20

Verification via Access Sets The need for havoc on the lock rule. When a

Verification via Access Sets The need for havoc on the lock rule. When a thread acquires o, this is modeled by assigning an arbitrary new value to o’s fields, since o might have been changed by another thread int x; lock (o) { x = o. f; } lock (o) { assert x == o. f; // fails } 21

Example for Data Race Freedom Counter ct = new Counter(); share(ct); new Thread(delegate ()

Example for Data Race Freedom Counter ct = new Counter(); share(ct); new Thread(delegate () { lock (ct) ct. Inc(); }). Start(); 22

Example for Data Race Freedom // thread t 0 Counter ct = new Counter();

Example for Data Race Freedom // thread t 0 Counter ct = new Counter(); share(ct); Session s 1 =new Session(ct, 1); Session s 2 =new Session(ct, 2); // transfers s 1 to t 1 = new Thread(s 1. Run); // transfers s 2 to t 2 = new Thread(s 2. Run); t 1. Start(); t 2. Start(); class Session { shared Counter ct ; int id; Session(Counter ct , int id) requires ct. shared; ensures tid. A[this] ∧ ! this. shared; { this. ct=ct; this. id=id; } void Run() requires tid. A[this]; { for (; ; ) lock (this. ct) this. ct. Inc(); }}} 23

Soundness Theorem • threads t 1, t 2 : : t 1 t 2

Soundness Theorem • threads t 1, t 2 : : t 1 t 2 t 1. A t 2. A = • object o, thread t : : o. shared && o ∈ t. A t holds o’s lock • Proof sketch for Theorem – – new share (o) Entry into lock (o) Exit from lock (o) Corollary • Valid programs don’t have data races 24

Outline of the talk • Data race prevention • Invariants and ownership trees •

Outline of the talk • Data race prevention • Invariants and ownership trees • Deadlock prevention 25

Invariants and Concurrency Invariants, whether over a single object or over an ownership tree,

Invariants and Concurrency Invariants, whether over a single object or over an ownership tree, can be protected via a single lock (coarse grained locking) For sharing and locking we • require an object o to be valid when o becomes free. This ensures that when a thread locks o, it can assume o’s invariant For invariants involving owned objects • pack(o) deletes o’s owned objects from the thread’s access set, • unpack(o) adds o’s owned objects to the thread’s access set, 26

Verifying Multi-threaded Pack/Unpack Tr[[unpack o; ]] = assert tid. A[o]; assert o. inv; foreach

Verifying Multi-threaded Pack/Unpack Tr[[unpack o; ]] = assert tid. A[o]; assert o. inv; foreach (c | c. owner = o) { tid. A[c] : = true; } o. inv : = false; Tr[[ pack o; ]] = assert tid. A[o]; assert ! o. inv; assert c: c. owner = o tid. A[c] ∧ c. inv; foreach (c | c. owner = o) { tid. A[c] : = false; } assert Inv( o ); o. inv : = true; 27

Ownership: Verifying Lock Blocks Finally, when locking we also have to “forget the knowledge

Ownership: Verifying Lock Blocks Finally, when locking we also have to “forget the knowledge about” (= assign an arbitrary value to) owned objects Tr[[lock (o) S; ]] = assert o. shared; assert ! tid. A[o]; foreach (p | !tid. A[p]) havoc p. *; tid. A[o]: =true; Tr[[S]] ; 28

Outline of the talk • Data race prevention • Invariants and ownership trees •

Outline of the talk • Data race prevention • Invariants and ownership trees • Deadlock prevention 29

Concurrency: Deadlocks A deadlock occurs when a set of threads each wait for a

Concurrency: Deadlocks A deadlock occurs when a set of threads each wait for a mutex (i. e shared object) that another thread holds Methodology: • partial order over all shared objects • in each thread, acquire shared Dining Philosophers Fork 1 3 1 Fork 3 2 objects in descending order Fork 2 1 has F 1, waits for F 2 2 has F 2, waits for F 3 3 has F 3, waits for F 1 30

Annotations Needed to Avoid Deadlocks We construct a partial order on shared objects, denoted

Annotations Needed to Avoid Deadlocks We construct a partial order on shared objects, denoted by ≺. • When o is shared, we add edges to the partial order as specified in the share command’s where clause. (Specified lower bounds have to be less than specified upper bounds) 31

Verification via Lock Ordering and Lockstacks Tr[[share o where p ≺ o && o

Verification via Lock Ordering and Lockstacks Tr[[share o where p ≺ o && o ≺ q; ]] = assert o tid. A; assert ! o. shared; tid. A[o] : = false; o. shared : = true; assert p ≺ q; assume p ≺ o && o ≺ q; Tr[[lock (o) S ]] = assert o. shared; assert tid. lockstack != empty o ≺ tid. lockstack. top(); tid. lock. Stack. push(o); foreach (p | !tid. A[p]) havoc p. *; tid. A[o]: =true; Tr[[S]] ; tid. A[o]: = false; tid. lockstack. pop(o); 32

Example: Deadlock Avoidance (contd. ) righ Fork 1 3 left t Dining Philosophers Fork

Example: Deadlock Avoidance (contd. ) righ Fork 1 3 left t Dining Philosophers Fork 3 rig le f t ht 1 2 rig h lef t t Fork 2 f 1 = new Fork(); share f 1; f 2 = new Fork(); share f 2 where f 1 ≺ f 2; f 3 = new Fork(); share f 3 where f 2 ≺ f 3 ; P 1 = new Thread( delegate() { lock (f 2) { lock (f 1) { /*eat*/ }}}); P 1. Start(); P 2 = new Thread( delegate() { lock (f 3) { lock (f 2) {/*eat*/ }}}); P 2. Start(); P 3 = new Thread( delegate() { lock (f 3) { lock (f 1) {/*eat*/ }}}); P 3. Start(); 33

Conclusion • Clients can reason entirely as if world was single-threaded for non-shared objects

Conclusion • Clients can reason entirely as if world was single-threaded for non-shared objects • Supports caller-side locking and calleeside locking • Deadlocks are prevented by partially ordering shared objects 34

Related Work • Type systems for safe concurrency – Checking is faster – Support

Related Work • Type systems for safe concurrency – Checking is faster – Support fewer programs – Do not verify object invariants or other assertions • Separation logic – Theorem proving technology less mature – Not yet applied to Java-style locking • Rely/guarantee reasoning – Too cumbersome for simple programs – Is complementary 35

The End (for now) http: //research. micsoft. com/specsharp

The End (for now) http: //research. micsoft. com/specsharp