Download - Specifying and Verifying Device Drivers

Transcript
Page 1: Specifying and Verifying Device Drivers

Specifying and VerifyingDevice Drivers

Wes Weimer

George Necula

Gregoire Sutre

Page 2: Specifying and Verifying Device Drivers

Overview

• Background and Motivation

• Safety Properties

• Events and History Registers

• Driver States

• Specifications

• Verification

• Implementation

Page 3: Specifying and Verifying Device Drivers

Background and Motivation

• Why bother?• PCC-style background

– Want a sound analysis

• Decided on an all-paths analysis– Similar to PREfix, ESC, Slam, Magick– Check pre- and post-conditions, kernel API– But also specify resource management, etc.

• Greg will recast as model checking later

Page 4: Specifying and Verifying Device Drivers

Safety Properties

• We aim to verify that …– Allocated resources are freed

• Over the lifetime of the driver

• Enforce acquire order: no deadlocks

– Kernel functions are called correctly• Also includes ordering requirements and

obligations, like schedule() and signal_pending().

– Certain minimum actions are performed

Page 5: Specifying and Verifying Device Drivers

Events

• Three broad types:+ allocation, - deallocation, ! event

• Applied to uninterpreted constructors:irq, dma, memory, schedule

• And given program values as arguments:+irq(5), -dma(3), !schedule

• Function call and return may be treated as events

Page 6: Specifying and Verifying Device Drivers

History Register

• Abstract register used to verify programs– Like the “memory register” in PCC/VCGen– Never appears in user-written code

• Keeps an ever-increasing list of events– E.g., [ +irq(7) ; !schedule ; +misc(&qpmouse) ]

• May refer to it in conditions– E.g., PRE(!historyContains(+irq(i)))

Page 7: Specifying and Verifying Device Drivers

Driver States

S0: Uninitialized

S1: Initialized

S2: Opened

init_module=0cleanup_module=0

misc_open=0 misc_release=0

misc_read=*

misc_write=*Inv(S0) = trueInv(S1) = historyContains(+misc(any))

Page 8: Specifying and Verifying Device Drivers

Driver States (cont’d)

• Kernel API guarantees a certain state-like behavior for device drivers– E.g., misc_open will only be called if the driver is in S1

• We associate an invariant with each state– Captures history register values– And global variables in the driver

• Must check all paths from S0 to S0.– For resource leaks, etc. – Similar to data-flow analysis

Page 9: Specifying and Verifying Device Drivers

Specifications

• Write spec in C, just like driver– May use data and control non-determinism– May use pre-conditions: like assert(), but will not fail

• Spec is minimal– List only things that must be done– Looks like a “template” device driver– Normal device drivers often written by copying others

• Side-conditions govern optional behavior– Like matching alloc/free– Or checking signals after sleeping

Page 10: Specifying and Verifying Device Drivers

Specification Example

int init_module() { /* user code must look like this */PRE(!historyContains(+misc(any)));switch (__rand_int_range(0,3)) {

case 0: misc_register(any); /* use kernel API */ return 0;

case 1: return –5; /* EIO */case 2: return –6; /* ENXIO */case 3: return –19; /* ENODEV */

}}

Page 11: Specifying and Verifying Device Drivers

Specification Example (2)

int misc_fasync(void *fd, void *filp, int on) {PRE(fd != 0 && filp && (on == 1 || on == 0)

&& current->state == TASK_RUNNING&& historyCount(!misc_open) >

historyCount(!misc_release));int retval = fasync_helper(fd, filp, on, any);if (retval < 0) return retval;else return 0;}

Page 12: Specifying and Verifying Device Drivers

Kernel API Spec

• Model behavior of kernel functions– Like kmalloc(), schedule(), kfree()– May modify history register

• Example:int request_irq(int i, void *f, …) {PRE(historyCount(+irq(i)) == historyCount(-irq(i)) &&

implements(f, irq_handler) && …);if (__rand_int_range(0,1)) { return –5; } else { historyAdd(+irq(i)); return 0; }}

Page 13: Specifying and Verifying Device Drivers

Kernel API Spec (2)int kmalloc(int size, int prio) {PRE(size > 0 && size < (128*1024) && (prio & GFP_KERNEL || prio & GFP_ATOMIC)

&& !(prio & GFP_KERNEL && prio & GFP_ATOMIC));if (prio & GFP_KERNEL) {

int old_state = current->state;current->state = TASK_INTERRUPTIBLE;schedule();current->state = old_state;historyAdd(!signal_pending);

}if (__rand_int_range(0,1) {

void *retval = __rand_int_range(1, MAX_INT/4)*4;historyAdd(+memory(retval));return retval;

} else return NULL;}

Page 14: Specifying and Verifying Device Drivers

Side Conditions

• Used to verify optional behavior– Like resource allocation, scheduling– Conditions on the history register

• Examples:– Every !schedule must match a !signal_pending– After !must_return(-512), must return –512– All +irq(…) before any +dma(…)– All +irq(…) occur after !misc_open

Page 15: Specifying and Verifying Device Drivers

Story so far …

• We have the user source code

• We have the history register abstraction– Keep track of the past, etc.

• We have a spec for the user code– Minimal, lists things that must be done

• We have a spec for the kernel API

• We have side conditions

Page 16: Specifying and Verifying Device Drivers

Verification

• When does the user code meet the spec?– Each user function F “matches up” (defined

next) with our spec for function F– If the user calls a kernel function with a pre-

condition, the condition will always be satisfied– All of the side conditions hold over the life of

the device driver

Page 17: Specifying and Verifying Device Drivers

“Matching Up”

• Can be phrased as a trace inclusion problem– More on that with the next speaker

• We’ll do it as an all-paths analysis

• Symbolically execute all paths in user code

• Keep track of history register

• Remove +resource(X) and –resource(X)

• Make sure other important events match

Page 18: Specifying and Verifying Device Drivers

How to Verify

• Symbolically execute all paths in user code– End up with a set of final states U– Verify pre-conditions, use theorem prover

• Symbolically execute all paths in spec– End up with a set of final states S

• For every uU, find an sS such that– They have the same return value– Their history registers match up– If s proves T then u proves T (for a few global T’s)

Page 19: Specifying and Verifying Device Drivers

How to Verify: States

• Start in S0, then check init_module()– Match each final state against the spec– Check all side-conditions in each final state

• Gather all states R with return value 0– Their LUB becomes the invariant for S1– Analyze all paths out of S1 (= symbolically

execute those functions with that invariant)

• Repeat until this terminates.

Page 20: Specifying and Verifying Device Drivers

What can go wrong?

• User code can call a kernel function and fail to meet the pre-condition

• User code can fail to meet a side condition

• User code can fail to match up to the spec

• If so, report an error!

• And hope that it’s not a false positive.

Page 21: Specifying and Verifying Device Drivers

Loop Invariants

• Used in symbolic execution to model loops

• Currently, we guess “true”– or use any other heuristic (e.g., on “for” loops)

• Except for the history register

• Gather all history changes in the loop body

• Add a special history element:• X_copies_of(history_element_list)

Page 22: Specifying and Verifying Device Drivers

Does it work?

• Sample implementation

• Tested on init_module() in 46 misc device drivers (about 300k post-processed each)

• 30 successful terminations (25 meet spec)– Others look fine, spec needs to be refined

• 16 “no answer after 45 seconds” – All-paths analysis is expensive: O(2n)

Page 23: Specifying and Verifying Device Drivers

Conclusions and Future Work

• Method for specifying and verifying– Spec looks like original code– Sound, incomplete, expensive– Uses abstract events

• Captures allocation, function ordering, other API and behavioral restrictions

• Next steps: – folding similar paths– handling concurrency (e.g., irq handlers)