Transcript Part 1

7/26/2016
Verification of Concurrent Programs
Part 1: Proof systems for concurrent programs
Outline
 Recap of Hoare proofs
 The Owicki-Gries method
 Modular Proofs: Rely-Guarantee proofs
 Atomicity: Lipton’s Reduction
 Proving a mutual exclusion protocol
 Concurrency on “real” machines
Recap of Hoare proofs
Hoare logic
 Presented in Floyd/Hoare in the late 60s
 Similar ideas in a 1949 paper by Turing
 Main idea: Build program proofs following the syntactic
structure, i.e., proof rules for control-flow structures
{ φ } C {ψ}
If execution of C begins in a state satisfying φ, on termination, the
final state satisfies ψ.
A sequential language: IMP
P ::=
skip
x := expr
P1 ; P2
if (cond) then P1 else P2
while (cond) P1
Here, cond is a Boolean expression over program variables
and expr is any expression over program variables.
Floyd/Hoare proof rules: Atomic
statements
{φ} skip {φ}
{φ[x/e]} x := e {φ}
Example:
{y+3>0}x = 3 {x+y>0}
NOOP
ASSIGN
Floyd/Hoare proof rules: If
{φ⋀C}P{ψ}
{φ ⋀ ┐ C} P’ {ψ}
{φ} if (C) P else P’ {ψ}
Examples:
{ x != 0 ⋀ x > 0 } skip { x > 0 } { x != 0 ⋀ x ≤ 0 } x = -1 * x { x > 0 }
{ x != 0 } if (x > 0) skip; else x = -1 * x { x > 0 }
{ x % 2 == 0 } skip { x % 2 == 0 }
{ x % 2 != 1 } x := x + 1 { x % 2 == 0 }
{ true } if (x % 2 == 0) skip; else x := x + 1 { x % 2 == 0 }
IF
Floyd/Hoare proof rules: While
{φ⋀C}P{φ}
{φ} while (C) P {φ ⋀ ¬ C}
Examples:
{ x ≥ 0 ⋀ x > 0 } x := x - 1{ x ≥ 0 }
{ x ≥ 0 } while (x > 0) x = x – 1; { x = 0 }
{ x ≥ 0 ⋀ x = y ⋀ x > 0 } x--; y--;
{x≥0⋀x=y}
{x ≥ 0 ⋀ x = y} while (x>0) x--;y--; { x ≥ 0 ⋀ x = y ⋀ x ≤ 0}
WHILE
Floyd/Hoare proof rules: Rest
φ’  φ
{φ}P{ψ}
ψ ψ’
{φ’} P {ψ’}
{ φ } P { ψ’ }
{ψ’ } Q { ψ }
{ φ } P; Q { ψ }
Examples:
{ x≥0⋀ x = y } x--; y--; { x ≥ 0 ⋀ x = y ⋀ x ≤ 0}
{ x ≥ 0 ⋀ x = y } while (x > 0) x--; y--; { y = 0 }
Floyd/Hoare proof: example
P:
if (bal > 1000)
cs := cs + 100
else
cs := cs + 0
Proof goal:
{cs = 0} P { cs ≥ 100  bal > 1000 }
{cs +100 ≥ 100  bal > 1000} cs := cs +100 {cs ≥100  bal > 1000}
{ cs = 0 ⋀ bal > 1000 } cs :=
cs +100 { cs ≥ 100  bal > 1000 }
{ cs ≥ 100  bal > 1000} cs := cs + 0 { cs ≥ 100  bal > 1000 }
{ cs = 0 ⋀ bal ≤ 1000 } cs := 0 { cs ≥ 100  bal > 1000 }
Floyd/Hoare proof: example
P:
if (bal > 1000)
cs := cs + 100
else
cs := cs + 0
Proof goal:
{cs = 0} P { cs ≥ 100  bal > 1000 }
{ cs = 0 ⋀ bal > 1000 } cs := 100 { cs ≥ 100  bal > 1000 }
{ cs = 0 ⋀ bal ≤ 1000 } cs := 0 { cs ≥ 100  bal > 1000 }
{ cs = 0 } if (bal>1000) cs:=cs+100 else cs:=cs+0 { cs ≥ 100  bal > 1000 }
Floyd/Hoare proofs
 Floyd/Hoare proofs are sound
 If you prove { φ } P { ψ }, every terminating execution of P
starting from a state satisfying φ ends in a state satisfying ψ.
 Floyd/Hoare proofs are relatively complete
 For every program P such that every terminating execution
of P starting from a state satisfying φ ends in a state satisfying
ψ, we can prove { φ } P { ψ }
 provided the underlying logic is complete
Owicki-Gries proofs
Owicki-Gries method: Introduction
 First extension of Floyd/Hoare proofs to shared-memory
concurrency
 Presented by Owicki and Gries in mid 70s
 New rules for concurrency constructs
 Simple idea: Can compose proofs as long as they don’t
interfere with each other
A concurrent language: IMP
P ::=
skip
x := expr
P1 ; P2
if (cond) then P1 else P2
while (cond) P1
P1 ║ P2
// parallel composition
[ P1 ]
// atomic
await cond then P1
// conditional atomic
Simple things first…
{φ}P{ψ}
{φ} [P] {ψ}
{ φ ⋀ C } [P] { ψ }
{φ } await (C) then P { ψ }
ATOMIC
COND_ATOMIC
 Executing something atomically is the same as executing it
in a sequential context
 Conditional atomic waits for its condition to be true and
then executes in a sequential context
Simple things first…
 Compare the conditional atomic rule to the if rule
{ φ ⋀ C } [P] { ψ }
{φ } await (C) then P { ψ }
{φ⋀C} P{ψ}
COND_ATOMIC
{ φ ⋀ ┐ C } P’ { ψ }
{φ } await (C) then P { ψ }
Intuitively, a conditional atomic is an if that waits for its condition.
IF
Example: A lock
lock(l):
await lock == 0 then
lock = tid
unlock(l):
await lock == tid then
lock = 0
 Correctness:
 If lock(l) succeeds,
current thread holds the
lock
 If unlock(l) succeeds, no
one holds the lock
Example: Verifying a lock
Lock:
{ tid = tid } lock = tid { lock = tid }
{ true ⋀ lock = 0 } lock = tid { lock = tid }
{ true } await lock = 0 then lock = tid { lock = tid }
Unlock:
{ 0 = 0 } lock = 0 { lock = 0 }
{ lock = tid ⋀ 0 = 0 } lock = 0 { lock = 0 }
{ true } await lock = tid then lock = 0 { lock = 0 }
Towards a parallel compostion rule
 Attempt 1:
t1():
t2():
sum = 0
sum = sum + x
sum = sum + y
sum2 = 0
sum2 = sum2 + x*x
sum2 = sum2 + y*y
 Prove using standard Floyd/Hoare rules
{ true } t1() { sum = x + y }
{ true } t2() { sum = x*x + y*y }
 Now, we have this:
{ true } t1() ║ t2() { sum = x + y ⋀ sum2 = x*x + y*y }
Towards a parallel compostion rule
 Attempt 1:
{ φ1 } P1 { ψ1 }
{ φ2 } P2 { ψ2 }
{ φ1 ⋀ φ2 } P1 ║ P2 { ψ1 ⋀ ψ2 }
 What’s wrong with this?
 If P1 and P2 work on the same variables, we have a
problem
 For (x := x + 1)║(x := x + 1), we have:
 { x = 0 } x := x + 1 { x = 1 }
 { x = 0 } x := x + 1 { x = 1 }
 But, not { x = 0 } (x := x + 1)║(x := x + 1) { x = 1 }
 This rule is unsound
Towards a parallel composition rule
 Attempt 2:
{ φ1 } P1 { ψ1 }
{ φ2 } P2 { ψ2 }
{ φ1 ⋀ φ2 } P1 ║ P2 { ψ1 ⋀ ψ2 }
given than P1 and P2 don’t read and write the same variables
 What’s wrong with this?
 No way to prove some program
 The rule is incomplete
Towards a parallel composition rule
if (bal > 1000)
cs := cs + 100
else
cs := cs + 0
bal := bal + 5000
Proof goal:
{ cs = 0 ⋀ bal = B } t1 ║ t2 { cs ≥ 100  bal ≥ 1000 ⋀ bal = B + 5000}
 We have already proved that
{ cs = 0 } t1 { cs ≥ 100  bal > 1000 }
Easy to prove that { bal = B } t2 { bal = B + 5000 }
 Now, the key point is that bal := bal + 5000 does not interfere
with the proof of { cs = 0 } t1 { cs ≥ 100  bal > 1000 }
The Owicki-Gries rule
 Main idea: We can compose proofs as long as they don’t
interfere with each other
 What is interference? The read/written variable based definition
is too strong
 Owicki and Gries: Two proofs don’t interfere if statements in
each don’t affect the critical predicates in the other
 In a proof {φ} P {ψ}, a critical predicate is either ψ or a
precondition φ_i of { φ_i } s_i { ψ _i }

{ φ1 } P1 { ψ1 }
{ φ2 } P2 { ψ2 }
{ φ1 ⋀ φ2 } P1 ║ P2 { ψ1 ⋀ ψ2 }
if for every critical formula φ from one proof and assignment or
atomic rule from the other proof { φ_i’} s_i’ {ψ_i’}, we have { φ ⋀
φ_i’ } s_i’ { φ }
Owicki-Gries rule: Example
if (bal > 1000)
cs := cs + 100
else
cs := cs + 0
bal := bal + 5000
 In our proof of { cs = 0 } t1 { cs ≥ 100  bal > 1000 }, assignment
rules were
{cs +100 ≥ 100 bal>1000} cs := cs+100 {cs ≥100  bal > 1000}
{ cs ≥ 100  bal > 1000} cs := cs + 0 { cs ≥ 100  bal > 1000 }
 For a proof of { bal = B } bal := bal + 5000 { bal = B + 5000 },
the assignement rules are
 { bal + 5000 = B + 5000 } bal := bal + 5000 { bal = B + 5000 }
OG rule: Example and remarks
 In the previous proof, what would happen if bal := bal
+ 5000 is replaced by bal := bal – 5000
 The big questions:
 Is the OG rule sound? Can we prove only true things?
 Yes
 Is it (relatively) complete? Can we prove all true things?
 No!!!
OG incompleteness: Example
 Prove {x = 0} x := x + 1 ║ x := x + 1 { x = 2 }
 Hard to end up with the value 2 for x
 For each statement, we don’t know what the post condition
should be: we have no idea if the other statement is
executed.
 Solution: Use auxiliary variables to encode such
information
Auxiliary variables
 Replace the program with a similar program with
additional variables
[ x := x + 1; d1 = 1]
[ x := x + 1; d2 = 1]
 We’ve encoded control-flow into data
 Now, we can talk about whether another statement has
been executed
{ d1 = 0 ⋀ (d2 = 0 ⋀ x = 0) ⋁ (d2 = 1 ⋀ x = 1) }
[ x := x + 1; d1 = 1 ]
{ d1 = 1 ⋀ (d2 = 0 ⋀ x = 1) ⋁ (d2 = 1 ⋀ x = 2) }
Auxiliary variables
{ d1 = 0 ⋀ (d2 = 0 ⋀ x = 0) ⋁ (d2 = 1 ⋀ x = 1) }
[ x := x + 1; d1 = 1 ]
{ d1 = 1 ⋀ (d2 = 0 ⋀ x = 1) ⋁ (d2 = 1 ⋀ x = 2) }
{ d2 = 0 ⋀ (d1 = 0 ⋀ x = 0) ⋁ (d1 = 1 ⋀ x = 1) }
[ x := x + 1; d2 = 1 ]
{ d2 = 1 ⋀ (d1 = 0 ⋀ x = 1) ⋁ (d1 = 1 ⋀ x = 2) }
{ d1 = 0 ⋀ d2 = 0 ⋀ x = 0 }
[ x := x + 1; d1 = 1 ] ║ [ x := x + 1; d2 = 1 ]
{x=2}
Auxiliary variables elimination rule
{φ}P{ψ}
{ φ } erase(P, V) { ψ }
 You can erase a set of variables that do not appear in ψ
and do not affect other variables in P.
 Main caveat of OG method: Coming up with auxiliary
variables is hard and tedious and proofs blow up
Owicki-Gries method: Summary
 Very simple rules for atomics
 Complicated rule for Non-interference
 Auxiliary variables for completeness
 In general, if you compose proofs containing m and n
statements each, you are doing m*n additional checks
Modular Proofs: Rely-Guarantee
Rely-Guarantee proofs
 Owicki-Gries proofs can become complex
 Auxiliary variables
 Quadratic number of non-interference checks
 Generally, not composable
 Rely-Guarantee overcomes some of these caveats
 Main idea: Instead of trying to write interference-free proofs,
why not explicitly account for the allowed interference
 No additional interference checks required
OG problems
if (bal > 1000)
cs := cs + 100
else
cs := cs + 0
bal
bal
bal
bal
bal
…
:=
:=
:=
:=
:=
bal
bal
bal
1.1
bal
+
+
+
*
+
5000
1000
5400
bal
30
 OG proof gets more and more complex
 The number of non-interference checks keeps growing
 Intuitively, all the statements in thread 2 are similar
 The non-interference is because of the same reason
Rely-Guarantee proofs
 Two state predicates
 Relating initial and final states after a statement or a
sequence of statements
 For example, x := x + 1 will be written as x’ = x + 1
C ⊨ ( φ , R, G, ψ )
If execution of C begins in a state satisfying φ and the other
threads only execute statements that satisfy R, then
(a)on termination, the final state satisfies ψ.
(b)C only executes statements that satisfy G
 Informally, C starting fromφ, relying on R and
guaranteeing G, terminates with a state satisfying ψ
 We need φ and ψ to be stable w.r.t R
Rely-Guarantee proofs
φ
R
ψ
G
C ⊨ ( φ , R, G, ψ )
Rely-Guarantee examples
 Independent statements: the rule relies on no other
thread changing the value of x
x = x + 1 ⊨ (x = 0, x’ = x, x’ = x + 1, x = 1)
 Invariant preserving: the rule relies on no other thread
decreasing the value of x
if (bal > 1000) cs := 100 ⊨
1000)
(true, bal’ > bal ⋀ cs = cs’, true, cs = 100 bal >
Rely-Guarantee rules: Parallel
composition
 Parallel composition
 Rely of one thread becomes guarantee for the other
 And vice versa,
P1 ⊨ (φ1, R1, G1, ψ1)
G1  R2
P2 ⊨ (φ2, R2, G2, ψ2)
G2  R1
P1║P2 ⊨ (φ1⋀ φ2, R1 ⋀ R2,G1 ⋁ G2, ψ1 ⋀ ψ2)
Rely-Guarantee: Atomic actions
For atomic actions, just copy things from Hoare triples
{φ}C{ψ}
C ⊨ (φ, Pres(p) ⋀ Pres(q), φ ⋀ ψ’, ψ)
The environment must preserve the pre- and post-condition
The guarantee is the pre and post- condition of the current
statement
Rely-Guarantee rules: Sequential
composition
P1 ⊨ (φ1, R, G, ψ1)
P2 ⊨ (φ2, R, G, ψ2)
ψ1  φ1
P1;P2 ⊨ (φ1, R, G, φ2)
 No surprises here---if rely and guarantee predicates are
the same, sequential composition is as in Hoare logic
Rely-Guarantee rules: Strengthening
φ’  φ
R’  R
G  G’
ψ ψ’
C ⊨ (φ, R, G, ψ)
C ⊨ (φ’, R’, G’, ψ’)
The Balance example
Prove:
(bal:=bal+5000)║(if (bal>1000) cs:=100 else cs:=0)
⊨ (true, bal’=bal ⋀ cs’=cs, true, cs = 100  bal > 1000)
bal := bal + 5000 ⊨ (true, bal’ = bal, bal’ ≥ bal, true)
T2 ⊨ (true, bal’ ≥ bal ⋀ cs’ = cs, true, cs = 100  bal > 1000)
Rely-Guarantee: Limitations
 Rely-Guarantee reasoning forgets the order and number
of actions in the environment
 Try proving
(x:=x+1; x:=x+1; x:=x+1)║(x:=x+1; x:=x+1; x:=x+1) ⊨ (x =
0, x’ = x, true, x = 6)
 No way other than introducing auxiliary variables
Simplifying programs: Lipton’s
Reduction
Reduction based methods
 Early work by Lipton in the 70s
 Extended to a more general technique by Elmas, Qadeer,
Tasiran, Sezgin and others starting 2009
 A program simplification technique
 Rewrite a given program with a simpler program
 What is simpler?
 Usually, larger atomic sections
Reduction: The simplest example
 Common programmer intuition: locks make everything
inside atomic.
 Why? And if correct, how do we prove it
 lock(l); x = x + 1; unlock(l)
lock(l)
Is equivalent to
lock(l)
 Keep moving lock(l) to the right till it is next to x:=x + 1
 Similarly, keep moving unlock(l) to the left
Reduction: The simplest example
 We have proved that the three statements can be
considered atomic
 Lock acquire moved to the right and lock release moved
to the left
 Cannot do it the other way. Why?
 Main idea: classify statements into those that move right
and those that move left
Lipton’s reduction
 A statement is a right mover if for every β from another
thread, we have that

; β  β;
 Secondary condition: If ; β fails an assertion, so should β;
 Similar rules of left movers
s1
s1
;β
;β
error
s2
s1
s1
s1
β;
β;
or
β;
error
s2
error
Mover types examples
 Usually, lock acquisition statements are right movers and
lock release statements are left movers
 Unrelated statements move across each other
 assume (bal > 1000) moves right across (bal := bal +
5000)
Reduction:
 Given a sequence of actions 1; 2; … ; n; β; γ1; … ; γn:
 If i’s are right movers
 And γi’s are left movers
 We can replace the sequence by
[ 1… nβ γ1…γn]
GOAL: Make larger and larger atomic sections till the program
becomes small enough to be reasoned about
Additional rules: Reduce if
 If you prove a guard and the corresponding branch are
atomic, then the whole if construct is atomic
if
(*)(bal > 1000)
[ if
Left if (bal
[ assume
cs
:= cs
(bal
+ 100
> 1000)
> 1000)
else cscs
100 ]
:=:=
cscs
+ +
100
cs := cs + 0
else
Leftelse
[
csassume
:= cs (bal
+ 0 ≤ 1000)
cs := cs + 0 ]
]
bal := bal + 5000
 Simplified the program
 Proving this using Owicki-Gries is much simpler
 Way fewer non-interference checks
A more complex example
Right
Both
Left
lock(x)
l1 := x
l1 := l1 + 1
x := l1
unlock(x)
lock(x) Right
l2 := x
l2 := l2 + 1 Both
x := l2
unlock(x) Left
 The locks and unlocks are clearly right and left movers
 How about l1 = l1 + 1 and l2 = l2 + 1?
 They are both left and right movers
 What about the rest? Mover checks fail
 We are missing some global information—e.g. when l1 :=
x is executed thread 1 holds the lock
Auxiliary Assertions
lock(x); o = 1
l1 := x; assert(o = 1)
l1 := l1 + 1; assert(o = 1)
x := l1; assert(o = 1)
unlock(x); o = 0
o = 2; lock(x)
assert(o = 2); l2 := x
assert(o = 2); l2 := l2 + 1
assert(o = 2); x := l2
o = 0; unlock(x)
 Now, mover checks magically succeed
 Why? Assertions fail in both cases
 Added global information into each action
 Added history information into static actions
Auxiliary Assertions
[
lock(x); o = 1
l1 := x; assert(o = 1)
l1 := l1 + 1; assert(o = 1)
x := l1; assert(o = 1)
unlock(x); o = 0
]
[
o = 2; lock(x)
assert(o = 2); l2 := x
assert(o = 2); l2 := l2 + 1
assert(o = 2); x := l2
o = 0; unlock(x)
]
 Apply reduction rule: The threads execute atomically
 This is not the original program—we have additional
assertions
 Discharge them using sequential reasoning
Compare to Owicki-Gries
 We aren’t done yet; we just simplified the program
 Now, we apply Owicki-Gries or any other method and
finish proving { x = 0 } t1║t2 { x = 2 }. Need 4 noninterference checks
 Compare to the original proof, before reduction
{ t2loc = 0  x = 0, t2loc = 5  x = 1 }
lock() ; owner = 1
{ t2loc = 0  x = 0, t2loc = 5  x = 1, owner = 1 }
30 non-interference
t := x
checks
{ t2loc = 0  x = 0, t2loc = 5  x = 1, owner = 1, t = x }
+
t := t + 1
Auxiliary
variables
{ t2loc
= 0  x = 0, t2loc = 5  x = 1, owner = 1, t = x + 1 }
x := t
{ t2loc = 0  x = 1, t2loc = 5  x = 2, owner = 1}
unlock(); t1loc = 5; owner = 0
{ t2loc = 0  x = 1, t2loc = 5  x = 2, t1lock=5 }
Reduction + Abstraction
 Sometimes, adding more behaviours to an action can
help
l1 = *
x
s1 := CAS(x, l1, l1 + 1)
l2 = *
x
s2 := CAS(x, l2, l2 + 1)
 Does l1 := x move right?
 No, it conflicts with the CAS from t2
 We abstract the statement by adding more possible
behaviours
 Now, we have made the mover checks pass by relaxing
the semantics of the program
 Is it worth doing this? Sometimes
Reduction + Abstraction
…
x := x + 5
…
…
x := 10 * x
…
 Mover checks fail both ways
 However, if the final proof only requires to check that x > 0,
we replace both statements with
[
if (x > 0)
x = *; assume (x > 0); // Pick a positive x
else
x = *;
// Pick any x
]
 Mover checks pass now
Reduction-based methods: Summary
 Program simplification technique
 Not a full pre/post condition proof
 Merge actions to build larger and larger atomic sections
 Use abstraction wherever necessary
 Caveats: Hard to write the auxiliary assertions
Don’t believe the truth
Concurrency on “real” machines
Demo
Concurrency in practice
 Rule 1: Compilers are evil
 Use volatile wherever necessary
 Rule 2: Processors are evil
 Don’t write your own locks/barriers
 Rule 3: Catch-fire semantics
 There are no benign data-races
Catch-fire semantics
 C11/C++11 semantics:
 Compiler ensures sequential consistency as long as there are
no data-races
 If there is a data-race, all bets are off
 Not only can you read any value from the variable, the
program can do anything from that point on
 With all these problem, concurrency verification seems
futile
 How about incomplete techniques instead?
 Bug-detection
Next week…
 Concurrency verification in practice
 Race detection
 Sequentialization
 Context Bounding
 Correctness conditions
 …