Verifying concurrent, crash-safe systems with Perennial Tej Chajed , - - PowerPoint PPT Presentation

verifying concurrent crash safe systems with perennial
SMART_READER_LITE
LIVE PREVIEW

Verifying concurrent, crash-safe systems with Perennial Tej Chajed , - - PowerPoint PPT Presentation

Verifying concurrent, crash-safe systems with Perennial Tej Chajed , Joseph Tassarotti*, Frans Kaashoek, Nickolai Zeldovich MIT and *Boston College Many systems need concurrency and crash safety Examples: file systems, databases, and key-value


slide-1
SLIDE 1

Verifying concurrent, crash-safe systems with Perennial

Tej Chajed, Joseph Tassarotti*, Frans Kaashoek, Nickolai Zeldovich MIT and *Boston College

slide-2
SLIDE 2

2

Many systems need concurrency and crash safety

Examples: file systems, databases, and key-value stores Make strong guarantees about keeping your data safe Achieve high performance with concurrency

slide-3
SLIDE 3

3

Simple example: replicated disk

replicated disk library disk 1 disk 2

slide-4
SLIDE 4

3

Simple example: replicated disk

replicated disk library disk 1 read/write disk 2

slide-5
SLIDE 5

3

Simple example: replicated disk

replicated disk library disk 1 read/write disk 2

slide-6
SLIDE 6

4

Replicated disk is subtle

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) d2.write(a, v) unlock_address(a) }

slide-7
SLIDE 7

4

Replicated disk is subtle

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) d2.write(a, v) unlock_address(a) }

what if system crashes here? what if disk 1 fails?

slide-8
SLIDE 8

4

Replicated disk is subtle

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) d2.write(a, v) unlock_address(a) } // runs on reboot func recover() { for a in … { // copy from d1 to d2 } }

what if system crashes here? what if disk 1 fails?

slide-9
SLIDE 9

4

Replicated disk is subtle

func read(a: addr): block { lock_address(a) v, ok := d1.read(a) if !ok { v, _ = d2.read(a) } unlock_address(a) return v } func write(a: addr, v: block) { lock_address(a) d1.write(a, v) d2.write(a, v) unlock_address(a) } // runs on reboot func recover() { for a in … { // copy from d1 to d2 } }

what if system crashes here? what if disk 1 fails?

slide-10
SLIDE 10

5

Goal: systematically reason about all executions with formal verification

slide-11
SLIDE 11

6

Existing verification frameworks do not support concurrency and crash safety

FSCQ [SOSP ’15] Yggdrasil [OSDI ’16] DFSCQ [SOSP ’17] … CertiKOS [OSDI ’16] CSPEC [OSDI ’18] AtomFS [SOSP ’19] … verified crash safety verified concurrency no system can do both

slide-12
SLIDE 12

7

Combining verified crash safety and concurrency is challenging

Crash and recovery can interrupt a critical section

➡ leases

Crash wipes in-memory state

➡ memory versioning

Recovery logically completes crashed threads’ operations

➡ recovery helping

slide-13
SLIDE 13

8

Perennial’s techniques address challenges integrating crash safety into concurrency reasoning

Crash and recovery can interrupt a critical section

➡ leases

Crash wipes in-memory state

➡ memory versioning

Recovery logically completes crashed threads’ operations

➡ recovery helping

slide-14
SLIDE 14

8

Perennial’s techniques address challenges integrating crash safety into concurrency reasoning

Crash and recovery can interrupt a critical section

➡ leases

Crash wipes in-memory state

➡ memory versioning

Recovery logically completes crashed threads’ operations

➡ recovery helping

this talk see paper

slide-15
SLIDE 15

9

Contributions

Perennial: framework for reasoning about crashes and concurrency Goose: reasoning about Go implementations Evaluation: verified mail server written in Go with Perennial

see paper

slide-16
SLIDE 16

10

Specifying correctness: concurrent recovery refinement

All operations are correct and atomic wrt concurrency and crashes Recovery repairs system after reboot

slide-17
SLIDE 17

Proving the replicated disk correct

11

slide-18
SLIDE 18

Background

12

Proving refinement with forward simulation: relate code and spec states

σ d1 d2

spec code

slide-19
SLIDE 19

Background

13

Proving refinement with forward simulation: prove every operation has a commit point

write(a, v) tid:

lock d1.write d2.write unlock

  • 1. Write down abstraction relation

between code and spec states

S1

C1 C2 C3 C4 C5

spec code

slide-20
SLIDE 20

Background

13

Proving refinement with forward simulation: prove every operation has a commit point

write(a, v) tid: write(a, v) tid:

lock d1.write d2.write unlock

  • 1. Write down abstraction relation

between code and spec states

  • 2. Prove every operation commits

S1 S2

C1 C2 C3 C4 C5

spec code

slide-21
SLIDE 21

Background

13

Proving refinement with forward simulation: prove every operation has a commit point

write(a, v) tid: write(a, v) tid:

lock d1.write d2.write unlock

  • 1. Write down abstraction relation

between code and spec states

  • 2. Prove every operation commits
  • 3. Prove abstraction relation is

preserved

S1 S2

C1 C2 C3 C4 C5

spec code

slide-22
SLIDE 22

14

Abstraction relation for the replicated disk

abstraction relation:

!locked(a) ⟹ σ[a] = d1[a] ∧ σ[a] = d2[a]

(if the disk has not failed)

σ d1 d2

slide-23
SLIDE 23

15

Crashing breaks the abstraction relation

func write(a: addr, v: block) { lock_address(a) d1.write(a, v)

lock reverts to being free, but disks are not in-sync abstraction relation:

!locked(a) ⟹ σ[a] = d1[a] ∧ σ[a] = d2[a]

slide-24
SLIDE 24

16

So far: abstraction relation always holds

R R ?

spec code

R

abstraction relation

crash

slide-25
SLIDE 25

17

Separate a crash invariant from the abstraction relation

R R C

R

abstraction relation

C

crash invariant

spec code crash

slide-26
SLIDE 26

18

Recovery proof uses the crash invariant to restore the abstraction relation

R R

crash

C

recover()

R R

crash

spec code

R

abstraction relation

C

crash invariant

slide-27
SLIDE 27

19

Proving recovery correct: makes writes atomic

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) func recover() { for a in … { v, ok := d1.read(a) if !ok { … } d2.write(a, v) } }

slide-28
SLIDE 28

20

User sees an atomic write due to recovery

code execution user’s view (spec)

crash

write(a, v) tid: pending spec operation

slide-29
SLIDE 29

20

User sees an atomic write due to recovery

tid:

crash

w1(a,v)

code execution user’s view (spec)

crash

write(a, v) tid: pending spec operation

slide-30
SLIDE 30

20

User sees an atomic write due to recovery

recover()

r1(a) w2(a,v) return tid:

crash

w1(a,v)

code execution user’s view (spec)

crash

write(a, v) tid: pending spec operation

slide-31
SLIDE 31

20

User sees an atomic write due to recovery

recover()

r1(a) w2(a,v) return tid:

crash

w1(a,v)

code execution user’s view (spec)

recovery helping crash

write(a, v) tid: write(a, v) tid: pending spec operation

slide-32
SLIDE 32

21

Recovery helping: recovery can commit writes from before the crash

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) func recover() { for a in … { v, ok := d1.read(a) if !ok { … } d2.write(a, v) } }

write(a, v) tid: write(a, v) tid:

slide-33
SLIDE 33

22

Crash invariant says “if disks disagree, some thread was writing the value on the first disk”

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) func recover() { for a in … { v, ok := d1.read(a) if !ok { … } d2.write(a, v) } }

crash invariant: d1[a] ≠ d2[a] ⟹

write(a, )

d1[a]

tid:

∃tid. write(a, v) tid: write(a, v) tid:

slide-34
SLIDE 34

22

Crash invariant says “if disks disagree, some thread was writing the value on the first disk”

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) func recover() { for a in … { v, ok := d1.read(a) if !ok { … } d2.write(a, v) } }

crash invariant: d1[a] ≠ d2[a] ⟹

write(a, )

d1[a]

tid:

∃tid. write(a, v) tid: write(a, v) tid:

slide-35
SLIDE 35

23

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) func recover() { for a in … { v, ok := d1.read(a) if !ok { … } d2.write(a, v) } }

crash invariant: d1[a] ≠ d2[a] ⟹

write(a, )

d1[a]

tid:

∃tid. write(a, v) tid: write(a, v) tid:

Key idea: crash invariant can refer to interrupted spec operations

slide-36
SLIDE 36

24

Recovery proof shows code restores the abstraction relation by completing all interrupted writes

func write(a: addr, v: block) { lock_address(a) d1.write(a, v) func recover() { for a in … { v, ok := d1.read(a) if !ok { … } d2.write(a, v) } }

crash abstraction relation:

!locked(a) ⟹ σ[a] = d1[a] ∧ σ[a] = d2[a]

write(a, v) tid: write(a, v) tid:

slide-37
SLIDE 37

25

Proving concurrent recovery refinement

Recovery proof uses crash invariant to restore abstraction relation Proof can refer to interrupted operations, enabling recovery helping reasoning Users get correct behavior and atomicity

slide-38
SLIDE 38

26

Implementation

Perennial (9k lines of Coq)

  • leases
  • memory versioning
  • recovery helping

Coq

Iris concurrency framework

this paper prior work developer-written

slide-39
SLIDE 39

26

Implementation

Perennial (9k lines of Coq)

  • leases
  • memory versioning
  • recovery helping

Coq

Iris concurrency framework

Go source

exe go build

this paper prior work developer-written

slide-40
SLIDE 40

26

Implementation

Perennial (9k lines of Coq)

  • leases
  • memory versioning
  • recovery helping

Coq

Iris concurrency framework

Goose translator (2k lines of Go) Proof

see paper

Go source

exe go build

this paper prior work developer-written

slide-41
SLIDE 41

26

Implementation

Perennial (9k lines of Coq)

  • leases
  • memory versioning
  • recovery helping

Coq

Iris concurrency framework

Goose translator (2k lines of Go) Proof

see paper

Go source

exe go build

machine checked by Coq

this paper prior work developer-written

slide-42
SLIDE 42

27

Evaluation

This talk:

  • proof-effort comparison

See paper:

  • verified examples
  • TCB
  • bug discussion
slide-43
SLIDE 43

28

Methodology:
 Verify the same mail server as previous work, CSPEC [OSDI ’18]

Users can read, deliver, and delete mail Implemented on top of a file system Operations are atomic (and crash safe in Perennial)

slide-44
SLIDE 44

29

Perennial mail server was easier to verify
 and proves crash safety

Perennial CSPEC [OSDI ’18] mail server proof 3,200 4,000 time 2 weeks (after framework) 6 months
 (with framework) code 159 (Go) 215 (Coq)

slide-45
SLIDE 45

30

Perennial mail server really is concurrent

0k 50k 100k 150k 200k 1 2 3 4 5 6 7 8 9 10 11 12

cores requests/sec (see the paper for details)

slide-46
SLIDE 46

31

Conclusion

Perennial introduces crash-safety techniques that extend concurrent verification in Iris Goose lets us reason about Go implementations Verified a Go mail server with less effort than previous work and proved crash safety chajed.io/perennial