Towards full verification of concurrent libraries Viktor Vafeiadis
Program verification Programs considered: Small (<100 LOC) Medium (KLOC) Large (MLOC) Simple Complicated Properties proved: functional correctness Basic safety Full correctness no memory no crashes termination leaks Proof automation: Manual Semi-automatic Automatic
Concurrent libraries? Small (<100 LOC) Medium (KLOC) Large (MLOC) Simple Complicated Imperative data structures ... dynamically allocated Highly concurrent ... shared memory concurrency ... with intended race conditions Simple interfaces (stacks, queues, finite maps) No higher-order functions linearizability Basic safety Full correctness no memory no crashes leaks lock-freedom
My program verification work deny-guarantee linearizability correctness PPoPP’06 ESOP’09 CAV’10 CONCUR’07 POPL’10 VMCAI’09 ECOOP’10 RGSep POPL’09 lock-freedom program logics case studies p. techniques VMCAI’10 tools SAS’07 APLAS’09 safety manual semi-automatic automatic
Michael & Scott non-blocking queue head tail null X 1 2 3 4 typedef struct Node_s *Node; struct Queue_s *Q; struct Node_s { init() { int val; n = new Node(); Node next; n → next = null; } Q = new Queue(); Q → head = node; struct Queue_s { Q → tail = node; Node head; } Node tail; }
A slight complication... The tail pointer can lag behind by one node: head tail null X 1 2 3 Except when the queue is empty: head tail null Y X
Enqueue & dequeue Don’t read the code ! dequeue () { enqueue(v) { while (true) { m = new Node(); m → val = v; h = Q → head; m → next = null; t = Q → tail; while (true) { n = h → next; t = Q → tail; if (Q → tail ≠ t) continue ; n = tail → next; if (h == t) { if (Q → tail ≠ tail) continue ; if (n == null) if (n == null) { return EMPTY; if (CAS(&t → next,n,m)) CAS(&Q → tail,t,n); break; } else { } else { if (CAS(&Q → head,h,n)) CAS(&Q → tail,t,n); return n → val; } } } } CAS(&Q → tail,t,n); } }
Length (first attempt) length() { num = 0; curr = Q → head → next; while (curr ≠ null) { num++; curr = curr → next; } return num; } head tail null X 1 2 3 4
Length (second attempt) length() { num = 0; while (true) { t = Q → tail; n = tail → next; Read Q → tail, and ensure if (n == null) break ; that Q → tail → next == null CAS(&Q → tail,t,n); } curr = Q → head → next; while (curr ≠ null) { num++; if (curr == t) break; curr = curr → next; } return num; }
Length (third attempt) length() { num = 0; do { h = Q → head; while (true) { Get a snapshot of t = Q → tail; Q → head and Q → tail n = tail → next; and ensure that if (n == null) break ; Q → tail → next==null. CAS(&Q → tail,t,n); } } while (h ≠ Q → head); curr = h → next; while (curr ≠ null) { num++; if (curr == t) break; curr = curr → next; } return num; }
Verification challenge Functional correctness: Every method executes ‘atomically’ and obeys a high-level specification VMCAI ’09 CAV ’10 Liveness properties, e.g. lock-freedom: At all times, some outstanding method call is guaranteed to terminate. POPL ’09 But first, do shape analysis to: (1) Find data structure invariants VMCAI ’10 (2) Prove memory safety
RGSep Combining rely-guarantee and separation logic
Whence RGSep? Separation logic [Reynolds, O’Hearn ~’01]: Good at describing heap-allocated data structures Local reasoning: proofs mention only the footprint . head tail null ∃ h,t. Q ↦ head:h,tail:t ∗ lseg(h,t) ∗ t ↦ next:null Rely-guarantee [Jones, ’83]: Good at reasoning about concurrency Describes interference between threads: how the state evolves
Local & shared assertions Logically divide the state into: Local: only one thread can access it Shared: any thread can access it. Example: enqueue just before the loop: local state shared state head tail null null RGSep assertions: p, q ::= (P local ¦ P shared ) normal separation logic | p 1 ∨ p 2 assertions (about the local & shared state respectively) | ∃ x. p
Rely-guarantee specifications Rely: interference caused by environment; Describes how the environment is allowed to change the shared precondition state R,G ⊢ RGSep {p} cmd {q} Guarantee: postcondition interference caused by the program. Describes how the program is allowed to change the shared state.
RGSep actions (pre-/postcondition pairs) Summarize the shared state updates head tail head tail Enqueue null null A B A head tail head tail Dequeue A B A B head tail head tail Advance tail pointer A B A B
The actions of enqueue & dequeue enqueue(v) { dequeue () { m = new Node(); while (true) { m → val = v; Local h = Q → head; m → next = null; updates t = Q → tail; while (true) { n = h → next; t = Q → tail; if (Q → tail ≠ t) continue ; n = tail → next; if (h == t) { if (Q → tail ≠ tail) continue ; if (n == null) if (n == null) { return EMPTY; if (CAS(&t → next,n,m)) ENQUEUE CAS(&Q → tail,t,n); ADV. TAIL break; } else { } else { if (CAS(&Q → head,h,n)) DEQUEUE CAS(&Q → tail,t,n); ADV. TAIL return n → val; } } } } } CAS(&Q → tail,t,n); ADV. TAIL }
The semantics of actions precondition postcondition [[P ↝ Q]] ≝ {(s 1 , s 2 ) | ∃ I ,s,s ′ ,s ctx . s 1 =s ⊎ s ctx ∧ [[P]] I (s) ∧ s 2 =s ′ ⊎ s ctx ∧ [[Q]] I (s ′ ) } R & G are sets of such actions: [[{a 1 ,...a n }]] ≝ ([[a 1 ]] ∪ ... ∪ [[a n ]])*
The action inference problem Given cmd, R, p: find G, q s.t. R, G ⊢ RGSep {p} cmd {q}. Preferably, the strongest G and q Top-level programs: R = ∅ and p = true. Libraries; the most general client: init(); ‖ ? while (?) { if (?) enqueue(?) else if (?) dequeue() else length() }
Experiments No join With lossless join Algorithm Iter Actions Time (s) Iter Actions Time (s) Treiber stack 4 5 0.1 4 2 0.1 M&S two-lock queue 5 26 0.3 5 12 0.3 M&S non-blocking queue 5 10 1.7 5 6 1.5 DGLM non-blocking queue 5 12 2.2 5 8 2.0 Lock-coupling list 4 21 1.0 3 10 0.8 Optimistic list 5 30 109.1 3 10 52.3 Lazy list 5 48 60.0 4 13 26.2 Vechev’s CAS list 3 9 24.7 3 5 8.8 Vechev’s DCAS list 2 6 0.3 3 4 0.3 Run action inference, finding data structure invariants & proving memory safety. Iter: number of iterations for finding the rely-guarantee specs of each thread. Actions: number of actions inferred.
Linearizability Automatically proving linearizability [Vafeiadis, CAV’09]
Linearizability & forward simulation Linearizability: The implementation (of every method) is a refinement of an atomic specification. Standard proof technique: forward simulation Abstract (spec) S abs S’ abs Concrete (impl) S conc S’ conc
Linearization points The implementation is a refinement of an atomic specification. abstract execution concrete execution linearization point (LP)
Linearization point of enqueue enqueue(v) { m = new Node(); m → val = v; m → next = null; while (true) { t = Q → tail; n = tail → next; if (Q → tail ≠ tail) continue ; if (n == null) { Lin. Point if (CAS(&t → next,n,m)) break; (provided CAS succeeds) } else { CAS(&Q → tail,t,n); } } CAS(&Q → tail,t,n); }
Proof search for the LP ? For each execution path of each method, choose a candidate LP Check whether it is a valid LP Does this work ?
Proof search for the LP ? For each execution path of each method, choose a candidate LP Check whether it is a valid LP Does this work ? Not quite. 1. LPs can be conditional 2. LPs can be in the code of another thread
LP of dequeue, when it returns EMPTY dequeue () { while (true) { h = Q → head; t = Q → tail; n = h → next; LP provided if (Q → tail ≠ t) this test fails, and continue ; the h==t test succeeds if (h == t) { the n==null test succeeds if (n == null) return EMPTY; CAS(&Q → tail,t,n); } else { if (CAS(&Q → head,h,n)) Condition: return n → val; } ¬prophecy(Q → tail ≠ t) } ∧ h == t } ∧ n == null
Key observation Method executions that logically modify the state have a simple LP. Method executions that do not logically modify the state often have a complex LP. So: Treat these two cases differently. Search for LPs of executions that logically modify the state; Do a non-constructive proof for executions that do not logically modify the state.
Recommend
More recommend