seL4: Formal Verification of an OS...

18
seL4: Formal Verification of an OS Kernel Gerwin Klein 1,2 , Kevin Elphinstone 1,2 , Gernot Heiser 1,2,3 June Andronick 1,2 , David Cock 1 , Philip Derrin 1* , Dhammika Elkaduwe 1,2, Kai Engelhardt 1,2 Rafal Kolanski 1,2 , Michael Norrish 1,4 , Thomas Sewell 1 , Harvey Tuch 1,2, Simon Winwood 1,2 1 NICTA, 2 UNSW, 3 Open Kernel Labs, 4 ANU [email protected] Abstract Complete formal verification is the only known way to guarantee that a system is free of programming errors. We present our experience in performing the for- mal, machine-checked verification of the seL4 mi- crokernel from an abstract specification down to its C implementation. We assume correctness of com- piler, assembly code, and hardware, and we used a unique design approach that fuses formal and oper- ating systems techniques. To our knowledge, this is the first formal proof of functional correctness of a complete, general-purpose operating-system kernel. Functional correctness means here that the implemen- tation always strictly follows our high-level abstract specification of kernel behaviour. This encompasses traditional design and implementation safety proper- ties such as the kernel will never crash, and it will never perform an unsafe operation. It also proves much more: we can predict precisely how the kernel will behave in every possible situation. seL4, a third-generation microkernel of L4 prove- nance, comprises 8,700 lines of C code and 600 lines of assembler. Its performance is comparable to other high-performance L4 kernels. 1 Introduction The security and reliability of a computer system can only be as good as that of the underlying oper- ating system (OS) kernel. The kernel, defined as the part of the system executing in the most privileged mode of the processor, has unlimited hardware access. Therefore, any fault in the kernel’s implementation has the potential to undermine the correct operation of the rest of the system. General wisdom has it that bugs in any sizeable code base are inevitable. As a consequence, when security or reliability is paramount, the usual ap- * Philip Derrin is now at Open Kernel Labs. Harvey Tuch is now at VMware. Dhammika Elkaduwe is now at University of Peradeniya proach is to reduce the amount of privileged code, in order to minimise the exposure to bugs. This is a primary motivation behind security kernels and separation kernels [38, 54], the MILS approach [4], microkernels [1, 12, 35, 45, 57, 71] and isolation ker- nels [69], the use of small hypervisors as a minimal trust base [16, 26, 56, 59], as well as systems that re- quire the use of type-safe languages for all code except some “dirty” core [7,23]. Similarly, the Common Cri- teria [66] at the strictest evaluation level requires the system under evaluation to have a “simple” design. With truly small kernels it becomes possible to take security and robustness further, to the point where it is possible to guarantee the absence of bugs [22, 36, 56, 64]. This can be achieved by formal, machine- checked verification, providing mathematical proof that the kernel implementation is consistent with its specification and free from programmer-induced implementation defects. We present seL4, a member of the L4 [46] microker- nel family, designed to provide this ultimate degree of assurance of functional correctness by machine- assisted and machine-checked formal proof. We have shown the correctness of a very detailed, low-level design of seL4 and we have formally verified its C implementation. We assume the correctness of the compiler, assembly code, boot code, management of caches, and the hardware; we prove everything else. Specifically, seL4 achieves the following: it is suitable for real-life use, and able to achieve performance that is comparable with the best- performing microkernels; its behaviour is precisely formally specified at an abstract level; its formal design is used to prove desirable properties, including termination and execution safety; its implementation is formally proven to satisfy the specification; and its access control mechanism is formally proven 1

Transcript of seL4: Formal Verification of an OS...

seL4: Formal Verification of an OS Kernel

Gerwin Klein1,2, Kevin Elphinstone1,2, Gernot Heiser1,2,3

June Andronick1,2, David Cock1, Philip Derrin1∗, Dhammika Elkaduwe1,2‡, Kai Engelhardt1,2

Rafal Kolanski1,2, Michael Norrish1,4, Thomas Sewell1, Harvey Tuch1,2†, Simon Winwood1,2

1 NICTA, 2 UNSW, 3 Open Kernel Labs, 4 [email protected]

AbstractComplete formal verification is the only known wayto guarantee that a system is free of programmingerrors.

We present our experience in performing the for-mal, machine-checked verification of the seL4 mi-crokernel from an abstract specification down to itsC implementation. We assume correctness of com-piler, assembly code, and hardware, and we used aunique design approach that fuses formal and oper-ating systems techniques. To our knowledge, this isthe first formal proof of functional correctness of acomplete, general-purpose operating-system kernel.Functional correctness means here that the implemen-tation always strictly follows our high-level abstractspecification of kernel behaviour. This encompassestraditional design and implementation safety proper-ties such as the kernel will never crash, and it willnever perform an unsafe operation. It also provesmuch more: we can predict precisely how the kernelwill behave in every possible situation.

seL4, a third-generation microkernel of L4 prove-nance, comprises 8,700 lines of C code and 600 linesof assembler. Its performance is comparable to otherhigh-performance L4 kernels.

1 IntroductionThe security and reliability of a computer systemcan only be as good as that of the underlying oper-ating system (OS) kernel. The kernel, defined as thepart of the system executing in the most privilegedmode of the processor, has unlimited hardware access.Therefore, any fault in the kernel’s implementationhas the potential to undermine the correct operationof the rest of the system.

General wisdom has it that bugs in any sizeablecode base are inevitable. As a consequence, whensecurity or reliability is paramount, the usual ap-∗Philip Derrin is now at Open Kernel Labs.†Harvey Tuch is now at VMware.‡Dhammika Elkaduwe is now at University of Peradeniya

proach is to reduce the amount of privileged code,in order to minimise the exposure to bugs. This isa primary motivation behind security kernels andseparation kernels [38, 54], the MILS approach [4],microkernels [1, 12, 35, 45, 57, 71] and isolation ker-nels [69], the use of small hypervisors as a minimaltrust base [16,26,56,59], as well as systems that re-quire the use of type-safe languages for all code exceptsome “dirty” core [7,23]. Similarly, the Common Cri-teria [66] at the strictest evaluation level requires thesystem under evaluation to have a “simple” design.

With truly small kernels it becomes possible to takesecurity and robustness further, to the point whereit is possible to guarantee the absence of bugs [22,36,56,64]. This can be achieved by formal, machine-checked verification, providing mathematical proofthat the kernel implementation is consistent withits specification and free from programmer-inducedimplementation defects.

We present seL4, a member of the L4 [46] microker-nel family, designed to provide this ultimate degreeof assurance of functional correctness by machine-assisted and machine-checked formal proof. We haveshown the correctness of a very detailed, low-leveldesign of seL4 and we have formally verified its Cimplementation. We assume the correctness of thecompiler, assembly code, boot code, management ofcaches, and the hardware; we prove everything else.

Specifically, seL4 achieves the following:

• it is suitable for real-life use, and able to achieveperformance that is comparable with the best-performing microkernels;• its behaviour is precisely formally specified at

an abstract level;• its formal design is used to prove desirable

properties, including termination and executionsafety;• its implementation is formally proven to satisfy

the specification; and• its access control mechanism is formally proven

1

to provide strong security guarantees.To our knowledge, seL4 is the first-ever general-

purpose OS kernel that is fully formally verified forfunctional correctness. As such, it is a platform ofunprecedented trustworthiness, which will allow theconstruction of highly secure and reliable systems ontop.

The functional-correctness property we prove forseL4 is much stronger and more precise than whatautomated techniques like model checking, staticanalysis or kernel implementations in type-safe lan-guages can achieve. We not only analyse specificaspects of the kernel, such as safe execution, but alsoprovide a full specification and proof for the kernel’sprecise behaviour.

We have created a methodology for rapid kernel de-sign and implementation that is a fusion of traditionaloperating systems and formal methods techniques.We found that our verification focus improved thedesign and was surprisingly often not in conflict withachieving performance.

In this paper, we present the design of seL4, discussthe methodologies we used, and provide an overviewof the approach used in the formal verification fromhigh-level specification to the C implementation. Wealso discuss the lessons we have learnt from theproject, and the implications for similar efforts.

The remainder of this paper is structured as fol-lows. In Sect. 2, we give an overview of seL4, of thedesign approach, and of the verification approach. InSect. 3, we describe how to design a kernel for formalverification. In Sect. 4, we describe precisely whatwe verified, and identify the assumptions we make.In Sect. 5, we describe important lessons we learntin this project, and in Sect. 6, we contrast our effortwith related work.

2 Overview2.1 seL4 programming modelThis paper is primarily about the formal verificationof the seL4 kernel, not its API design. We thereforeprovide only a brief overview of its main characteris-tics.

seL4 [20], similarly to projects at Johns Hopkins(Coyotos) and Dresden (Nova), is a third-generationmicrokernel, and is broadly based on L4 [46] andinfluenced by EROS [58]. It features abstractions forvirtual address spaces, threads, inter-process commu-nication (IPC), and, unlike most L4 kernels, capabili-ties for authorisation. Virtual address spaces have nokernel-defined structure; page faults are propagatedvia IPC to pager threads, responsible for definingthe address space by mapping frames into the virtualspace. Exceptions and non-native system calls are

also propagated via IPC to support virtualisation.IPC uses synchronous and asynchronous endpoints(port-like destinations without in-kernel buffering)for inter-thread communication, with RPC facilitatedvia reply capabilities. Capabilities are segregated andstored in capability address spaces composed of ca-pability container objects called CNodes.

As in traditional L4 kernels, seL4 device drivers runas normal user-mode applications that have accessto device registers and memory either by mappingthe device into the virtual address space, or by con-trolled access to device ports on Intel x86 hardware.seL4 provides a mechanism to receive notification ofinterrupts (via IPC) and acknowledge their receipt.

Memory management in seL4 is explicit: both in-kernel objects and virtual address spaces are pro-tected and managed via capabilities. Physical mem-ory is initially represented by untyped capabilities,which can be subdivided or retyped into kernel objectssuch as page tables, thread control blocks, CNodes,endpoints, and frames (for mapping in virtual ad-dress spaces). The model guarantees all memoryallocation in the kernel is explicit and authorised.

Initial development of seL4, and all verificationwork, was done for an ARMv6-based platform, witha subsequent port of the kernel (so far without proof)to x86.

2.2 Kernel design processOS developers tend to take a bottom-up approach tokernel design. High performance is obtained by man-aging the hardware efficiently, which leads to designsmotivated by low-level details. In contrast, formalmethods practitioners tend toward top-down design,as proof tractability is determined by system com-plexity. This leads to designs based on simple modelswith a high degree of abstraction from hardware.

As a compromise that blends both views, weadopted an approach [19, 22] based around an in-termediate target that is readily accessible by bothOS developers and formal methods practitioners. Ituses the functional programming language Haskell toprovide a programming language for OS developers,while at the same time providing an artefact that canbe automatically translated into the theorem provingtool and reasoned about.

Fig. 1 shows our approach in more detail. Thesquare boxes are formal artefacts that have a directrole in the proof. The double arrows represent imple-mentation or proof effort, the single arrows representdesign/implementation influence of artefacts on otherartefacts. The central artefact is the Haskell proto-type of the kernel. The prototype requires the designand implementation of algorithms that manage thelow-level hardware details. To execute the Haskell

Design Cycle

Haskell Prototype

Design

Formal Executable Spec

High-Performance C ImplementationUser Programs

Hardware Simulator

ProofManual Implementation

+

Figure 1: The seL4 design process

prototype in a near-to-realistic setting, we link itwith software (derived from QEMU) that simulatesthe hardware platform. Normal user-level executionis enabled by the simulator, while traps are passedto the kernel model which computes the result ofthe trap. The prototype modifies the user-level stateof the simulator to appear as if a real kernel hadexecuted in privileged mode.

This arrangement provides a prototyping environ-ment that enables low-level design evaluation fromboth the user and kernel perspective, including low-level physical and virtual memory management. Italso provides a realistic execution environment that isbinary-compatible with the real kernel. For example,we ran a subset of the Iguana embedded OS [37] onthe simulator-Haskell combination. The alternativeof producing the executable specification directlyin the theorem prover would have meant a steeplearning curve for the design team and a much lesssophisticated tool chain for execution and simulation.

We restrict ourselves to a subset of Haskell thatcan be automatically translated into the languageof the theorem prover we use. For instance, we donot make any substantial use of laziness, make onlyrestricted use of type classes, and we prove that allfunctions terminate. The details of this subset aredescribed elsewhere [19,41].

While the Haskell prototype is an executable modeland implementation of the final design, it is not thefinal production kernel. We manually re-implementthe model in the C programming language for severalreasons. Firstly, the Haskell runtime is a significantbody of code (much bigger than our kernel) whichwould be hard to verify for correctness. Secondly, theHaskell runtime relies on garbage collection which isunsuitable for real-time environments. Incidentally,the same arguments apply to other systems based ontype-safe languages, such as SPIN [7] and Singular-ity [23]. Additionally, using C enables optimisation ofthe low-level implementation for performance. Whilean automated translation from Haskell to C wouldhave simplified verification, we would have lost most

Abstract Specification

Executable Specification

High-Performance C Implementation

Haskell Prototype

Isabelle/HOL

Automatic Translation

Refinement Proof

Figure 2: The refinement layers in the verification ofseL4

opportunities to micro-optimise the kernel, which isrequired for adequate microkernel performance.

2.3 Formal verificationThe technique we use for formal verification is inter-active, machine-assisted and machine-checked proof.Specifically, we use the theorem prover Isabelle/HOL[50]. Interactive theorem proving requires humanintervention and creativity to construct and guidethe proof. However, it has the advantage that it isnot constrained to specific properties or finite, feasi-ble state spaces, unlike more automated methods ofverification such as static analysis or model checking.

The property we are proving is functional correct-ness in the strongest sense. Formally, we are showingrefinement [18]: A refinement proof establishes acorrespondence between a high-level (abstract) anda low-level (concrete, or refined) representation of asystem.

The correspondence established by the refinementproof ensures that all Hoare logic properties of theabstract model also hold for the refined model. Thismeans that if a security property is proved in Hoarelogic about the abstract model (not all security prop-erties can be), refinement guarantees that the sameproperty holds for the kernel source code. In thispaper, we concentrate on the general functional cor-rectness property. We have also modelled and provedthe security of seL4’s access-control system in Is-abelle/HOL on a high level. This is described else-where [11,21], and we have not yet connected it tothe proof presented here.

Fig. 2 shows the specification layers used in theverification of seL4; they are related by formal proof.Sect. 4 explains the proof and each of these layers indetail; here we give a short summary.

The top-most layer in the picture is the abstractspecification: an operational model that is the main,complete specification of system behaviour. Theabstract level contains enough detail to specify theouter interface of the kernel, e.g., how system-callarguments are encoded in binary form, and it de-

scribes in abstract logical terms the effect of eachsystem call or what happens when an interrupt orVM fault occurs. It does not describe in detail howthese effects are implemented in the kernel.

The next layer down in Fig. 2 is the executablespecification generated from Haskell into the theo-rem prover. The translation is not correctness-criticalbecause we seek assurance about the generated def-initions and ultimately C, not the Haskell source,which only serves as an intermediate prototype. Theexecutable specification contains all data structureand implementation details we expect the final Cimplementation to have.

Finally, the bottom layer in this verification effortis the high-performance C implementation of seL4.For programs to be formally verified they must haveformally-defined semantics. One of the achievementsof this project is a very exact and faithful formalsemantics for a large subset of the C programminglanguage [62]. Even with a formal semantics of C inthe logic of the theorem prover, we still have to readand translate the specific program into the prover.This is discussed in Sect. 4.3.

Verification can never be absolute; it always mustmake fundamental assumptions. In our work westop at the source-code level, which implies that weassume at least the compiler and the hardware to becorrect.

An often-raised concern is the question of proofcorrectness. More than 30 years of research in the-orem proving has addressed this issue, and we cannow achieve a degree of trustworthiness of formal,machine-checked proof that far surpasses the confi-dence levels we rely on in engineering or mathematicsfor our daily survival. We use two specific techniques:firstly, we work foundationally from first principles;mathematics, semantics, and Hoare logic are not ax-iomatised, but defined and proved. Secondly, theIsabelle theorem prover we are using can produceexternal proof representations that can be indepen-dently checked by a small, simple proof checker.

3 Kernel Design for VerificationThe main body of the correctness proof can bethought of as showing Hoare triples on program state-ments and on functions in each of the specificationlevels. The proof in our refinement and Hoare logicframework decomposes along function boundaries.Each unit of proof has a set of preconditions thatneed to hold prior to execution, a statement or se-quence of statements in a function that modify thesystem state, and the post-conditions that must holdafterwards. The degree of difficulty in showing thatpre- and post-conditions hold is directly related to thecomplexity of the statement, the state the statement

can modify, and the complexity of the properties thepre- and post-conditions express. Around 80 % of theproperties we show relate to preserving invariants.

To make verification of the kernel feasible, its designshould minimise the complexity of these components.Ideally, the kernel code (and associated proofs) wouldconsist of simple statements that rely on explicitlocal state, with simple invariants. These smallerelements could then be composed abstractly intolarger elements that avoid exposing underlying localelements. Unfortunately, OS kernels are not usuallystructured like this, and generally feature highly inter-dependent subsystems [10].

As a consequence of our design goal of suitabilityfor real-life use, our kernel design attempts to min-imise the proof complexity without compromisingperformance. In this light we will now examine typi-cal properties of kernels and discuss their effect onverification, including presenting specific features ofour kernel design.

3.1 Global variables and side effectsProgramming with global variables and with sideeffects is common in operating systems kernels andour verification technique has no problem dealingwith them. However, implicit state updates andcomplex use of the same global for different purposescan make verification harder than necessary.

Global variables usually require stating and provinginvariant properties. For example, if global schedulerqueues are implemented as doubly-linked lists, thecorresponding invariant might state that all backlinks in the list point to the appropriate nodes andthat all elements point to thread control blocks. In-variants are expensive because they need to be provednot only locally for the functions that directly manip-ulate the scheduler queue, but for the whole kernel—we have to show that no other pointer manipulationin the kernel accidentally destroys the list or its prop-erties. This proof can be easy or hard, depending onhow modularly the global variable is used.

A hypothetical, problematic example would be acomplex, overloaded page-table data structure thatcan represent translation, validity of memory regions,copy-on-write, zero-on-demand memory, and the lo-cation of data in swap space, combined with a re-lationship to the frame table. This would create alarge, complex invariant for each of the involved datastructures, and each of the involved operations wouldhave to preserve all of it.

The treatment of globals becomes especially diffi-cult if invariants are temporarily violated. For ex-ample, adding a new node to a doubly-linked listtemporarily violates invariants that the list is wellformed. Larger execution blocks of unrelated code,

as in preemption or interrupts, should be avoidedduring that violation. We address these issues bylimiting preemption points, and by deriving the codefrom Haskell, thus making side effects explicit andbringing them to the attention of the design team.

3.2 Kernel memory managementThe seL4 kernel uses a model of memory allocationthat exports control of the in-kernel allocation to ap-propriately authorised applications [20]. While thismodel is mostly motivated by the need for preciseguarantees of memory consumption, it also benefitsverification. The model pushes the policy for allo-cation outside of the kernel, which means we onlyneed to prove that the mechanism works, not thatthe user-level policy makes sense. Obviously, mov-ing it into userland does not change the fact thatthe memory-allocation module is part of the trustedcomputing base. It does mean, however, that such amodule can be verified separately, and can rely onverified kernel properties.

The correctness of the allocation algorithm involveschecks that new objects are wholly contained withinan untyped (free) memory region and that they donot overlap with any other objects allocated from theregion. Our memory allocation model keeps track ofcapability derivations in a tree-like structure, whosenodes are the capabilities themselves.

Before re-using a block of memory, all references tothis memory must be invalidated. This involves eitherfinding all outstanding capabilities to the object, orreturning the object to the memory pool only whenthe last capability is deleted. Our kernel uses bothapproaches.

In the first approach, the capability derivation treeis used to find and invalidate all capabilities referringto a memory region. In the second approach, thecapability derivation tree is used to ensure, with acheck that is local in scope, that there are no system-wide dangling references. This is possible becauseall other kernel objects have further invariants ontheir own internal references that relate back to theexistence of capabilities in this derivation tree.

3.3 Concurrency and non-determinismConcurrency is the execution of computation in par-allel (in the case of multiple hardware processors), orby non-deterministic interleaving via a concurrencyabstraction like threads. Proofs about concurrentprograms are hard, much harder than proofs aboutsequential programs.

While we have some ideas on how to construct verifi-able systems on multiprocessors, they are outside thescope of this paper and left for future work. In thispaper we focus on uniprocessor support where the de-

gree of interleaving of execution and non-determinismcan be controlled. However, even on a uniprocessorthere is some remaining concurrency resulting fromasynchronous I/O devices. seL4 avoids much of thecomplications resulting from I/O by running devicedrivers at user level, but it must still address inter-rupts.

Consider the small code fragment A; X; B, whereA must establish the state that X relies on, X mustestablish the state B relies on, and so on. Concur-rency issues in the verification of this code arise fromyielding, interrupts and exceptions.

Yielding at X results in the potential execution ofany reachable activity in the system. This impliesA must establish the preconditions required for allreachable activities, and all reachable activities onreturn must establish the preconditions of B. Yieldingincreases complexity significantly and makes verifica-tion harder. Preemption is a non-deterministicallyoptional yield. Blocking kernel primitives, such as inlock acquisition and waiting on condition variables,are also a form of non-deterministic yield.

By design, we side-step addressing the verificationcomplexity of yield by using an event-based kernelexecution model, with a single kernel stack, and amostly atomic application programming interface[25].

Interrupt complexity has two forms: non-deterministic execution of the interrupt handlers,and interrupt handling resulting in preemption (as aresult of timer ticks). Theoretically, this complexitycan be avoided by disabling interrupts during kernelexecution. However, this would be at the expenseof large or unbounded interrupt latency, which weconsider unacceptable.

Instead, we run the kernel with interrupts mostlydisabled, except for a small number of carefully-placed interrupt points. If, in the above code frag-ment, X is the interrupt point, A must establish thestate that all interrupt handlers rely on, and all reach-able interrupt handlers must establish or preservethe properties B relies on.

We simplify the problem further by implementinginterrupt points via polling, rather than temporaryenabling of interrupts. On detection of a pendinginterrupt, we explicitly return through the functioncall stack to the kernel/user boundary. At the bound-ary we leave a (potentially modified) event storedin the saved user-level registers. The interrupt be-comes a new kernel event (prefixed to the pendinguser-triggered event). After the in-kernel compo-nent of interrupt handling, the interrupted eventis restarted. This effectively re-tries the (modified)operation, including re-establishing all the precondi-

tions for execution. In this way we avoid the needfor any interrupt-point specific post-conditions forinterrupt handlers, but still achieve Fluke-like partialpreemptability [25].

The use of interrupt points creates a trade-off, con-trolled by the kernel designer, between proof com-plexity and interrupt processing latency. Almost allof seL4’s operations have short and bounded latency,and can execute without any interrupt points at all.The exception is object destruction, whose cleanupoperations are inherently unbounded, and are, ofcourse, critical to kernel integrity.

We make these operations preemptable by storingthe state of progress of destruction in the last capabil-ity referencing the object being destroyed; we refer tothis as a zombie capability. This guarantees that thecorrectness of a restarted destroy is not dependent onuser-accessible registers. Another advantage of thisapproach is that if another user thread attempts todestroy the zombie, it will simply continue where thefirst thread was preempted (a form of priority inheri-tance), instead of making the new thread dependent(blocked) on the completion of another.

Exceptions are similar to interrupts in their effect,but are synchronous in that they result directly fromthe code being executed and cannot be deferred.In the seL4 kernel we avoid exceptions completelyand much of that avoidance is guaranteed as a side-effect of verification. Special care is required only formemory faults.

We avoid having to deal with virtual-memory ex-ceptions in kernel code by mapping a fixed region ofthe virtual address space to physical memory, inde-pendent of whether it is actively used or not. Theregion contains all the memory the kernel can poten-tially use for its own internal data structures, and isguaranteed to never produce a fault. We prove thatthis region appears in every virtual address space.

Arguments passed to the kernel from user level areeither transferred in registers or limited to prereg-istered physical frames accessed through the kernelregion.

3.4 I/OAs described earlier we avoid most of the complexityof I/O by moving device drivers into protected user-mode components. When processing an interruptevent, our interrupt delivery mechanism determinesthe interrupt source, masks further interrupts fromthat specific source, notifies the registered user-levelhandler (device driver) of the interrupt, and unmasksthe interrupt when the handler acknowledges theinterrupt.

We coarsely model the hardware interrupt con-troller of the ARM platform to include interrupt

support in the proof. The model includes existenceof the controller, masking of interrupts, and thatinterrupts only occur if unmasked. This is sufficientto include interrupt controller access, and basic be-haviour in the proof, without modelling correctnessof the interrupt controller management in detail. Theproof is set up such that it is easy to include moredetail in the hardware model should it become nec-essary later to prove additional properties.

Our kernel contains a single device driver, the timerdriver, which generates timer ticks for the scheduler.This is set up in the initialisation phase of the ker-nel as an automatically reloaded source of regularinterrupts. It is not modified or accessed during theexecution of the kernel. We did not need to modelthe timer explicitly in the proof, we just prove thatsystem behaviour on each tick event is correct.

3.5 ObservationsThe requirements of verification force the designers tothink of the simplest and cleanest way of achievingtheir goals. We found repeatedly that this leadsto overall better design, which tends to reduce thelikelihood of bugs.

In a number of cases there were significant otherbenefits. This is particularly true for the design deci-sions aimed at simplifying concurrency-related verifi-cation issues. Non-preemptable execution (except fora few interrupt-points) has traditionally been usedin L4 kernels to maximise average-case performance.Recent L4 kernels aimed at embedded use [32] haveadopted an event-based design to reduce the kernel’smemory footprint (due to the use of a single kernelstack rather than per-thread stacks).

4 seL4 VerificationThis section describes each of the specification layersas well as the proof in more detail.

4.1 Abstract specificationThe abstract level describes what the system doeswithout saying how it is done. For all user-visible ker-nel operations it describes the functional behaviourthat is expected from the system. All implemen-tations that refine this specification will be binarycompatible.

We precisely describe argument formats, encodingsand error reporting, so for instance some of the C-level size restrictions become visible on this level. Inorder to express these, we rarely make use of infinitetypes like natural numbers. Instead, we use finitemachine words, such as 32-bit integers. We modelmemory and typed pointers explicitly. Otherwise,the data structures used in this abstract specificationare high-level — essentially sets, lists, trees, functionsand records. We make use of non-determinism in

schedule ≡ do

threads ← all_active_tcbs;

thread ← select threads;

switch_to_thread thread

od OR switch_to_idle_thread

Figure 3: Isabelle/HOL code for scheduler at abstractlevel.

order to leave implementation choices to lower levels:If there are multiple correct results for an operation,this abstract layer would return all of them and makeclear that there is a choice. The implementation isfree to pick any one of them.

An example of this is scheduling. No schedul-ing policy is defined at the abstract level. Instead,the scheduler is modelled as a function picking anyrunnable thread that is active in the system or theidle thread. The Isabelle/HOL code for this is shownin Fig. 3. The function all_active_tcbs returnsthe abstract set of all runnable threads in the system.Its implementation (not shown) is an abstract logicalpredicate over the whole system. The select state-ment picks any element of the set. The OR makes anon-deterministic choice between the first block andswitch_to_idle_thread. The executable specifica-tion makes this choice more specific.4.2 Executable specificationThe purpose of the executable specification is to fillin the details left open at the abstract level and tospecify how the kernel works (as opposed to what itdoes). While trying to avoid the messy specifics ofhow data structures and code are optimised in C, wereflect the fundamental restrictions in size and codestructure that we expect from the hardware and the Cimplementation. For instance, we take care not to usemore than 64 bits to represent capabilities, exploitingfor instance known alignment of pointers. We do notspecify in which way this limited information is laidout in C.

The executable specification is deterministic; theonly non-determinism left is that of the underlyingmachine. All data structures are now explicit datatypes, records and lists with straightforward, efficientimplementations in C. For example the capabilityderivation tree of seL4, modelled as a tree on theabstract level, is now modelled as a doubly linkedlist with limited level information. It is manipulatedexplicitly with pointer-update operations.

Fig. 4 shows part of the scheduler specification atthis level. The additional complexity becomes appar-ent in the chooseThread function that is no longermerely a simple predicate, but rather an explicitsearch backed by data structures for priority queues.The specification fixes the behaviour of the schedulerto a simple priority-based round-robin algorithm. It

schedule = doaction <- getSchedulerActioncase action of

ChooseNewThread -> dochooseThreadsetSchedulerAction ResumeCurrentThread...

chooseThread = dor <- findM chooseThread’ (reverse [minBound .. maxBound])when (r == Nothing) $ switchToIdleThread

chooseThread’ prio = doq <- getQueue prioliftM isJust $ findM chooseThread’’ q

chooseThread’’ thread = dorunnable <- isRunnable threadif not runnable then do

tcbSchedDequeue threadreturn False

else doswitchToThread threadreturn True

Figure 4: Haskell code for schedule.

mentions that threads have time slices and it clarifieswhen the idle thread will be scheduled. Note thatpriority queues duplicate information that is alreadyavailable (in the form of thread states), in order tomake it available efficiently. They make it easy tofind a runnable thread of high priority. The optimi-sation will require us to prove that the duplicatedinformation is consistent.

We have proved that the executable specificationcorrectly implements the abstract specification. Be-cause of its extreme level of detail, this proof alonealready provides stronger design assurance than hasbeen shown for any other general-purpose OS kernel.

4.3 C implementationThe most detailed layer in our verification is the Cimplementation. The translation from C into Isabelleis correctness-critical and we take great care to modelthe semantics of our C subset precisely and founda-tionally. Precisely means that we treat C semantics,types, and memory model as the standard prescribes,for instance with architecture-dependent word size,padding of structs, type-unsafe casting of pointers,and arithmetic on addresses. As kernel program-mers do, we make assumptions about the compiler(GCC) that go beyond the standard, and about thearchitecture used (ARMv6). These are explicit inthe model, and we can therefore detect violations.Foundationally means that we do not just axiomatisethe behaviour of C on a high level, but we derive itfrom first principles as far as possible. For example,in our model of C, memory is a primitive functionfrom addresses to bytes without type information orrestrictions. On top of that, we specify how typeslike unsigned int are encoded, how structures arelaid out, and how implicit and explicit type castsbehave. We managed to lift this low-level memorymodel to a high-level calculus that allows efficient,

abstract reasoning on the type-safe fragment of thekernel [62,63,65]. We generate proof obligations as-suring the safety of each pointer access and write.They state that the pointer in question must be non-null and of the correct alignment. They are typicallyeasy to discharge. We generate similar obligationsfor all restrictions the C99 standard demands.

We treat a very large, pragmatic subset of C99 inthe verification. It is a compromise between verifica-tion convenience and the hoops the kernel program-mers were willing to jump through in writing theirsource. The following paragraphs describe what isnot in this subset.

We do not allow the address-of operator & on localvariables, because, for better automation, we makethe assumption that local variables are separate fromthe heap. This could be violated if their addresswas available to pass on. It is the most far-reachingrestriction we implement, because it is common touse local variable references for return parameters oflarge types that we do not want to pass on the stack.We achieved compliance with this requirement byavoiding reference parameters as much as possible,and where they were needed, used pointers to globalvariables (which are not restricted).

One feature of C that is problematic for verifica-tion (and programmers) is the unspecified order ofevaluation in expressions with side effects. To dealwith this feature soundly, we limit how side effectscan occur in expressions. If more than one functioncall occurs within an expression or the expressionotherwise depends on global state, a proof obliga-tion is generated to show that these functions areside-effect free. This proof obligation is dischargedautomatically by Isabelle.

We do not allow function calls through functionpointers. (We do allow handing the address of afunction to assembler code, e.g. for installing ex-ception vector tables.) We also do not allow gotostatements, or switch statements with fall-throughcases. We support C99 compound literals, making itconvenient to return structs from functions, and re-ducing the need for reference parameters. We do notallow compound literals to be lvalues. Some of theserestrictions could be lifted easily, but the featureswere not required in seL4.

We did not use unions directly in seL4 and thereforedo not support them in the verification (althoughthat would be possible). Since the C implementationwas derived from a functional program, all unionsin seL4 are tagged, and many structs are packedbitfields. Like other kernel implementors, we donot trust GCC to compile and optimise bitfieldspredictably for kernel code. Instead, we wrote a

void setPriority(tcb_t *tptr, prio_t prio) {prio_t oldprio;if(thread_state_get_tcbQueued(tptr->tcbState)) {

oldprio = tptr->tcbPriority;ksReadyQueues[oldprio] =

tcbSchedDequeue(tptr, ksReadyQueues[oldprio]);if(isRunnable(tptr)) {

ksReadyQueues[prio] =tcbSchedEnqueue(tptr, ksReadyQueues[prio]);

}else {

thread_state_ptr_set_tcbQueued(&tptr->tcbState,false);

}}tptr->tcbPriority = prio;

}

Figure 5: C code for part of the scheduler.

small tool that takes a specification and generates Ccode with the necessary shifting and masking for suchbitfields. The tool helps us to easily map structures topage table entries or other hardware-defined memorylayouts. The generated code can be inlined and, aftercompilation on ARM, the result is more compactand faster than GCC’s native bitfields. The toolnot only generates the C code, it also automaticallygenerates Isabelle/HOL specifications and proofs ofcorrectness [13].

Fig. 5 shows part of the implementation of thescheduling functionality described in the previoussections. It is standard C99 code with pointers, ar-rays and structs. The thread_state functions usedin Fig. 5 are examples of generated bitfield accessors.

4.4 Machine modelProgramming in C is not sufficient for implementinga kernel. There are places where the programmerhas to go outside the semantics of C to manipulatehardware directly. In the easiest case, this is achievedby writing to memory-mapped device registers, asfor instance with a timer chip; in other cases one hasto drop down to assembly to implement the requiredbehaviour, as for instance with TLB flushes.

Presently, we do not model the effects of certaindirect hardware instructions because they are toofar below the abstraction layer of C. Of these, cacheand TLB flushes are relevant for the correctnessof the code, and we rely on traditional testing forthese limited number of cases. Higher assurance canbe obtained by adding more detail to the machinemodel—we have phrased the machine interface suchthat future proofs about the TLB and cache can beadded with minimal changes. Additionally, requiredbehaviour can be guaranteed by targeted assertions(e.g., that page-table updates always flush the TLB),which would result in further proof obligations.

The basis of this formal model of the machine isthe internal state of the relevant devices, collectedin one record machine_state. For devices that we

configureTimer :: irq => unit machine_mresetTimer :: unit machine_msetCurrentPD :: paddr => unit machine_msetHardwareASID :: hw_asid => unit machine_minvalidateTLB :: unit machine_minvalidateHWASID :: hw_asid => unit machine_minvalidateMVA :: word => unit machine_mcleanCacheMVA :: word => unit machine_mcleanCacheRange :: word => word => unit machine_mcleanCache :: unit machine_minvalidateCacheRange :: word => word => unit machine_mgetIFSR :: word machine_mgetDFSR :: word machine_mgetFAR :: word machine_mgetActiveIRQ :: (irq option) machine_mmaskInterrupt :: bool => irq => unit machine_m

Figure 6: Machine interface functions.

model more closely, such as the interrupt controller,the relevant part in machine_state contains detailssuch as which interrupts are currently masked. Forthe parts that we do not model, such as the TLB, weleave the corresponding type unspecified, so it canbe replaced with more details later.

Fig. 6 shows our machine interface. The functionsare all of type X machine_m which restricts any sideeffects to the machine_state component of the sys-tem. Most of the functions return nothing (typeunit), but change the state of a device. In the ab-stract and executable specification, these functionsare implemented with maximal non-determinism.This means that in the extreme case they may arbi-trarily change their part of the machine state. Evenfor devices that we model, we are careful to leave asmuch behaviour as possible non-deterministic. Theless behaviour we prescribe, the less assumptions themodel makes about the hardware.

In the seL4 implementation, the functions in Fig. 6are implemented in C where possible, and otherwisein assembly; we must check (but we do not prove)that the implementations match the assumptions wemake in the levels above. An example is the functiongetIFSR, which on ARM returns the instruction faultstatus register after a page fault. For this function,which is basically a single assembly instruction, weonly assume that it does not change the memorystate of the machine, which is easy to check.

4.5 The proofThis section describes the main theorem we haveshown and how its proof was constructed.

As mentioned, the main property we are inter-ested in is functional correctness, which we prove byshowing formal refinement. We have formalised thisproperty for general state machines in Isabelle/HOL,and we instantiate each of the specifications in theprevious sections into this state-machine framework.

We have also proved the well-known reduction ofrefinement to forward simulation, illustrated in Fig. 7:To show that a concrete state machineM2 refines an

abstract oneM1, it is sufficient to show that for eachtransition in M2 that may lead from an initial states to a set of states s′, there exists a correspondingtransition on the abstract side from an abstract stateσ to a set σ′ (they are sets because the machines maybe non-deterministic). The transitions correspond ifthere exists a relation R between the states s andσ such that for each concrete state in s′ there is anabstract one in σ′ that makes R hold between themagain. This has to be shown for each transition withthe same overall relation R. For each refinementlayer in Fig. 2, we have strengthened and varied thisproof technique slightly, but the general idea remainsthe same. Details are published elsewhere [14,70].

We now describe the instantiation of this frame-work to the seL4 kernel. We have the following typesof transition in our state machines: kernel transi-tions, user transitions, user events, idle transitions,and idle events. Kernel transitions are those thatare described by each of the specification layers inincreasing amount of detail. User transitions arespecified as non-deterministically changing arbitraryuser-accessible parts of the state space. User eventsmodel kernel entry (trap instructions, faults, inter-rupts). Idle transitions model the behaviour of theidle thread. Finally, idle events are interrupts oc-curring during idle time; other interrupts that occurduring kernel execution are modelled explicitly andseparately in each layer of Fig. 2.

The model of the machine and the model of userprograms remain the same across all refinement lay-ers; only the details of kernel behaviour and kerneldata structures change. The fully non-deterministicmodel of the user means that our proof includes allpossible user behaviours, be they benign, buggy, ormalicious.

Let machine MA denote the system framework in-stantiated with the abstract specification of Sect. 4.1,let machine ME represent the framework instanti-ated with the executable specification of Sect. 4.2,and let machine MC stand for the framework in-stantiated with the C program read into the theoremprover. Then we prove the following two, very simple-looking theorems:

Theorem 1 ME refines MA.

Theorem 2 MC refines ME.

Therefore, because refinement is transitive, we have

Theorem 3 MC refines MA.

Assumptions The assumptions we make are cor-rectness of the C compiler, the assembly code level,

State Relation

State Relation

Concrete Operation M2

Abstract Operation M1σ, σ′

s s'

Figure 7: Forward Simulation.

and the hardware. We currently omit correctnessof the boot/initialisation code which takes up about1.2 kLOC of the kernel; the theorems above statecorrespondence between the kernel entry and exitpoints in each specification layer. We describe theseassumptions in more detail below and discuss theirimplications.

For the C level, we assume that the GCC compilercorrectly implements our subset according to theISO/IEC C99 standard [39], that the formal modelof our C subset accurately reflects this standard andthat it makes the correct architecture-specific assump-tions for the ARMv6 architecture on the Freescalei.MX31 platform.

The assumptions on the hardware and assemblylevel mean that we do not prove correctness of theregister save/restore and the potential context switchon kernel exit. As described in Sect. 4.4, cache con-sistency, cache colouring, and TLB flushing require-ments are part of the assembly-implemented ma-chine interface. These machine interface functionsare called from C, and we assume they do not haveany effect on the memory state of the C program.This is only true under the assumption they are usedcorrectly.

In-kernel memory and code access is translated bythe TLB on ARM processors. For our C semantics,we assume a traditional, flat view of in-kernel memorythat is consistent because all kernel reads and writesare performed through a constant one-to-one VMwindow which the kernel establishes in every addressspace. We make this consistency argument onlyinformally; our model does not oblige us to prove it.We do however substantiate the model by manuallystated properties and invariants. This means ourtreatment of in-kernel virtual memory is different tothe high standards in the rest of our proof where wereason from first principles and the proof forces usto be complete.

These are not fundamental limitations of the ap-proach, but a decision taken to achieve the maximum

outcome with available resources. For instance, wehave verified the executable design of the boot codein an earlier design version. For context switching, Niet al. [49] report verification success, and the Verisoftproject [3] showed how to verify assembly code andhardware interaction. Leroy verified an optimising Ccompiler [44] for the PowerPC architecture. We havealso shown that kernel VM access and faults can bemodelled foundationally from first principles [42].

Assurance Having outlined the limitations of ourverification, we now discuss the properties that areproved.

Overall, we show that the behaviour of the C im-plementation is fully captured by the abstract spec-ification. This is a strong statement, as it allowsus to conduct all further analysis of properties thatcan be expressed as Hoare triples on the massivelysimpler abstract specification instead of a complexC program. Coverage is complete. Any remainingimplementation errors (deviations from the specifica-tion) can only occur below the level of C.

A cynic might say that an implementation proofonly shows that the implementation has precisely thesame bugs that the specification contains. This istrue: the proof does not guarantee that the specifica-tion describes the behaviour the user expects. Thedifference is the degree of abstraction and the absenceof whole classes of bugs. In the same notation, theabstract specification is one third the size of the Ccode and works with concepts that are simpler andfaster to reason about. The current level of abstrac-tion is low enough to be precise for the operationalbehaviour of the kernel. To analyse specific proper-ties of the system, one might also introduce another,even higher level of abstraction that contains onlythe aspects relevant for the property. An example isour access control model of seL4 [11,21].

In addition to the implementation correctness state-ment, our strengthened proof technique for forwardsimulation [14] implies that bothME andMC neverfail and always have defined behaviour. This meansthe kernel can never crash or otherwise behave un-expectedly as long as our assumptions hold. Thisincludes that all assertions1 in the kernel design aretrue on all code paths, and that the kernel neveraccesses a null pointer or a misaligned pointer.

We proved that all kernel API calls terminate andreturn to user level. There is no possible situation inwhich the kernel can enter an infinite loop. Since the

1One might think that assertions are pointless in a verifiedkernel. In fact, they are not only a great help during devel-opment, they also convey a useful message from the kerneldesigner to the verifier about an important invariant in thecode, and as such aid verification.

interface from user level to the abstract specificationis binary compatible with the final implementation,our refinement theorem implies that the kernel doesall argument checking correctly and that it can not besubverted by buggy encodings, spurious calls, mali-ciously constructed arguments to system calls, bufferoverflow attacks or other such vectors from user level.All these properties hold with the full assurance ofmachine-checked proof.

As part of the refinement proof between levelsMA

andME , we had to show a large number of invariants.These invariants are not merely a proof device, butprovide valuable information and assurance in them-selves. In essence, they collect information aboutwhat we know to be true of each data structure inthe kernel, before and after each system call, andalso for large parts during kernel execution wheresome of these invariants may be temporarily violatedand re-established later. The overall proof effortwas clearly dominated by invariant proofs, with theactual refinement statements between abstract andexecutable specification accounting for at most 20 %of the total effort for that stage. There is not enoughspace in this paper to enumerate all the invariantstatements we have proved, but we will attempt arough categorisation, show a few representatives, andgive a general flavour.

There are four main categories of invariants in ourproof: low-level memory invariants, typing invariants,data structure invariants, and algorithmic invariants.

The first two categories could in part be coveredby a type-safe language: low-level memory invariantsinclude that there is no object at address 0, thatkernel objects are aligned to their size, and that theydo not overlap. The typing invariants say that eachkernel object has a well-defined type and that itsreferences in turn point to objects of the right type.An example would be a capability slot containing areference to a thread control block (TCB). The invari-ant would state that the type of the first object is acapability-table entry and that its reference points toa valid object in memory with type TCB. Intuitively,this invariant implies that all reachable, potentiallyused references in the kernel—be it in capabilities,kernel objects or other data structures—always pointto an object of the expected type. This is a necessarycondition for safe execution: we need to know thatpointers point to well-defined and well-structureddata, not garbage. This is also a dynamic property,because objects can be deleted and memory can bere-typed at runtime. Note that the main invariant isabout potentially used references. We do allow somereferences, such as in ARM page table objects, to betemporarily left dangling, as long as we can prove

that these dangling references will never be touched.Our typing invariants are stronger than those onewould expect from a standard programming languagetype system. They are context dependent and theyinclude value ranges such as using only a certainnumber of bits for hardware address space identifiers(ASIDs). They also often exclude specific values suchas -1 or 0 as valid values because these are used inC to indicate success or failure of the correspondingoperations. Typing invariants are usually simple tostate and for large parts of the code, their preser-vation can be proved automatically. There are onlytwo operations where the proof is difficult: removingand retyping objects. Type preservation for thesetwo operations is the main reason for a large numberof other kernel invariants.

The third category of invariants are classical datastructure invariants like correct back links in doubly-linked lists, a statement that there are no loops inspecific pointer structures, that other lists are alwaysterminated correctly with NULL, or that data struc-ture layout assumptions are interpreted the sameway everywhere in the code. These invariants arenot especially hard to state, but they are frequentlyviolated over short stretches of code and then re-established later — usually when lists are updatedor elements are removed.

The fourth and last category of invariants that weidentify in our proof are algorithmic invariants thatare specific to how the seL4 kernel works. Theseare the most complex invariants in our proof andthey are where most of the proof effort was spent.These invariants are either required to prove thatspecific optimisations are allowed (e.g. that a checkcan be left out because the condition can be shownto be always true), or they are required to show thatan operation executes safely and does not violateother invariants, especially not the typing invariant.Examples of simple algorithmic invariants are thatthe idle thread is always in thread state idle, and thatonly the idle thread is in this state. Another one isthat the global kernel memory containing kernel codeand data is mapped in all address spaces. Slightlymore involved are relationships between the existenceof capabilities and thread states. For instance, ifa Reply capability exists to a thread, this threadmust always be waiting to receive a reply. This isa non-local property connecting the existence of anobject somewhere in memory with a particular stateof another object somewhere else. Other invariantsformally describe a general symmetry principle thatseL4 follows: if an object x has a reference to anotherobject y, then there is a reference in object y that canbe used to find object x directly or indirectly. This

fact is exploited heavily in the delete operation toclean up all remaining references to an object beforeit is deleted.

The reason this delete operation is safe is compli-cated. Here is a simplified, high-level view of thechain of invariants that show an efficient local pointertest is enough to ensure that deletion is globally safe:

(1) If an object is live (contains references to otherobjects), there exists a capability to it somewherein memory. (2) If an untyped capability c1 coversa sub-region of another capability c2, then c1 mustbe a descendant of c2 according to the capabilityderivation tree (CDT). (3) If a capability c1 pointsto a kernel object whose memory is covered by anuntyped capability c2, then c1 must be a descendantof c2.

With these, we have: If an untyped capability hasno children in the CDT (a simple pointer comparisonaccording to additional data structure invariants),then all kernel objects in its region must be non-live (otherwise there would be capabilities to them,which in turn would have to be children of the un-typed capability). If the objects are not live and nocapabilities to them exist, there is no further refer-ence in the whole system that could be made unsafeby the type change because otherwise the symmetryprinciple on references would be violated. Deletingthe object will therefore preserve the basic typingand safety properties. Of course we also have toshow that deleting the object preserves all the newinvariants we just used as well.

We have proved over 150 invariants on the differ-ent specification levels, most are interrelated, manyare complex. All these invariants are expressed asformulae on the kernel state and are proved to bepreserved over all possible kernel executions.

5 Experience and Lessons Learnt5.1 PerformanceIPC performance is the most critical metric for evalu-ation in a microkernel in which all interaction occursvia IPC. We have evaluated the performance of seL4by comparing IPC performance with L4, which hasa long history of data points to draw upon [47].

Publicly available performance for the Intel XScalePXA 255 (ARMv5) is 151 in-kernel cycles for a one-way IPC [43], and our experiments with OKL4 2.1[51] on the platform we are using (Freescale i.MX31evaluation board based on a 532 MHz ARM1136JF-Swhich is an ARMv6 ISA) produced 206 cycles as apoint for comparison—this number was producedusing a hand-crafted assembly-language path. Thenon-optimised C version in the same kernel took 756cycles. These are hot-cache measurements obtained

Haskell/C Isabelle Invariants ProofLOC LOC LOP

abst. — 4,900 ∼ 75exec. 5,700 13,000 ∼ 80 110,000

impl. 8,700 15,000 0 55,000

Table 1: Code and proof statistics.

using the processor cycle counter.Our measurement for seL4 is 224 cycles for one-way

IPC in an optimised C path, which is approachingthe performance of optimised assembly-language IPCpaths for other L4 kernels on ARM processors. Thisputs seL4 performance into the vicinity of the fastestL4 kernels.

At the time of writing, this optimised IPC C pathis not yet in the verified code base, but it is withinthe verifiable fragment of C and is not fundamentallydifferent from the rest of the code.

5.2 Verification effortThe overall code statistics are presented in Table 1.

The project was conducted in three phases. Firstan initial kernel with limited functionality (no inter-rupts, single address space and generic linear page ta-ble) was designed and implemented in Haskell, whilethe verification team mostly worked on the verifi-cation framework and generic proof libraries. In asecond phase, the verification team developed the ab-stract spec and performed the first refinement whilethe development team completed the design, Haskellprototype and C implementation. The third phaseconsisted of extending the first refinement step tothe full kernel and performing the second refinement.The overall size of the proof, including framework,libraries, and generated proofs (not shown in thetable) is 200,000 lines of Isabelle script.

The abstract spec took about 4 person months(pm) to develop. About 2 person years (py) wentinto the Haskell prototype (over all project phases),including design, documentation, coding, and testing.The executable spec only required setting up thetranslator; this took 3 pm.

The initial C translation was done in 3 weeks, intotal the C implementation took about 2 pm, for atotal cost of 2.2 py including the Haskell effort.

This compares well with other efforts for develop-ing a new microkernel from scratch: The Karlsruheteam reports that, on the back of their experiencefrom building the earlier Hazelnut kernel, the devel-opment of the Pistachio kernel cost about 6 py [17].SLOCCount [68] with the “embedded” profile esti-mates the total cost of seL4 at 4 py. Hence, there isstrong evidence that the detour via Haskell did notincrease the cost, but was in fact a significant netcost saver. This means that our development process

can be highly recommended even for projects notconsidering formal verification.

The cost of the proof is higher, in total about 20 py.This includes significant research and about 9 pyinvested in formal language frameworks, proof tools,proof automation, theorem prover extensions andlibraries. The total effort for the seL4-specific proofwas 11 py.

We expect that re-doing a similar verification fora new kernel, using the same overall methodology,would reduce this figure to 6 py, for a total (kernelplus proof) of 8 py. This is only twice the SLOCCountestimate for a traditionally-engineered system withno assurance. It certainly compares favourable toindustry rules-of-thumb of $10k/LOC for CommonCriteria EAL6 certification, which would be $87Mfor seL4, yet provides far less assurance than formalverification.

The breakdown of effort between the two refinementstages is illuminating: The first refinement step (fromabstract to executable spec) consumed 8 py, thesecond (to concrete spec) less than 3 py, almost a3:1 breakdown. This is a reflection of the low-levelnature of our Haskell implementation, which capturesmost of the properties of the final product. This isalso reflected in the proof size—the first proof stepcontained most of the deep semantic content. 80 % ofthe effort in the first refinement went into establishinginvariants, only 20 % into the actual correspondenceproof. We consider this asymmetry a significantbenefit, as the executable spec is more convenientand efficient to reason about than the C level. Ourformal refinement framework for C made it possible toavoid proving any invariants on the C code, speedingup this stage of the proof significantly. We provedonly few additional invariants on the executable speclayer to substantiate optimisations in C.

The first refinement step lead to some 300 changesin the abstract spec and 200 in the executable spec.About 50 % of these changes relate to bugs in theassociated algorithms or design, the rest were intro-duced for verification convenience. The ability tochange and rearrange code in discussion with thedesign team (to predict performance impact) was animportant factor in the verification team’s produc-tivity. It is a clear benefit of the approach describedin Sect. 2.2 and was essential to complete the verifi-cation in the available time.

By the time the second refinement started, the ker-nel had been used by a number of internal studentprojects and the x86 port was underway. Those twoactivities uncovered 16 defects in the implementa-tion before verification had started in earnest, theformal verification has uncovered another 144 defects

and resulted in 54 further changes to the code toaid in the proof. None of the bugs found in the Cverification stage were deep in the sense that thecorresponding algorithm was flawed. This is becausethe C code was written according to a very precise,low-level specification which was already verified inthe first refinement stage. Algorithmic bugs foundin the first stage were mainly missing checks on usersupplied input, subtle side effects in the middle of anoperation breaking global invariants, or over-strongassumptions about what is true during execution.The bugs discovered in the second proof from exe-cutable spec to C were mainly typos, misreading thespecification, or failing to update all relevant codeparts for specification changes. Simple typos alsomade up a surprisingly large fraction of discoveredbugs in the relatively well tested executable specifica-tion in the first refinement proof, which suggests thatnormal testing may not only miss hard and subtlebugs, but also a larger number of simple, obviousfaults than one may expect. Even though their causewas often simple, understandable human error, theireffect in many cases was sufficient to crash the kernelor create security vulnerabilities. Other more inter-esting bugs found during the C implementation proofwere missing exception case checking, and differentinterpretations of default values in the code. Forexample, the interrupt controller on ARM returns0xFF to signal that no interrupt is active which isused correctly in most parts of the code, but in oneplace the check was against NULL instead.

The C verification also lead to changes in the exe-cutable and abstract specifications: 44 of these wereto make the proof easier; 34 were implementationrestrictions, such as the maximum size of virtualaddress space identifiers, which the specificationsshould make visible to the user.

5.3 The cost of changeAn obvious issue of verification is the cost of proofmaintenance: how much does it cost to re-verify afterchanges made to the kernel? It obviously depends onthe nature of the change, specifically the amount ofcode it changes, the number of invariants it affects,and how localised it is. We are not able to quantifysuch costs, but our iterative approach to verificationhas provided us with some relevant experience.

The best case are local, low-level code changes, typ-ically optimisations that do not affect the observablebehaviour. We made such changes repeatedly, andfound that the effort for re-verification was always lowand roughly proportional to the size of the change.

Adding new, independent features, which do notinteract in a complex way with existing features, usu-ally has a moderate effect. For example, adding a new

system call to the seL4 API that atomically batchesa specific, short sequence of existing system callstook one day to design and implement. Adjustingthe proof took less than 1 pw.

Adding new, large, cross-cutting features, such asadding a complex new data structure to the kernelsupporting new API calls that interact with otherparts of the kernel, is significantly more expensive.We experienced such a case when progressing fromthe first to the final implementation, adding inter-rupts, ARM page tables and address spaces. Thischange cost several pms to design and implement,and resulted in 1.5–2 py to re-verify. It modifiedabout 12 % of existing Haskell code, added another37 %, and re-verification cost about 32 % of the timepreviously invested in verification.

The new features required only minor adjustmentsof existing invariants, but lead to a considerablenumber of new invariants for the new code. Theseinvariants have to be preserved over the whole kernelAPI, not just the new features.

Unsurprisingly, fundamental changes to existingfeatures are bad news. We had one example of sucha change when we added reply capabilities for ef-ficient RPC as an API optimisation after the firstrefinement was completed. Reply capabilities arecreated on the fly in the receiver of an IPC and aretreated in most cases like other capabilities. They aresingle-use, and thus deleted immediately after use.This fundamentally broke a number of properties andinvariants on capabilities. Creation and deletion ofcapabilities require a large number of preconditionsto execute safely. The operations were carefully con-strained in the kernel. Doing them on the fly requiredcomplex preconditions to be proved for many newcode paths. Some of these turned out not to be true,which required extra work on special-case proofs orchanges to existing invariants (which then neededto be re-proved for the whole kernel). Even thoughthe code size of this change was small (less than 5 %of the total code base), the comparative amount ofconceptual cross-cutting was huge. It took about1 py or 17 % of the original proof effort to re-verify.

There is one class of otherwise frequent codechanges that does not occur after the kernel hasbeen verified: implementation bug fixes.

6 Related WorkWe briefly summarise the literature on OS verifica-tion. Klein [40] provides a comprehensive overview.

The first serious attempts to verify an OS kernelwere in the late 1970s UCLA Secure Unix [67] andthe Provably Secure Operating System (PSOS) [24].Our approach mirrors the UCLA effort in using re-finement and defining functional correctness as the

main property to prove. The UCLA project man-aged to finish 90 % of their specification and 20 % oftheir proofs in 5 py. They concluded that invariantreasoning dominated the proof effort, which we foundconfirmed in our project.

PSOS was mainly focussed on formal kernel designand never completed any substantial implementationproofs. Its design methodology was later used for theKernelized Secure Operating System (KSOS) [52] byFord Aerospace. The Secure Ada Target (SAT) [30]and the Logical Coprocessor Kernel (LOCK) [55] arealso inspired by the PSOS design and methodology.

In the 1970s, machine support for theorem prov-ing was rudimentary. Basic language concepts likepointers still posed large problems. The UCLA ef-fort reports that the simplifications required to makeverification feasible made the kernel an order of mag-nitude slower [67]. We have demonstrated that withmodern tools and techniques, this is no longer thecase.

The first real, completed implementation proofs,although for a highly idealised OS kernel are reportedfor KIT, consisting of 320 lines of artificial, but real-istic assembly instructions [8].

Bevier and Smith later produced a formalisation ofthe Mach microkernel [9] without implementationsproofs. Other formal modelling and proofs for OSkernels that did not proceed to the implementationlevel include the EROS kernel [57], the high-levelanalysis of SELinux [5,29] based on FLASK [60], andthe MASK [48] project which was geared towardsinformation-flow properties.

The VFiasco project [36] and later the Robinproject [61] attempted to verify C++ kernel imple-mentations. They managed to create a precise modelof a large, relevant part of C++, but did not verifysubstantial parts of a kernel.

Heitmeyer et al [33] report on the verification andCommon Criteria certification of a “software-basedembedded device” featuring a small (3,000 LOC)separation kernel. They show data separation only,not functional correctness. Although they seem toat least model the implementation level, they didnot conduct a machine-checked proof directly on theC-code.

Hardin et al [31] formally verified information-flowproperties of the AAMP7 microprocessor [53], whichimplements the functionality of a static separationkernel in hardware. The functionality provided isless complex than a general purpose microkernel—the processor does not support online reconfigurationof separation domains. The proof goes down to alow-level design that is in close correspondence tothe micro code. This correspondence is not proven

formally, but by manual inspection.A similar property was recently shown for Green

Hills’ Integrity kernel [28] during a Common CriteriaEAL6+ certification [27]. The Separation KernelProtection Profile [38] of Common Criteria showsdata separation only. It is a weaker property thanfull functional correctness.

A closely related contemporary project isVerisoft [2], which is attempting to verify not only theOS, but a whole software stack from verified hardwareup to verified application programs. This includes aformally verified, non-optimising compiler for theirown Pascal-like implementation language. Even ifnot all proofs are completed yet, the project has suc-cessfully demonstrated that such a verification stackfor full functional correctness can be achieved. Theyhave also shown that verification of assembly-levelcode is feasible. However, Verisoft accepts two ordersof magnitude slow-down for their highly-simplifiedVAMOS kernel (e.g. only single-level page tables)and that their verified hardware platform VAMP isnot widely deployed. We deal with real C and stan-dard tool chains on ARMv6, and have aimed for acommercially deployable, realistic microkernel.

Other formal techniques for increasing the trustwor-thiness of operating systems include static analysis,model checking and shape analysis. Static analysiscan in the best case only show the absence of certainclasses of defects such as buffer overruns. Modelchecking in the OS space includes SLAM [6] andBLAST [34]. They can show specific safety prop-erties of C programs automatically, such as correctAPI usage in device drivers. The terminator tool [15]increases reliability of device drivers by attemptingto prove termination automatically. Full functionalcorrectness of a realistic microkernel is still beyondthe scope of these automatic techniques.

Implementations of kernels in type-safe languagessuch as SPIN [7] and Singularity [23] offer increasedreliability, but they have to rely on traditional “dirty”code to implement their language runtime, whichtends to be substantially bigger than the completeseL4 kernel. While type safety is a good propertyto have, it is not very strong. The kernel may stillmisbehave or attempt, for instance, a null pointeraccess. Instead of randomly crashing, it will report acontrolled exception. In our proof, we show a variantof type safety for the seL4 code. Even though thekernel deliberately breaks the C type system, it onlydoes so in a safe way. Additionally, we prove muchmore: that there will never be any such null pointeraccesses, that the kernel will never crash and thatthe code always behaves in strict accordance withthe abstract specification.

7 ConclusionsWe have presented our experience in formally verify-ing seL4. We have shown that full, rigorous, formalverification is practically achievable for OS microker-nels with very reasonable effort compared to tradi-tional development methods.

Although we have not invested significant effortinto optimisation, we have shown that optimisationsare possible and that performance does not needto be sacrificed for verification. The seL4 kernel ispractical, usable, and directly deployable, runningon ARMv6 and x86.

Collateral benefits of the verification include ourrapid prototyping methodology for kernel design. Weobserved a confluence of design principles from theformal methods and the OS side, leading to designdecisions such as an event-based kernel that is mostlynon-preemptable and uses interrupt polling. Thesedecisions made the kernel design simpler and easier toverify without sacrificing performance. Evidence sug-gests that taking the detour via a Haskell prototypeincreased our productivity even without consideringverification.

Future work in this project includes verification ofthe assembly parts of the kernel, a multi-core ver-sion of the kernel, as well as application verification.The latter now becomes much more meaningful thanpreviously possible: application proofs can rely onthe abstract, formal kernel specification that seL4 isproven to implement.

Compared to the state of the art in software cer-tification, the ultimate degree of trustworthiness wehave achieved is redefining the standard of highestassurance.

AcknowledgementsWe thank Timothy Bourke, Timothy Roscoe, andAdam Wiggins for valued feedback on drafts of thisarticle. We also would like to acknowledge the contri-bution of the former team members on this verifica-tion project: Jeremy Dawson, Jia Meng, CatherineMenon, and David Tsai.

NICTA is funded by the Australian Governmentas represented by the Department of Broadband,Communications and the Digital Economy and theAustralian Research Council through the ICT Centreof Excellence program.

References[1] M. Accetta, R. Baron, W. Bolosky, D. Golub,

R. Rashid, A. Tevanian, and M. Young. Mach: Anew kernel foundation for UNIX development. In1986 Summer USENIX, pages 93–112, 1986.

[2] E. Alkassar, M. Hillebrand, D. Leinenbach,N. Schirmer, A. Starostin, and A. Tsyban. Bal-

ancing the load — leveraging a semantics stack forsystems verification. JAR, 42(2–4), 2009.

[3] E. Alkassar, N. Schirmer, and A. Starostin. Formalpervasive verification of a paging mechanism. InC. R. Ramakrishnan and J. Rehof, editors, Tools andAlg. for the Construction and Analysis of Systems(TACAS), volume 4963 of LNCS, pages 109–123.Springer, 2008.

[4] J. Alves-Foss, P. W. Oman, C. Taylor, and S. Har-rison. The MILS architecture for high-assuranceembedded systems. Int. J. Emb. Syst., 2:239–247,2006.

[5] M. Archer, E. Leonard, and M. Pradella. Analyzingsecurity-enhanced Linux policy specifications. InPOLICY ’03: Proc. 4th IEEE Int. WS on Policiesfor Distributed Systems and Networks, pages 158–169. IEEE Computer Society, 2003.

[6] T. Ball and S. K. Rajamani. SLIC: A specificationlanguage for interface checking. Technical ReportMSR-TR-2001-21, Microsoft Research, 2001.

[7] B. N. Bershad, S. Savage, P. Pardyak, E. G. Sirer,M. E. Fiuczynski, D. Becker, C. Chambers, andS. Eggers. Extensibility, safety and performancein the SPIN operating system. In 15th SOSP, Dec1995.

[8] W. R. Bevier. Kit: A study in operating systemverification. IEEE Transactions on Software Engi-neering, 15(11):1382–1396, 1989.

[9] W. R. Bevier and L. Smith. A mathematical modelof the Mach kernel: Atomic actions and locks. Tech-nical Report 89, Computational Logic Inc., Apr1993.

[10] I. T. Bowman, R. C. Holt, and N. V. Brewster. Linuxas a case study: its extracted software architecture.In ICSE ’99: Proc. 21st Int. Conf. on SoftwareEngineering, pages 555–563. ACM, 1999.

[11] A. Boyton. A verified shared capability model. InG. Klein, R. Huuck, and B. Schlich, editors, 4thWS Syst. Softw. Verification SSV’09, ENTCS, pages99–116. Elsevier, Jun 2009.

[12] P. Brinch Hansen. The nucleus of a multiprogram-ming operating system. CACM, 13:238–250, 1970.

[13] D. Cock. Bitfields and tagged unions in C: Verifica-tion through automatic generation. In B. Beckertand G. Klein, editors, VERIFY’08, volume 372 ofCEUR Workshop Proceedings, pages 44–55, Aug2008.

[14] D. Cock, G. Klein, and T. Sewell. Secure micro-kernels, state monads and scalable refinement. InO. A. Mohamed, C. Munoz, and S. Tahar, editors,21st TPHOLs, volume 5170 of LNCS, pages 167–182.Springer, Aug 2008.

[15] B. Cook, A. Gotsman, A. Podelski, A. Rybalchenko,and M. Y. Vardi. Proving that programs eventuallydo something good. In 34th POPL. ACM, 2007.

[16] J. Criswell, A. Lenharth, D. Dhurjati, and V. Adve.Secure virtual architecture: A safe execution envi-ronment for commodity operating systems. In 16thSOSP, pages 351–366, Oct 2007.

[17] U. Dannowski. Personal communication.

[18] W.-P. de Roever and K. Engelhardt. Data Refine-ment: Model-Oriented Proof Methods and their Com-parison. Number 47 in Cambridge Tracts in The-oretical Computer Science. Cambridge UniversityPress, 1998.

[19] P. Derrin, K. Elphinstone, G. Klein, D. Cock, andM. M. T. Chakravarty. Running the manual: An ap-proach to high-assurance microkernel development.In ACM SIGPLAN Haskell WS, Sep 2006.

[20] D. Elkaduwe, P. Derrin, and K. Elphinstone. Kerneldesign for isolation and assurance of physical mem-ory. In 1st IIES, pages 35–40. ACM SIGOPS, Apr2008.

[21] D. Elkaduwe, G. Klein, and K. Elphinstone. Veri-fied protection model of the seL4 microkernel. InJ. Woodcock and N. Shankar, editors, VSTTE 2008

— Verified Softw.: Theories, Tools & Experiments,volume 5295 of LNCS, pages 99–114. Springer, Oct2008.

[22] K. Elphinstone, G. Klein, P. Derrin, T. Roscoe, andG. Heiser. Towards a practical, verified kernel. In11th HotOS, pages 117–122, May 2007.

[23] M. Fahndrich, M. Aiken, C. Hawblitzel, O. Hodson,G. C. Hunt, J. R. Larus, and S. Levi. Languagesupport for fast and reliable message-based commu-nication in Singularity OS. In 1st EuroSys Conf.,pages 177–190, Apr 2006.

[24] R. J. Feiertag and P. G. Neumann. The foundationsof a provably secure operating system (PSOS). InAFIPS Conf. Proc., 1979 National Comp. Conf.,Jun 1979.

[25] B. Ford, M. Hibler, J. Lepreau, R. McGrath, andP. Tullmann. Interface and execution models in theFluke kernel. In 3rd OSDI. USENIX, Feb 1999.

[26] T. Garfinkel, B. Pfaff, J. Chow, M. Rosenblum, andD. Boneh. Terra: A virtual machine-based platformfor trusted computing. In 19th SOSP, Oct 2003.

[27] Green Hills Software, Inc. INTEGRITY-178B separation kernel security target version1.0. http://www.niap-ccevs.org/cc-scheme/st/stvid10119-st.pdf, 2008.

[28] Greenhills Software, Inc. Integrity real-time oper-ating system. http://www.ghs.com/products/rtos/integrity.html, 2008.

[29] J. D. Guttman, A. L. Herzog, J. D. Ramsdell, andC. W. Skorupka. Verifying information flow goalsin security-enhanced Linux. Journal of ComputerSecurity, 13(1):115–134, 2005.

[30] J. T. Haigh and W. D. Young. Extending the non-interference version of MLS for SAT. IEEE Trans.on Software Engineering, 13(2):141–150, 1987.

[31] D. S. Hardin, E. W. Smith, and W. D. Young. Arobust machine code proof framework for highlysecure applications. In ACL2’06: Proc. Int. WS onthe ACL2 theorem prover and its applications. ACM,2006.

[32] G. Heiser. Hypervisors for consumer electronics. In6th IEEE CCNC, 2009.

[33] C. L. Heitmeyer, M. Archer, E. I. Leonard, andJ. McLean. Formal specification and verificationof data separation in a separation kernel for anembedded system. In CCS ’06: Proc. 13th Conf.on Computer and Communications Security, pages346–355. ACM, 2006.

[34] T. A. Henzinger, R. Jhala, R. Majumdar, andG. Sutre. Software verification with Blast. InSPIN’03, Workshop on Model Checking Software,2003.

[35] M. Hohmuth, M. Peter, H. Hartig, and J. S. Shapiro.Reducing TCB size by using untrusted components

— small kernels versus virtual-machine monitors. In11th SIGOPS Eur. WS, Sep 2004.

[36] M. Hohmuth and H. Tews. The VFiasco approachfor a verified operating system. In 2nd PLOS, Jul2005.

[37] Iguana. http://www.ertos.nicta.com.au/software/kenge/iguana-project/latest/.

[38] Information Assurance Directorate. U.S. Govern-ment Protection Profile for Separation Kernels inEnvironments Requiring High Robustness, Jun 2007.Version 1.03. http://www.niap-ccevs.org/cc-scheme/pp/pp.cfm/id/pp skpp hr v1.03/.

[39] ISO/IEC. Programming languages — C. Techni-cal Report 9899:TC2, ISO/IEC JTC1/SC22/WG14,May 2005.

[40] G. Klein. Operating system verification — anoverview. Sadhana, 34(1):27–69, Feb 2009.

[41] G. Klein, P. Derrin, and K. Elphinstone. Expe-rience report: seL4 — formally verifying a high-performance microkernel. In 14th ICFP, Aug 2009.

[42] R. Kolanski and G. Klein. Types, maps and sepa-ration logic. In S. Berghofer, T. Nipkow, C. Urban,and M. Wenzel, editors, Proc. TPHOLs’09, volume5674 of LNCS. Springer, 2009.

[43] L4HQ. http://l4hq.org/arch/arm/.

[44] X. Leroy. Formal certification of a compiler back-end,or: Programming a compiler with a proof assistant.In J. G. Morrisett and S. L. P. Jones, editors, 33rdPOPL, pages 42–54. ACM, 2006.

[45] J. Liedtke. Improving IPC by kernel design. In 14thSOSP, pages 175–188, Dec 1993.

[46] J. Liedtke. Towards real microkernels. CACM,39(9):70–77, Sep 1996.

[47] J. Liedtke, K. Elphinstone, S. Schonberg, H. Hartig,G. Heiser, N. Islam, and T. Jaeger. Achieved IPCperformance (still the foundation for extensibility).In 6th HotOS, pages 28–31, May 1997.

[48] W. B. Martin, P. White, A. Goldberg, and F. S.Taylor. Formal construction of the mathematicallyanalyzed separation kernel. In ASE ’00: Proc. 15thIEEE Int. Conf. on Automated software engineering,pages 133–141. IEEE Computer Society, 2000.

[49] Z. Ni, D. Yu, and Z. Shao. Using XCAP to certifyrealistic system code: Machine context management.In Proc. TPHOLs’07, volume 4732 of LNCS, pages189–206. Springer, Sep 2007.

[50] T. Nipkow, L. Paulson, and M. Wenzel. Is-abelle/HOL — A Proof Assistant for Higher-OrderLogic, volume 2283 of LNCS. Springer, 2002.

[51] OKL4 web site. http://okl4.org.

[52] T. Perrine, J. Codd, and B. Hardy. An overviewof the kernelized secure operating system (KSOS).In Proceedings of the Seventh DoD/NBS ComputerSecurity Initiative Conference, pages 146–160, Sep1984.

[53] Rockwell Collins, Inc. AAMP7r1 Reference Manual,2003.

[54] J. M. Rushby. Design and verification of securesystems. In 8th SOSP, pages 12–21, 1981.

[55] O. Saydjari, J. Beckman, and J. Leaman. Lockingcomputers securely. In 10th National ComputerSecurity Conference, pages 129–141, Sep 1987.

[56] A. Seshadri, M. Luk, N. Qu, and A. Perrig. SecVisor:A tiny hypervisor to provide lifetime kernel codeintegrity for commodity OSes. In 16th SOSP, pages335–350, Oct 2007.

[57] J. S. Shapiro, D. F. Faber, and J. M. Smith. Statecaching in the EROS kernel—implementing efficientorthogonal peristence in a pure capability system.In 5th IWOOOS, pages 89–100, Nov 1996.

[58] J. S. Shapiro, J. M. Smith, and D. J. Farber. EROS:A fast capability system. In 17th SOSP, Dec 1999.

[59] L. Singaravelu, C. Pu, H. Hartig, and C. Helmuth.Reducing TCB complexity for security-sensitive ap-plications: Three case studies. In 1st EuroSys Conf.,pages 161–174, Apr 2006.

[60] R. Spencer, S. Smalley, P. Loscocco, M. Hibler,D. Andersen, and J. Lepreau. The Flask securityarchitecture: System support for diverse securitypolicies. In 8th USENIX Security Symp., Aug 1999.

[61] H. Tews, T. Weber, and M. Volp. A formal modelof memory peculiarities for the verification of low-level operating-system code. In R. Huuck, G. Klein,and B. Schlich, editors, Proc. 3rd Int. WS on Sys-tems Software Verification (SSV’08), volume 217 ofENTCS, pages 79–96. Elsevier, Feb 2008.

[62] H. Tuch. Formal Memory Models for Verifying CSystems Code. PhD thesis, UNSW, Aug 2008.

[63] H. Tuch. Formal verification of C systems code:Structured types, separation logic and theorem prov-ing. JAR, 42(2–4):125–187, 2009.

[64] H. Tuch, G. Klein, and G. Heiser. OS verification— now! In 10th HotOS, pages 7–12. USENIX, Jun2005.

[65] H. Tuch, G. Klein, and M. Norrish. Types, bytes, andseparation logic. In M. Hofmann and M. Felleisen,editors, 34th POPL, pages 97–108, Jan 2007.

[66] US National Institute of Standards. Common Crite-ria for IT Security Evaluation, 1999. ISO Standard15408. http://csrc.nist.gov/cc/.

[67] B. J. Walker, R. A. Kemmerer, and G. J. Popek.Specification and verification of the UCLA Unixsecurity kernel. CACM, 23(2):118–131, 1980.

[68] D. A. Wheeler. SLOCCount. http://www.dwheeler.com/sloccount/, 2001.

[69] A. Whitaker, M. Shaw, and S. D. Gribble. Scaleand performance in the Denali isolation kernel. In5th OSDI, Dec 2002.

[70] S. Winwood, G. Klein, T. Sewell, J. Andronick,D. Cock, and M. Norrish. Mind the gap: A verifi-cation framework for low-level C. In S. Berghofer,T. Nipkow, C. Urban, and M. Wenzel, editors, Proc.TPHOLs’09, volume 5674. Springer, 2009.

[71] W. Wulf, E. Cohen, W. Corwin, A. Jones, R. Levin,C. Pierson, and F. Pollack. HYDRA: The kernel ofa multiprocessor operating system. CACM, 17:337–345, 1974.