seL4: Formal Verification of an OS Kernel
Klein, Elphinstone, Heiser, Andronick, Cock, Derrin,
Elkaduwe, Engelhardt, Kolanski, Norrish, Sewell,
Tuch, Winwood (2009)
What kind of paper is this?
- Big deal: First real system that was verified.
- "First formal proof of functional correctness of a complete
general-purpose operating system kernel."
The Story
- Once upon a time people built operating systems without any way to know they were correct
other than to run tests. However, operating systems are kind of critical and people thought it
might be nice to be able to prove they were correct. Unfortunately, everyone thought that formal
verification methods simply wouldn't work on something the size and complexity of an operating
system. The brave knights of Australia decided to give this a try. They developed a two stage
refinement approach 1) from abstract specification to executable specificatoin and 2) from
executable specification to implementation (in C). After two long person-decades they emerged
victorious from the quagmire -- with such a verified system, people could build secure operating
systems and sleep well, living happily ever after (as long as the specs were correct ...).
The rules of engagement
- Assumed to be correct: compiler, assembly code,
boot code, cache management, and hardware
- Definition of "kernel" = microkernel. (Sometimes we call the
entire operating system the kernel, so it's important to get this
clear.) -- the entire kernel is 8700 lines of C and 600 lines of
assembler.
- Definition of correctness: Implementation strictly follows the
high-level abstract specification
Kernel Overview
- 3rd generation microkernel
- based on L4
- Contains: virtual address spaces, threads, IPC, and capabilities (not
from L4)
- However, the kernel's VM has no kernel-defined structure -- that is
implemented by user-level pagers.
- Exceptions and non-native IPC also go to user-level servers to support
virtualization.
- Capabilities stored in capability container objects, CNodes, in
capability address spaces.
- Device drivers run at user level.
- Kernel memory allocation is typed and explicit.
Methodology
- Select Haskell (subset) as an intermediate target that is:
- readily accessible by both OS developers and formal methods practitioners
- providing an artefact that can be automatically translated into the theorem proving tool and reasoned about
- Write prototype kernel in Haskell (run it on simulated hardware). This
can be automatically translated into an "Executable Specifiction."
- Manually rewrite the kernel in C (allow for optimization)
- Verification technique is machine-assisted and machine-checked proof,
using Isabelle/HOL.
- They are showing that the implementation is a refinement of an
abstract specification. More precisely: the executable specification (translated
automatically from the Haskell prototype) is a refinement of the abstract
specification. The C implementation is a refinement of the Executable
Specification.
- So, the three parts are:
- Abstract specification: specifies the external kernel interface, how system
call argument are encoded in binary; what each system call does (in abstract
terms) and what happens on an interrupt or fault. Describes WHAT happens but
not how it happens.
- Executable specification: Contains all the data structure and implementation
details we expect to have in the final C kernel.
- C implementation. (That means there is a formal semantics for a large
subset of C.)
- Basic proof structure
- Preconditions
- Statement(s) that modify state
- Postconditions
OS Design
- Implicit state updates are bad (surprisingly global state is OK).
- Data structures with lots of different uses and invariants are also hard.
- Memory Management POLICY outside kernel; only need to prove that the
mechanism works in the kernel.
- Control parallelism (concurrency) by limiting to a uniprocessor.
- Limit concurrency as much as possible -- event driven model and mostly
atomic APIs.
- Run as much as possible with interrupts disabled and then enable
interrupts via polling (which gives you tight control over where they happen).
- Typically return out of the kernel on an interrupt and then retry later.
- Design to eliminate exceptions.
The Abstract Specification
- Describes WHAT the kernel does.
- Precisely describe: argument formats, encodings and error reporting
- Types are finite (e.g., 32-bit integer).
- Explicitly model memory and typed pointers.
- High level data structures: sets, lists, trees, functions, records.
- Allows for non-determinism.
Executable Specification
- Describes HOW the kernel works.
- Deterministic.
- Data structures get explicit types.
- Question: FIgure 4 is Haskell -- that's the Haskell prototype, not
the executable specification automatically translated from the Haskell
prototype, yes?
C Implementation
- C must be translated into Isabelle, therefore ..
- There must be a precise semantic model for (subset) C.
- Examples: memory model, structure padding, word sizes, unsafe casts, etc.
- Make assumptions about the compiler.
- Excluded from subset C:
- No address of (&) operator on locals
- Expressions with multiple functions must be side-effect free.
- No function calls through pointers
- No goto/switch statements with fallthroughs
- No compound literals as lvals
- No unions
Escaping from C
- Do not model hardware instructions (rely on testing instead for things
like cache/TLB flushes).
- machine_state encapsulates the formal machine model.
Verification and the Central Theorem
- Functional correctness via refinement.
- Formalised for general state machines.
- Main technique in refinement is to show that operations between states in
the abstract specification map to transitions in states in the refined
representation.
- Transition types
- kernel: the things described by each layer (in increasing detail)
- user: non-deterministically changing arbitrary user-accessible parts of
kernel state.
- User events: kernel entry
- idle: behavior of the idle thread
- idle events: interrupts that occur during idle time
- Let:
- MA: The Abstract machine
- ME: The Executable specification
- MC: The C implementation
- Therefore, we just need to show:
- ME refines MA
- MC refines ME
- Therefore, transitively, MC refines MA
Claims
- The behavior of the C implementation is fully captured by the abstract
specification.
- Coverage is complete.
- MC never fails and always has defined behavior.
- The kernel can never crash (all assertions are true).
- All kernel API calls terminate (and return to user level).
- No infinite loops.
- All parameter checking is correct.
- Four types of invariants
- Low-level memory invariants: no object at 0, everything is properly
aligned, objects have well-defined types, references refer to objects
of the correct types.
- Typing invariants: stronger than typical PL typing invariants.
Context dependent and include value ranges and exclude values (e.g., NULL).
- Data structure invariants: Links are correct in linked lists; no
loops in data structures.
- Algorithmic invariants: Prove things about how seL4 works (hardest part).
Evaluation
- Yet again, we have a paper whose evaluation is kind of tricky.
- I would call it a dancing bear, but they do claim performance is good.
- IPC performance looks good, but it's not actually part of the verified
C code base. That seems kind of unfair.
- Code size: 32,900 of Isabelle; 14,400 of Haskell/C and 165,000 of Proof!!!
- The proof is TWENTY PERSON YEARS.
- I found the section about effort fascinating.