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
…