Separation Logic and Concurrency Verification Xinyu Feng (冯新宇) University of Science and Technology of China
Why concurrency verification Concurrent programs show in many systems Multi-task support in OS kernels Handling interrupts from external devices … Will be more common Multi-core processors Intellectually interesting Correctness/incorrectness are not obvious
Shared-State Concurrency Model B Memory
Execution Model: Simple Examples [ 100 ] := 3 [ 100 ] := 5 [ 100 ] := 3 [ 101 ] := 5 100 101 3 5 100 3/5 Don’t know which one is written first, but order doesn’t matter. Order may affect the result if threads share resources.
Execution Model: Simple Examples It is difficult to discuss resource sharing with memory pointer aliasing. T1: T2: [ x ] := 3 [ y ] := 5 x x y y 3/5 3 5
Execution Model: Simple Examples C1n C21; C22; C2n T1 T2 Non-deterministic interleaving may produce exponential num. of execution traces; Different traces may lead to different results (depends on the resource sharing).
Challenges to reason about concurrent programs Sharing of resources makes the result dependent on the ordering of execution Non-deterministic interleaving produces exponential num. of possible ordering Memory pointer aliasing makes it difficult to tell how resources are shared
Outline of this Lecture Separation Logic Concurrent Separation Logic (CSL) Extension of CSL to handle Interrupts
Separation Logic A Hoare-style program logic: { p } C { q } [Ishtiaq&O’Hearn’01,Reynolds’02] A Hoare-style program logic: { p } C { q } What’s new here is the assertion language.
Separation Logic Assertions emp empty heap l n n l p q p q p q
Separation Logic Assertions emp empty heap l n n l p q p q l_ defined as n. l n ln defined as (ln) true n l
Properties pemp p pq qp pp p ptrue p pp p (l_)(l_) false p ptrue ptrue p
Assertions Model Ownership [l] := m; {(l m)} {emp} [l] := m; {???} Ownership cannot be duplicated: (l_) (l_)(l_)
Strength of Separation {(xn) (yn)} [x] := m; {(xm) (yn)} {(xn) (yn)} [x] := m; {(xm) (yn)} what if x=y ?
A Frame Rule for Modularity C p q r { p } C { q } { p r } C { q r } r Another example showing the strength of separation!
Specification of a List top … List (top) (top = null) emp next. top (_, next) List ( next ).
Example: getNode getNode() if (top <> null){ { List (top) } r1 = top; r2 = top.next; top = r2; } else {r1 = null } { List (top) } { List (top) top null } { next. top (_, next) List ( next ) } { r1 = top next. top (_, next) List ( next ) r2 = next } { r1 (_, _) List ( r2 ) } { r1 (_, _) List ( top ) } { List ( top ) * (top = r1 = null emp r1 (_, _) ) }
Reading Materials See the miniCourse webpage of John Reynolds: http://www.cs.cmu.edu/~jcr/
Outline of This Lecture Separation Logic Concurrent Separation Logic (CSL) Extension of CSL to handle Interrupts
Separation Logic for Concurrency [ 100 ] := 3 [ 100 ] := 5 [ 100 ] := 3 [ 101 ] := 5 100 101 3 5 100 3/5 Separation is an effective way to control interference.
Concurrent Separation Logic (CSL) [O’Hearn 2004] Key ideas: Threads can only access disjoint resources at the same time. p q {p} C1 {p'} {q} C2 {q'} p q Transfer is logical: no memory copying {p q} C1 || C2 {p' q'} C1 and C2 are verified as sequential programs But how to allow threads to share resources?
Locks and Critical Regions Lock-based critical regions (CR): l.acq(); … l.rel() Invariants about memory protected by locks: = {l1 r1, …, ln rn} r1, …, rn disjoint l1 ln r1 rn Note: different locks protect disjoint resources.
Concurrent Separation Logic (CSL) ┝ {p} C1 {p'} ┝ {q} C2 {q'} ┝ {p q} C1 || C2 {p' q'} l1 ln Memory Model: p q r1 rn
CSL (cont’d) p1 r p2 p2 p1 r p2 p1’ p1’ r p2 r p2 p2 Each thread can freely access to its local resource.
CSL (cont’d) p1 r p2 p2 p1 r p2 p1 r p2 l.acq(); C l.rel(); Suppose (l) = r
CSL (cont’d) p1 r p2 p2 p1 r p2 p1 r p2 p1’ r p2 l.acq(); l.rel()
CSL (cont’d) p1 r p2 p2 p1 r p2 p1 r p2 p1’ r p2 l.acq(); p1 r p2 C p1’ r p2 l.rel(); p1’ r p2
CSL (cont’d) Reasoning about lock acquire/release: ┝ {emp} l.acq() { (l) } Acquiring the lock means getting the ownership of the lock-protected resource. (l) is transferred from shared to local.
CSL (cont’d) The rule does not support reentrant locks: ┝ {emp} l.acq() { (l) } {emp} l.acq() {(l) } {(l) (l) }
CSL (cont’d) Reasoning about lock acquire/release: ┝ { (l) } l.rel() { emp } Before releasing the lock, the corresponding resource needs to be well-formed. Releasing the lock means losing the ownership of the resource (which is transferred from local to shared).
CSL (cont’d) Reasoning about lock acquire/release: ┝ {emp} l.acq() { (l) } ┝ { (l) } l.rel() { emp } We call this ownership-transfer axiomatic semantics of locks
Example: List = { l List(top) } getNode(): l.acq(); -{emp} … l.rel(); -{emp} top … r -{emp * List(top)}; -{r(_,_) * List(top)} -{r(_,_) } Sequential verification
Outline of This Lecture Separation Logic Concurrent Separation Logic (CSL) Extension of CSL to handle Interrupts
Layering of OS Code B: concurrent code with explicit interrupts How to certify ???
Example: Spinlocks (uniprocessor) 0/1 acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; // disable Interrupt timer_0: ... switch iret // lock is taken by others … [l] :=1; …
Example: Spinlocks (uniprocessor) 0/1 acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; // lock available // acquire lock // enable Interrupt
Concurrency with Interrupts: Challenges IR0 IR1 Asymmetric preemption between handlers and non-handler code Intertwining between threads and handlers Asymmetric synchronization: cli/sti are different from locks
Study the problem in 3 steps Interrupts with Sequential Programs Interrupts with Multi-threaded Programs Adding block/unblock primitives
AIM – I : The Machine (program) P ::=(C,S,pc) (data heap) H f1: I1 pc addu … cli sti iret … j f 1 2 … f2: I2 r1 r2 r3 … rn ih: ISR (register file) R ie … (code heap) C (state) S ::=(H,R,ie) ::={f I}* (program) P ::=(C,S,pc)
Example: Teeter-Totter left right 50 50 timer: if([left] == 0){ print(“right wins!”); iret; } [left] := [left]-1; [right] := [right]+1; while([right] != 0){ cli; [right] := [right]-1; [left] := [left]+1; sti; } print(“left wins!”); Which side wins depends on how frequently the timer interrupt comes.
AIM – I : The Memory Model INV Wellformedness of A; Protocol for sharing B A Memory sti … iret cli … Non-handler Handler
AIM – I : cli/sti B cli B A critical region B sti B A B ie = 1 ie = 0 INV B INV cli B A critical region B Explain memory models: add animation sti B A INV B INV ie = 1 ie = 0
Ownership Transfer Semantics for cli/sti ┝ {ie=1} cli {ie=0 INV } ┝ {ie=0 INV} sti {ie=1}
AIM – I : handler B A irq B A iret B A B A ie = 1 ie = 0 INV INV INV Explain memory models: add animation iret B A INV B A INV ie = 1 ie = 0
Example: Teeter-Totter left right INV: m, n. (left m) (right n) (m+n = 100) m n while(!done){ -{ie=1} cli; -{ie=0 INV} [right] := [right]-1; [left] := [left]+1; done := ([right] == 0); sti; } timer: -{INV} if([left] == 0){ print(“right wins!”); iret; } [left] := [left]-1; [right] := [right]+1; INV is broken in the middle
Step 2: Interrupts with Multi-threaded Programs
AIM – II : The Machine (program) P ::=(C,S,Q,pc) … (ready. queue) Q (data heap) H f1: I1 1 2 … f2: I2 pc cli sti … switch j f r1 r2 r3 … rn ih: ISR (register file) R ie … (code heap) C (state) S ::=(H,R,ie) ::={f I}* (program) P ::=(C,S,Q,pc)
AIM – II : switch A primitive implemented at layer C Interrupt must be disabled before executing switch Operational semantics: Put the current thread into ready Q Resume a thread in Q (non-deterministic op) Explain where does switch come from
Example: spin locks acquire (l): cli; while([l] == 0){ sti; } return; switch release (l): cli; [l] := 1; sti; return;
Examples Layer A: timer_0: ... switch iret yield: cli switch sti ret ie = 0: non-preemptive threads ie = 1 & timer_1: non-preemptive threads ie = 1 & timer_0: preemptive threads timer_1: ... iret
T1/T2: threads’ private memory AIM – II : Memory Model INV B A A T1/T2: threads’ private memory C T1 T2 INV1 C: Shared by threads A INV
cli/sti t1: C t1: A T1 T2 T1 T2 C A critical region t1: C' t1: A' T1' INV T1 T2 INV1 T1 T2 t1: cli C A INV1 INV critical region t1: C' t1: A' INV T1' T2 INV1 C' A' INV T1' T2 INV1 t1: sti ie = 1 ie = 0
Example: Spinlocks (uniprocessor) 0/1 R -{ emp } -{ R } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; Lock l protects the resource R release (l): cli; [l] := 1; sti; return; Don’t talk about the details of abstract specifications
Example: Spinlocks (uniprocessor) 0/1 R -{ emp } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; INV1: b. (l b) (b=0emp b=1R) Lock and resource R are available Lock is taken; no R for share. A INV C T1 T2 INV1 The lock and the resource R are shared between threads (part of block C). -{ R }
Example: Spinlocks (uniprocessor) 0/1 R INV1: b. (l b) (b=0emp b=1R) -{ emp } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; -{ emp INV1 INV}
Example: Spinlocks (uniprocessor) 0/1 R INV1: b. (l b) (b=0emp b=1R) -{ emp } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; -{ emp INV1 INV} -{ emp INV1 INV} -{ emp }
Example: Spinlocks (uniprocessor) 0/1 R INV1: b. (l b) (b=0emp b=1R) -{ emp } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; -{ emp INV1 INV} -{ emp INV1 INV} maybe different state -{ emp } -{ emp INV1 INV} -{ (l 1) R INV} emp INV1
Example: Spinlocks (uniprocessor) 0/1 R INV1: b. (l b) (b=0emp b=1R) -{ emp } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; -{ emp INV1 INV} -{ emp INV1 INV} -{ emp } -{ emp INV1 INV} -{ (l 1) R INV} emp INV1 -{ (l 0) R INV}
Example: Spinlocks (uniprocessor) 0/1 R INV1: b. (l b) (b=0emp b=1R) -{ emp } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; -{ emp INV1 INV } -{ emp INV1 INV } -{ emp } -{ emp INV1 INV } Verification is the same as sequential reasoning -{ (l 1) R INV } -{ (l 0) R INV } INV1 -{ R } -{ R } The verification is sequential!
Example: Spinlocks (uniprocessor) 0/1 R INV1: b. (l b) (b=0emp b=1R) -{ emp } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; -{ emp INV1 INV } timer_0: ... switch iret … [l] :=1; … -{ emp INV1 INV } -{ emp } -{ emp INV1 INV } Verification is the same as sequential reasoning -{ (l 1) R INV } -{ (l 0) R INV } INV1 -{ R } -{ R } The verification is sequential!
switch Thread t1 Thread t2 t2: C t2: A T1 T2 T1 T2 untouched t1: C INV T1 T2 INV1 T1 T2 untouched t1: switch t1: C t1: A INV1 * INV preserved INV1 INV … sti … cli … switch t1: C' t1: A' INV T1 T2' INV1 t2: C' t2: A' INV T1 T2' INV1 t2: switch Thread t1 Thread t2 ie = 0
Local Semantics for switch ┝ {ie = 0 INV1 INV } switch {ie = 0 INV1 INV }
Example: Spinlocks (uniprocessor) 0/1 R INV1: b. (l b) (b=0emp b=1R) -{ emp } acquire (l): cli; while([l] == 0){ sti; } [l] := 0; return; -{ emp INV1 INV } -{ emp INV1 INV } switch INV1 INV preserved -{ emp } -{ emp INV1 INV } Verification is the same as sequential reasoning -{ (l 1) R INV } -{ (l 0) R INV } INV1 -{ R } -{ R } The verification is sequential!
Step 3: Adding block/unblock primitives
AIM – III : The Machine (program) P ::=(C,S,B,Q,pc) … (ready. queue) Q f1: f2: ih: r1 1 2 … r2 r3 rn (data heap) H (register file) R ie pc R … (ready. queue) Q w1 w2 (code heap) C (state) S ::=(H,R,ie) … block unblock … pc wn B (program) P ::=(C,S,B,Q,pc)
AIM – III : block and unblock block rs put current thread into the block queue B(rs) pick a thread from ready queue to execute unblock rs, rd move a thread from B(rs) to the ready queue put the thread id into rd, put 0 if B(rs) empty no context switching!
Examples: locks l 0/1 R B(l) = Ql acquire_0(l): cli; if ([l] == 0) block l; else [l] := 0; sti; return; acquire_1(l): cli; while ([l] == 0) block l; [l] := 0; sti; return; release_0(l): local x; cli; unblock l x; if (x == 0) [l] := 1; sti; return; release_1(l): local x; cli; unblock l x; [l] := 1; sti; return;
How to interpret block/unblock Threads block themselves to wait for resources. locks: wait for resources protected by locks condition variables: wait for resources over which the condition holds w1 R1 R2 Rn ::= {w0 R0, … , wn Rn} Ri can be emp ! w2 … wn B
How to interpret block/unblock INV T1 INV1 ! T1 ? T2 T2 block T1 T1 INV1 INV block unblock T1 INV INV1 T2 ! T2 INV INV1 T1 ! ! switch Thread 1 Thread 2
Local Semantics for block/unblock p (ie = 0) INV1 INV0 ┝ { p } block l {p (l)} p ie = 0 ┝ {p (l)} unblock l x {p (x 0 emp x = 0 (l))} Blocked thread is released: ownership of (l) is transferred No threads waiting and no ownership transfer
Examples: locks l 0/1 R B(l) = Ql (l) = R INV1: b, (l b) (b=0emp b=1R) -{ emp } acquire_0(l): cli; if ([l] == 0) block l; else [l] := 0; sti; return; -{ emp } -{ emp INV1 INV} -{ emp INV1 INV} -{ emp INV1 INV (l) } -{ emp INV1 INV R} -{ emp R} -{ R }
Examples: locks l 0/1 R B(l) = Ql (l) = R INV1: b, (l b) (b=0emp b=1R) -{ R } release_0(l): local x; cli; unblock l x; if (x == 0) [l] := 1; sti; return; -{ R } -{ R INV1 INV} -{(x=0 R x 0 emp) INV1 INV} -{ R INV1 INV0} -{ (l 0) R INV} -{ (l 1) R INV} -{ INV1 INV} -{ emp } -{ emp }
Examples: locks l 0/1 R B(l) = Ql (l) = R (l) = emp INV1: b, (l b) (b=0emp b=1R) -{ emp } -{ R } acquire_1(l): cli; while ([l] == 0) block l; [l] := 0; sti; return; release_1(l): local x; cli; unblock l x; [l] := 1; sti; return; -{ R } -{ emp }
Examples : Condition Variables wait()/notify() Hoare Style / Brinch-Hanson Style Mesa style Reading material: Feng et al. “Certifying Low-Level Programs with Hardware Interrupts and Preemptive Threads”.
Summary Separation logic Concurrent Separation Logic (CSL) Ownership transfer semantics to thread primitives and interrupt primitives Can be further extended
Thank you