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 , 4 ANU [email protected]

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

1 to provide strong security guarantees. also propagated via IPC to support virtualisation. To our knowledge, seL4 is the first-ever general- IPC uses synchronous and asynchronous endpoints purpose OS kernel that is fully formally verified for (port-like destinations without in-kernel buffering) functional correctness. As such, it is a platform of for inter- communication, with RPC facilitated unprecedented trustworthiness, which will allow the via reply capabilities. Capabilities are segregated and construction of highly secure and reliable systems on stored in capability address spaces composed of ca- top. pability container objects called CNodes. The functional-correctness property we prove for As in traditional L4 kernels, seL4 device drivers run seL4 is much stronger and more precise than what as normal user-mode applications that have access automated techniques like model checking, static to device registers and memory either by mapping analysis or kernel implementations in type-safe lan- the device into the virtual address space, or by con- guages can achieve. We not only analyse specific trolled access to device ports on Intel hardware. aspects of the kernel, such as safe execution, but also seL4 provides a mechanism to receive notification of provide a full specification and proof for the kernel’s (via IPC) and acknowledge their receipt. precise behaviour. in seL4 is explicit: both in- We have created a methodology for rapid kernel de- kernel objects and virtual address spaces are pro- sign and implementation that is a fusion of traditional tected and managed via capabilities. Physical mem- operating systems and formal methods techniques. ory is initially represented by untyped capabilities, We found that our verification focus improved the which can be subdivided or retyped into kernel objects design and was surprisingly often not in conflict with such as page tables, thread control blocks, CNodes, achieving performance. endpoints, and frames (for mapping in virtual ad- In this paper, we present the design of seL4, discuss dress spaces). The model guarantees all memory the methodologies we used, and provide an overview allocation in the kernel is explicit and authorised. of the approach used in the formal verification from Initial development of seL4, and all verification high-level specification to the C implementation. We work, was done for an ARMv6-based platform, with also discuss the lessons we have learnt from the a subsequent port of the kernel (so far without proof) project, and the implications for similar efforts. to x86. The remainder of this paper is structured as fol- 2.2 Kernel design lows. In Sect. 2, we give an overview of seL4, of the OS developers tend to take a bottom-up approach to design approach, and of the verification approach. In kernel design. High performance is obtained by man- Sect. 3, we describe how to design a kernel for formal aging the hardware efficiently, which leads to designs verification. In Sect. 4, we describe precisely what motivated by low-level details. In contrast, formal we verified, and identify the assumptions we make. methods practitioners tend toward top-down design, In Sect. 5, we describe important lessons we learnt as proof tractability is determined by system com- in this project, and in Sect. 6, we contrast our effort plexity. This leads to designs based on simple models with related work. with a high degree of abstraction from hardware. As a compromise that blends both views, we 2 Overview adopted an approach [19, 22] based around an in- 2.1 seL4 programming model termediate target that is readily accessible by both This paper is primarily about the formal verification OS developers and formal methods practitioners. It of the seL4 kernel, not its API design. We therefore uses the functional programming language Haskell to provide only a brief overview of its main characteris- provide a programming language for OS developers, tics. while at the same time providing an artefact that can seL4 [20], similarly to projects at Johns Hopkins be automatically translated into the theorem proving (Coyotos) and Dresden (Nova), is a third-generation tool and reasoned about. microkernel, and is broadly based on L4 [46] and Fig. 1 shows our approach in more detail. The influenced by EROS [58]. It features abstractions for square boxes are formal artefacts that have a direct virtual address spaces, threads, inter-process commu- role in the proof. The double arrows represent imple- nication (IPC), and, unlike most L4 kernels, capabili- mentation or proof effort, the single arrows represent ties for authorisation. Virtual address spaces have no design/implementation influence of artefacts on other kernel-defined structure; page faults are propagated artefacts. The central artefact is the Haskell proto- via IPC to pager threads, responsible for defining type of the kernel. The prototype requires the design the address space by mapping frames into the virtual and implementation of algorithms that manage the space. Exceptions and non-native system calls are low-level hardware details. To execute the Haskell Design Cycle Isabelle/HOL Design Abstract Specification

Hardware Haskell Executable Specification Haskell Prototype Formal Executable Spec Simulator + Prototype

Manual Proof Implementation Automatic Translation High-Performance C Implementation Refinement Proof User Programs High-Performance C Implementation

Figure 1: The seL4 design process Figure 2: The refinement layers in the verification of seL4 prototype in a near-to-realistic setting, we link it with (derived from QEMU) that simulates opportunities to micro-optimise the kernel, which is the hardware platform. Normal user-level execution required for adequate microkernel performance. is enabled by the simulator, while traps are passed 2.3 Formal verification to the kernel model which computes the result of The technique we use for formal verification is inter- the trap. The prototype modifies the user-level state active, machine-assisted and machine-checked proof. of the simulator to appear as if a real kernel had Specifically, we use the theorem prover Isabelle/HOL executed in privileged mode. [50]. Interactive theorem proving requires human This arrangement provides a prototyping environ- intervention and creativity to construct and guide ment that enables low-level design evaluation from the proof. However, it has the advantage that it is both the user and kernel perspective, including low- not constrained to specific properties or finite, feasi- level physical and management. It ble state spaces, unlike more automated methods of also provides a realistic execution environment that is verification such as static analysis or model checking. binary-compatible with the real kernel. For example, The property we are proving is functional correct- we ran a subset of the Iguana embedded OS [37] on ness in the strongest sense. Formally, we are showing the simulator-Haskell combination. The alternative refinement [18]: A refinement proof establishes a of producing the executable specification directly correspondence between a high-level (abstract) and in the theorem prover would have meant a steep a low-level (concrete, or refined) representation of a learning curve for the design team and a much less system. sophisticated tool chain for execution and simulation. The correspondence established by the refinement We restrict ourselves to a subset of Haskell that proof ensures that all Hoare logic properties of the can be automatically translated into the language abstract model also hold for the refined model. This of the theorem prover we use. For instance, we do means that if a security property is proved in Hoare not make any substantial use of laziness, make only logic about the abstract model (not all security prop- restricted use of type classes, and we prove that all erties can be), refinement guarantees that the same functions terminate. The details of this subset are property holds for the kernel source code. In this described elsewhere [19, 41]. paper, we concentrate on the general functional cor- While the Haskell prototype is an executable model rectness property. We have also modelled and proved and implementation of the final design, it is not the the security of seL4’s access-control system in Is- final production kernel. We manually re-implement abelle/HOL on a high level. This is described else- the model in the C programming language for several where [11, 21], and we have not yet connected it to reasons. Firstly, the Haskell runtime is a significant the proof presented here. body of code (much bigger than our kernel) which Fig. 2 shows the specification layers used in the would be hard to verify for correctness. Secondly, the verification of seL4; they are related by formal proof. Haskell runtime relies on garbage collection which is Sect. 4 explains the proof and each of these layers in unsuitable for real-time environments. Incidentally, detail; here we give a short summary. the same arguments apply to other systems based on The top-most layer in the picture is the abstract type-safe languages, such as SPIN [7] and Singular- specification: an operational model that is the main, ity [23]. Additionally, using C enables optimisation of complete specification of system behaviour. The the low-level implementation for performance. While abstract level contains enough detail to specify the an automated translation from Haskell to C would outer interface of the kernel, .g., how system-call have simplified verification, we would have lost most arguments are encoded in binary form, and it de- scribes in abstract logical terms the effect of each can modify, and the complexity of the properties the system call or what happens when an or pre- and post-conditions express. Around 80 % of the VM fault occurs. It does not describe in detail how properties we show relate to preserving invariants. these effects are implemented in the kernel. To make verification of the kernel feasible, its design The next layer down in Fig. 2 is the executable should minimise the complexity of these components. specification generated from Haskell into the theo- Ideally, the kernel code (and associated proofs) would rem prover. The translation is not correctness-critical consist of simple statements that rely on explicit because we seek assurance about the generated def- local state, with simple invariants. These smaller initions and ultimately C, not the Haskell source, elements could then be composed abstractly into which only serves as an intermediate prototype. The larger elements that avoid exposing underlying local executable specification contains all data structure elements. Unfortunately, OS kernels are not usually and implementation details we expect the final C structured like this, and generally feature highly inter- implementation to have. dependent subsystems [10]. Finally, the bottom layer in this verification effort As a consequence of our design goal of suitability is the high-performance C implementation of seL4. for real-life use, our kernel design attempts to min- For programs to be formally verified they must have imise the proof complexity without compromising formally-defined semantics. One of the achievements performance. In this light we will now examine typi- of this project is a very exact and faithful formal cal properties of kernels and discuss their effect on semantics for a large subset of the C programming verification, including presenting specific features of language [62]. Even with a formal semantics of C in our kernel design. the logic of the theorem prover, we still have to read 3.1 Global variables and side effects and translate the specific program into the prover. Programming with global variables and with side This is discussed in Sect. 4.3. effects is common in operating systems kernels and Verification can never be absolute; it always must our verification technique has no problem dealing make fundamental assumptions. In our work we with them. However, implicit state updates and stop at the source-code level, which implies that we complex use of the same global for different purposes assume at least the compiler and the hardware to be can make verification harder than necessary. correct. Global variables usually require stating and proving An often-raised concern is the question of proof invariant properties. For example, if global scheduler correctness. More than 30 years of research in the- queues are implemented as doubly-linked lists, the orem proving has addressed this issue, and we can corresponding invariant might state that all back now achieve a degree of trustworthiness of formal, links in the list point to the appropriate nodes and machine-checked proof that far surpasses the confi- that all elements point to thread control blocks. In- dence levels we rely on in engineering or mathematics variants are expensive because they need to be proved for our daily survival. We use two specific techniques: not only locally for the functions that directly manip- firstly, we work foundationally from first principles; ulate the scheduler queue, but for the whole kernel— mathematics, semantics, and Hoare logic are not ax- we have to show that no other pointer manipulation iomatised, but defined and proved. Secondly, the in the kernel accidentally destroys the list or its prop- Isabelle theorem prover we are using can produce erties. This proof can be easy or hard, depending on external proof representations that can be indepen- how modularly the global variable is used. dently checked by a small, simple proof checker. A hypothetical, problematic example would be a 3 Kernel Design for Verification complex, overloaded page-table data structure that The main body of the correctness proof can be can represent translation, validity of memory regions, thought of as showing Hoare triples on program state- copy-on-write, zero-on-demand memory, and the lo- ments and on functions in each of the specification cation of data in swap space, combined with a re- levels. The proof in our refinement and Hoare logic lationship to the frame table. This would create a framework decomposes along function boundaries. large, complex invariant for each of the involved data Each unit of proof has a set of preconditions that structures, and each of the involved operations would need to hold prior to execution, a statement or se- have to preserve all of it. quence of statements in a function that modify the The treatment of globals becomes especially diffi- system state, and the post-conditions that must hold cult if invariants are temporarily violated. For ex- afterwards. The degree of difficulty in showing that ample, adding a new node to a doubly-linked list pre- and post-conditions hold is directly related to the temporarily violates invariants that the list is well complexity of the statement, the state the statement formed. Larger execution blocks of unrelated code, as in or interrupts, should be avoided gree of interleaving of execution and non-determinism during that violation. We address these issues by can be controlled. However, even on a uniprocessor limiting preemption points, and by deriving the code there is some remaining concurrency resulting from from Haskell, thus making side effects explicit and asynchronous I/O devices. seL4 avoids much of the bringing them to the attention of the design team. complications resulting from I/O by running device drivers at user level, but it must still address inter- 3.2 Kernel memory management rupts. The seL4 kernel uses a model of memory allocation Consider the small code fragment A; X; B, where that exports control of the in-kernel allocation to ap- A must establish the state that X relies on, X must propriately authorised applications [20]. While this establish the state B relies on, and so on. Concur- model is mostly motivated by the need for precise rency issues in the verification of this code arise from guarantees of memory consumption, it also benefits yielding, interrupts and exceptions. verification. The model pushes the policy for allo- Yielding at X results in the potential execution of cation outside of the kernel, which means we only any reachable activity in the system. This implies need to prove that the mechanism works, not that A must establish the preconditions required for all the user-level policy makes sense. Obviously, mov- reachable activities, and all reachable activities on ing it into userland does not change the fact that return must establish the preconditions of B. Yielding the memory-allocation module is part of the trusted increases complexity significantly and makes verifica- computing base. It does mean, however, that such a tion harder. Preemption is a non-deterministically module can be verified separately, and can rely on optional yield. Blocking kernel primitives, such as in verified kernel properties. lock acquisition and waiting on condition variables, The correctness of the allocation algorithm involves are also a form of non-deterministic yield. checks that new objects are wholly contained within By design, we side-step addressing the verification an untyped (free) memory region and that they do complexity of yield by using an event-based kernel not overlap with any other objects allocated from the execution model, with a single kernel stack, and a region. Our memory allocation model keeps track of mostly atomic application programming interface capability derivations in a tree-like structure, whose [25]. nodes are the capabilities themselves. Interrupt complexity has two forms: non- Before re-using a block of memory, all references to deterministic execution of the interrupt handlers, this memory must be invalidated. This involves either and interrupt handling resulting in preemption (as a finding all outstanding capabilities to the object, or result of timer ticks). Theoretically, this complexity returning the object to the memory pool only when can be avoided by disabling interrupts during kernel the last capability is deleted. Our kernel uses both execution. However, this would be at the expense approaches. of large or unbounded interrupt , which we In the first approach, the capability derivation tree consider unacceptable. is used to find and invalidate all capabilities referring Instead, we run the kernel with interrupts mostly to a memory region. In the second approach, the disabled, except for a small number of carefully- capability derivation tree is used to ensure, with a placed interrupt points. If, in the above code frag- check that is local in scope, that there are no system- ment, is the interrupt point, must establish the wide dangling references. This is possible because X A state that all interrupt handlers rely on, and all reach- all other kernel objects have further invariants on able interrupt handlers must establish or preserve their own internal references that relate back to the the properties relies on. existence of capabilities in this derivation tree. B We simplify the problem further by implementing 3.3 Concurrency and non-determinism interrupt points via polling, rather than temporary Concurrency is the execution of computation in par- enabling of interrupts. On detection of a pending allel (in the case of multiple hardware processors), or interrupt, we explicitly return through the function by non-deterministic interleaving via a concurrency call stack to the kernel/user boundary. At the bound- abstraction like threads. Proofs about concurrent ary we leave a (potentially modified) event stored programs are hard, much harder than proofs about in the saved user-level registers. The interrupt be- sequential programs. comes a new kernel event (prefixed to the pending While we have some ideas on how to construct verifi- user-triggered event). After the in-kernel compo- able systems on multiprocessors, they are outside the nent of interrupt handling, the interrupted event scope of this paper and left for future work. In this is restarted. This effectively re-tries the (modified) paper we focus on uniprocessor support where the de- operation, including re-establishing all the precondi- tions for execution. In this way we avoid the need support in the proof. The model includes existence for any interrupt-point specific post-conditions for of the controller, masking of interrupts, and that interrupt handlers, but still achieve Fluke-like partial interrupts only occur if unmasked. This is sufficient preemptability [25]. to include interrupt controller access, and basic be- The use of interrupt points creates a trade-off, con- haviour in the proof, without modelling correctness trolled by the kernel designer, between proof com- of the interrupt controller management in detail. The plexity and interrupt processing latency. Almost all proof is set up such that it is easy to include more of seL4’s operations have short and bounded latency, detail in the hardware model should it become nec- and can execute without any interrupt points at all. essary later to prove additional properties. The exception is object destruction, whose cleanup Our kernel contains a single , the timer operations are inherently unbounded, and are, of driver, which generates timer ticks for the scheduler. course, critical to kernel integrity. This is set up in the initialisation phase of the ker- We make these operations preemptable by storing nel as an automatically reloaded source of regular the state of progress of destruction in the last capabil- interrupts. It is not modified or accessed during the ity referencing the object being destroyed; we refer to execution of the kernel. We did not need to model this as a zombie capability. This guarantees that the the timer explicitly in the proof, we just prove that correctness of a restarted destroy is not dependent on system behaviour on each tick event is correct. user-accessible registers. Another advantage of this 3.5 Observations approach is that if another user thread attempts to The requirements of verification force the designers to destroy the zombie, it will simply continue where the think of the simplest and cleanest way of achieving first thread was preempted (a form of priority inheri- their goals. We found repeatedly that this leads tance), instead of making the new thread dependent to overall better design, which tends to reduce the (blocked) on the completion of another. likelihood of bugs. Exceptions are similar to interrupts in their effect, In a number of cases there were significant other but are synchronous in that they result directly from benefits. This is particularly true for the design deci- the code being executed and cannot be deferred. sions aimed at simplifying concurrency-related verifi- In the seL4 kernel we avoid exceptions completely cation issues. Non-preemptable execution (except for and much of that avoidance is guaranteed as a side- a few interrupt-points) has traditionally been used effect of verification. Special care is required only for in L4 kernels to maximise average-case performance. memory faults. Recent L4 kernels aimed at embedded use [32] have We avoid having to deal with virtual-memory ex- adopted an event-based design to reduce the kernel’s ceptions in kernel code by mapping a fixed region of memory footprint (due to the use of a single kernel the virtual address space to physical memory, inde- stack rather than per-thread stacks). pendent of whether it is actively used or not. The region contains all the memory the kernel can poten- 4 seL4 Verification tially use for its own internal data structures, and is This section describes each of the specification layers guaranteed to never produce a fault. We prove that as well as the proof in more detail. this region appears in every virtual address space. 4.1 Abstract specification Arguments passed to the kernel from user level are The abstract level describes what the system does either transferred in registers or limited to prereg- without saying how it is done. For all user-visible ker- istered physical frames accessed through the kernel nel operations it describes the functional behaviour region. that is expected from the system. All implemen- 3.4 I/O tations that refine this specification will be binary As described earlier we avoid most of the complexity compatible. of I/O by moving device drivers into protected user- We precisely describe argument formats, encodings mode components. When processing an interrupt and error reporting, so for instance some of the C- event, our interrupt delivery mechanism determines level size restrictions become visible on this level. In the interrupt source, masks further interrupts from order to express these, we rarely make use of infinite that specific source, notifies the registered user-level types like natural numbers. Instead, we use finite handler (device driver) of the interrupt, and unmasks machine words, such as 32-bit integers. We model the interrupt when the handler acknowledges the memory and typed pointers explicitly. Otherwise, interrupt. the data structures used in this abstract specification We coarsely model the hardware interrupt con- are high-level — essentially sets, lists, trees, functions troller of the ARM platform to include interrupt and records. We make use of non-determinism in schedule ≡ do schedule = do threads ← all_active_tcbs; action <- getSchedulerAction thread ← select threads; case action of ChooseNewThread -> do switch_to_thread thread chooseThread od OR switch_to_idle_thread setSchedulerAction ResumeCurrentThread ... Figure 3: Isabelle/HOL code for scheduler at abstract chooseThread = do level. r <- findM chooseThread’ (reverse [minBound .. maxBound]) when (r == Nothing) $ switchToIdleThread order to leave implementation choices to lower levels: chooseThread’ prio = do q <- getQueue prio If there are multiple correct results for an operation, liftM isJust $ findM chooseThread’’ q this abstract layer would return all of them and make chooseThread’’ thread = do runnable <- isRunnable thread clear that there is a choice. The implementation is if not runnable then do free to pick any one of them. tcbSchedDequeue thread return False An example of this is . No schedul- else do ing policy is defined at the abstract level. Instead, switchToThread thread return True the scheduler is modelled as a function picking any runnable thread that is active in the system or the Figure 4: Haskell code for schedule. idle thread. The Isabelle/HOL code for this is shown mentions that threads have time slices and it clarifies in Fig. 3. The function all_active_tcbs returns when the idle thread will be scheduled. Note that the abstract set of all runnable threads in the system. priority queues duplicate information that is already Its implementation (not shown) is an abstract logical available (in the form of thread states), in order to predicate over the whole system. The select state- make it available efficiently. They make it easy to ment picks any element of the set. The OR makes a find a runnable thread of high priority. The optimi- non-deterministic choice between the first block and sation will require us to prove that the duplicated switch_to_idle_thread. The executable specifica- information is consistent. tion makes this choice more specific. We have proved that the executable specification 4.2 Executable specification correctly implements the abstract specification. Be- The purpose of the executable specification is to fill cause of its extreme level of detail, this proof alone in the details left open at the abstract level and to already provides stronger design assurance than has specify how the kernel works (as opposed to what it been shown for any other general-purpose OS kernel. does). While trying to avoid the messy specifics of how data structures and code are optimised in C, we 4.3 C implementation reflect the fundamental restrictions in size and code The most detailed layer in our verification is the C structure that we expect from the hardware and the C implementation. The translation from C into Isabelle implementation. For instance, we take care not to use is correctness-critical and we take great care to model more than 64 bits to represent capabilities, exploiting the semantics of our C subset precisely and founda- for instance known alignment of pointers. We do not tionally. Precisely means that we treat C semantics, specify in which way this limited information is laid types, and memory model as the standard prescribes, out in C. for instance with architecture-dependent word size, The executable specification is deterministic; the padding of structs, type-unsafe casting of pointers, only non-determinism left is that of the underlying and arithmetic on addresses. As kernel program- machine. All data structures are now explicit data mers do, we make assumptions about the compiler types, records and lists with straightforward, efficient (GCC) that go beyond the standard, and about the implementations in C. For example the capability architecture used (ARMv6). These are explicit in derivation tree of seL4, modelled as a tree on the the model, and we can therefore detect violations. abstract level, is now modelled as a doubly linked Foundationally means that we do not just axiomatise list with limited level information. It is manipulated the behaviour of C on a high level, but we derive it explicitly with pointer-update operations. from first principles as far as possible. For example, Fig. 4 shows part of the scheduler specification at in our model of C, memory is a primitive function this level. The additional complexity becomes appar- from addresses to bytes without type information or ent in the chooseThread function that is no longer restrictions. On top of that, we specify how types merely a simple predicate, but rather an explicit like unsigned int are encoded, how structures are search backed by data structures for priority queues. laid out, and how implicit and explicit type casts The specification fixes the behaviour of the scheduler behave. We managed to lift this low-level memory to a simple priority-based round-robin algorithm. It model to a high-level calculus that allows efficient, void setPriority(tcb_t *tptr, prio_t prio) { abstract reasoning on the type-safe fragment of the prio_t oldprio; kernel [62, 63, 65]. We generate proof obligations as- if(thread_state_get_tcbQueued(tptr->tcbState)) { oldprio = tptr->tcbPriority; suring the safety of each pointer access and write. ksReadyQueues[oldprio] = They state that the pointer in question must be non- tcbSchedDequeue(tptr, ksReadyQueues[oldprio]); if(isRunnable(tptr)) { null and of the correct alignment. They are typically ksReadyQueues[prio] = easy to discharge. We generate similar obligations tcbSchedEnqueue(tptr, ksReadyQueues[prio]); } for all restrictions the C99 standard demands. else { We treat a very large, pragmatic subset of C99 in thread_state_ptr_set_tcbQueued(&tptr->tcbState, false); the verification. It is a compromise between verifica- } tion convenience and the hoops the kernel program- } tptr->tcbPriority = prio; mers were willing to jump through in writing their } source. The following paragraphs describe what is not in this subset. Figure 5: C code for part of the scheduler. We do not allow the address-of operator & on local small tool that takes a specification and generates C variables, because, for better automation, we make code with the necessary shifting and masking for such the assumption that local variables are separate from bitfields. The tool helps us to easily map structures to the heap. This could be violated if their address page table entries or other hardware-defined memory was available to pass on. It is the most far-reaching layouts. The generated code can be inlined and, after restriction we implement, because it is common to compilation on ARM, the result is more compact use local variable references for return parameters of and faster than GCC’s native bitfields. The tool large types that we do not want to pass on the stack. not only generates the C code, it also automatically We achieved compliance with this requirement by generates Isabelle/HOL specifications and proofs of avoiding reference parameters as much as possible, correctness [13]. and where they were needed, used pointers to global Fig. 5 shows part of the implementation of the variables (which are not restricted). scheduling functionality described in the previous One feature of C that is problematic for verifica- sections. It is standard C99 code with pointers, ar- tion (and programmers) is the unspecified order of rays and structs. The thread_state functions used evaluation in expressions with side effects. To deal in Fig. 5 are examples of generated bitfield accessors. with this feature soundly, we limit how side effects can occur in expressions. If more than one function 4.4 Machine model call occurs within an expression or the expression Programming in C is not sufficient for implementing otherwise depends on global state, a proof obliga- a kernel. There are places where the programmer tion is generated to show that these functions are has to go outside the semantics of C to manipulate side-effect free. This proof obligation is discharged hardware directly. In the easiest case, this is achieved automatically by Isabelle. by writing to memory-mapped device registers, as We do not allow function calls through function for instance with a timer chip; in other cases one has pointers. (We do allow handing the address of a to drop down to assembly to implement the required function to assembler code, e.g. for installing ex- behaviour, as for instance with TLB flushes. ception vector tables.) We also do not allow goto Presently, we do not model the effects of certain statements, or switch statements with fall-through direct hardware instructions because they are too cases. We support C99 compound literals, making it far below the abstraction layer of C. Of these, cache convenient to return structs from functions, and re- and TLB flushes are relevant for the correctness ducing the need for reference parameters. We do not of the code, and we rely on traditional testing for allow compound literals to be lvalues. Some of these these limited number of cases. Higher assurance can restrictions could be lifted easily, but the features be obtained by adding more detail to the machine were not required in seL4. model—we have phrased the machine interface such We did not use unions directly in seL4 and therefore that future proofs about the TLB and cache can be do not support them in the verification (although added with minimal changes. Additionally, required that would be possible). Since the C implementation behaviour can be guaranteed by targeted assertions was derived from a functional program, all unions (e.g., that page-table updates always flush the TLB), in seL4 are tagged, and many structs are packed which would result in further proof obligations. bitfields. Like other kernel implementors, we do The basis of this formal model of the machine is not trust GCC to compile and optimise bitfields the internal state of the relevant devices, collected predictably for kernel code. Instead, we wrote a in one record machine_state. For devices that we configureTimer :: irq => unit machine_m resetTimer :: unit machine_m abstract one M1, it is sufficient to show that for each setCurrentPD :: paddr => unit machine_m transition in M2 that may lead from an initial state setHardwareASID :: hw_asid => unit machine_m 0 invalidateTLB :: unit machine_m s to a set of states s , there exists a corresponding invalidateHWASID :: hw_asid => unit machine_m transition on the abstract side from an abstract state invalidateMVA :: word => unit machine_m 0 cleanCacheMVA :: word => unit machine_m σ to a set σ (they are sets because the machines may cleanCacheRange :: word => word => unit machine_m be non-deterministic). The transitions correspond if cleanCache :: unit machine_m invalidateCacheRange :: word => word => unit machine_m there exists a relation R between the states s and getIFSR :: word machine_m σ such that for each concrete state in s0 there is an getDFSR :: word machine_m 0 getFAR :: word machine_m abstract one in σ that makes R hold between them getActiveIRQ :: (irq option) machine_m again. This has to be shown for each transition with maskInterrupt :: bool => irq => unit machine_m the same overall relation R. For each refinement Figure 6: Machine interface functions. layer in Fig. 2, we have strengthened and varied this proof technique slightly, but the general idea remains model more closely, such as the interrupt controller, the same. Details are published elsewhere [14, 70]. the relevant part in machine_state contains details We now describe the instantiation of this frame- such as which interrupts are currently masked. For work to the seL4 kernel. We have the following types the parts that we do not model, such as the TLB, we of transition in our state machines: kernel transi- leave the corresponding type unspecified, so it can tions, user transitions, user events, idle transitions, be replaced with more details later. and idle events. Kernel transitions are those that Fig. 6 shows our machine interface. The functions are described by each of the specification layers in are all of type X machine_m which restricts any side increasing amount of detail. User transitions are effects to the machine_state component of the sys- specified as non-deterministically changing arbitrary tem. Most of the functions return nothing (type user-accessible parts of the state space. User events unit), but change the state of a device. In the ab- model kernel entry (trap instructions, faults, inter- stract and executable specification, these functions rupts). Idle transitions model the behaviour of the are implemented with maximal non-determinism. idle thread. Finally, idle events are interrupts oc- This means that in the extreme case they may arbi- curring during idle time; other interrupts that occur trarily change their part of the machine state. Even during kernel execution are modelled explicitly and for devices that we model, we are careful to leave as separately in each layer of Fig. 2. much behaviour as possible non-deterministic. The The model of the machine and the model of user less behaviour we prescribe, the less assumptions the programs remain the same across all refinement lay- model makes about the hardware. ers; only the details of kernel behaviour and kernel In the seL4 implementation, the functions in Fig. 6 data structures change. The fully non-deterministic are implemented in C where possible, and otherwise model of the user means that our proof includes all in assembly; we must check (but we do not prove) possible user behaviours, be they benign, buggy, or that the implementations match the assumptions we malicious. make in the levels above. An example is the function Let machine MA denote the system framework in- getIFSR, which on ARM returns the instruction fault stantiated with the abstract specification of Sect. 4.1, status register after a page fault. For this function, let machine ME represent the framework instanti- which is basically a single assembly instruction, we ated with the executable specification of Sect. 4.2, only assume that it does not change the memory and let machine MC stand for the framework in- state of the machine, which is easy to check. stantiated with the C program read into the theorem 4.5 The proof prover. Then we prove the following two, very simple- This section describes the main theorem we have looking theorems: shown and how its proof was constructed. Theorem 1 M refines M . As mentioned, the main property we are inter- E A ested in is functional correctness, which we prove by Theorem 2 M refines M . showing formal refinement. We have formalised this C E property for general state machines in Isabelle/HOL, Therefore, because refinement is transitive, we have and we instantiate each of the specifications in the previous sections into this state-machine framework. Theorem 3 MC refines MA. We have also proved the well-known reduction of refinement to forward simulation, illustrated in Fig. 7: Assumptions The assumptions we make are cor- To show that a concrete state machine M2 refines an rectness of the C compiler, the assembly code level, ed oee usataetemdlb manually by model the substantiate however do We . LCo h enl h hoesaoestate above theorems the kernel; the of kLOC 1.2 idwwihtekre salse neeyaddress every in establishes kernel the which window memory in-kernel of view flat traditional, a assume we used are they assumption the under true only is This rah u eiintknt civ h maximum the achieve to taken decision a but proach, us forces we proof complete. where the be proof and to our principles of first rest from the reason to in different standards is our high memory the means virtual This in-kernel of invariants. treatment and it. properties prove stated to us only oblige argument not consistency does model this our make informally; We VM space. one-to-one constant writes a and through reads kernel performed all are because consistent is semantics, that C our For processors. ARM on TLB the program. C the correctly. of have state not memory do the they on assume we effect and any C, functions from interface ma- called machine are assembly-implemented These the interface. of chine part require- con- are flushing cache TLB ments 4.4, and Sect. colouring, in cache described sistency, As exit. the switch kernel of context on potential correctness the prove and not save/restore do register we that mean level Freescale the on platform. architecture i.MX31 ARMv6 the for and assump- tions standard architecture-specific correct this the reflects makes it accurately that model subset formal C the our the that of to [39], standard according C99 subset ISO/IEC our implements correctly their discuss and below implications. these detail describe more We in layer. exit assumptions specification and each entry in kernel points the between about correspondence up takes which correctness code omit boot/initialisation currently the We of hardware. the and hs r o udmna iiain fteap- the of limitations fundamental not are These by translated is access code and memory In-kernel assembly and hardware the on assumptions The compiler GCC the that assume we level, C the For

State Relation σ s iue7: Figure , Concrete Operation Abstract Operation owr Simulation. Forward M M 1 2 σ s' ′ State Relation hc h enlcnetra nnt op ic the Since loop. infinite an enter can kernel the which eicto,w o ics h rprista are that properties the discuss now we verification, in a nyocrblwtelvlo C. of level the below occur only specifica- can the remaining tion) from Any (deviations complete. errors is implementation Coverage complex a program. massively of C the instead on specification triples abstract that Hoare allows simpler properties as it of expressed as analysis be further statement, can all strong conduct a to us is spec- This abstract the by ification. captured fully is plementation proved. be Assurance [42]. can principles faults first and from access foundationally VM modelled kernel have We that shown architecture. PowerPC also the C for optimising [44] and an compiler code verified assembly Leroy verify interaction. to hardware Verisoft how the showed and [3] success, verification project Ni report switching, [49] context al. For code et boot version. design the earlier of an design in executable we the instance, verified For have resources. available with outcome einrt h eie bu niprativrati the verification. in aid invariant kernel such important the as an devel- and from during about code, message verifier help useful the great to a a convey designer only not also are they they opment, fact, In kernel. in situation possible no is There level. user to never return pointer. kernel misaligned the a that or and pointer null paths, a code accesses all This on true hold. assertions assumptions all our that un- includes as behave long otherwise or as crash expectedly means never This can behaviour. kernel defined the have always and fail both that forward implies for [14] technique simulation proof strengthened our ment, 21]. [11, seL4 is of example model An control property. access the only our for contains relevant that aspects another, abstraction the introduce of also level might higher one even proper- system, specific the operational analyse of the To ties for kernel. precise the be abstrac- of to of behaviour enough level low current is The and tion simpler about. are reason C that to the concepts faster of with size the works the notation, and third same code one the is In specification bugs. abstract of absence classes the whole and abstraction The of of expects. degree the user specifica- is the difference the behaviour that the guarantee is describes not This tion does proof contains. the specification true: the the that precisely bugs has same implementation the that shows only yi ih a hta mlmnainproof implementation an that say might cynic A im- C the of behaviour the that show we Overall, epoe htalkre P al emnt and terminate calls API kernel all that proved We state- correctness implementation the to addition In 1 n ih hn htasrin r onls naverified a in pointless are assertions that think might One aigotie h iiain four of limitations the outlined Having 1 ntekre einare design kernel the in M E and M C never interface from user level to the abstract specification that these dangling references will never be touched. is binary compatible with the final implementation, Our typing invariants are stronger than those one our refinement theorem implies that the kernel does would expect from a standard programming language all argument checking correctly and that it can not be type system. They are context dependent and they subverted by buggy encodings, spurious calls, mali- include value ranges such as using only a certain ciously constructed arguments to system calls, buffer number of bits for hardware address space identifiers overflow attacks or other such vectors from user level. (ASIDs). They also often exclude specific values such All these properties hold with the full assurance of as -1 or 0 as valid values because these are used in machine-checked proof. C to indicate success or failure of the corresponding

As part of the refinement proof between levels MA operations. Typing invariants are usually simple to and ME, we had to show a large number of invariants. state and for large parts of the code, their preser- These invariants are not merely a proof device, but vation can be proved automatically. There are only provide valuable information and assurance in them- two operations where the proof is difficult: removing selves. In essence, they collect information about and retyping objects. Type preservation for these what we know to be true of each data structure in two operations is the main reason for a large number the kernel, before and after each system call, and of other kernel invariants. also for large parts during kernel execution where The third category of invariants are classical data some of these invariants may be temporarily violated structure invariants like correct back links in doubly- and re-established later. The overall proof effort linked lists, a statement that there are no loops in was clearly dominated by invariant proofs, with the specific pointer structures, that other lists are always actual refinement statements between abstract and terminated correctly with NULL, or that data struc- executable specification accounting for at most 20 % ture layout assumptions are interpreted the same of the total effort for that stage. There is not enough way everywhere in the code. These invariants are space in this paper to enumerate all the invariant not especially hard to state, but they are frequently statements we have proved, but we will attempt a violated over short stretches of code and then re- rough categorisation, show a few representatives, and established later — usually when lists are updated give a general flavour. or elements are removed. There are four main categories of invariants in our The fourth and last category of invariants that we proof: low-level memory invariants, typing invariants, identify in our proof are algorithmic invariants that data structure invariants, and algorithmic invariants. are specific to how the seL4 kernel works. These The first two categories could in part be covered are the most complex invariants in our proof and by a type-safe language: low-level memory invariants they are where most of the proof effort was spent. include that there is no object at address 0, that These invariants are either required to prove that kernel objects are aligned to their size, and that they specific optimisations are allowed (e.g. that a check do not overlap. The typing invariants say that each can be left out because the condition can be shown kernel object has a well-defined type and that its to be always true), or they are required to show that references in turn point to objects of the right type. an operation executes safely and does not violate An example would be a capability slot containing a other invariants, especially not the typing invariant. reference to a thread control block (TCB). The invari- Examples of simple algorithmic invariants are that ant would state that the type of the first object is a the idle thread is always in thread state idle, and that capability-table entry and that its reference points to only the idle thread is in this state. Another one is a valid object in memory with type TCB. Intuitively, that the global kernel memory containing kernel code this invariant implies that all reachable, potentially and data is mapped in all address spaces. Slightly used references in the kernel—be it in capabilities, more involved are relationships between the existence kernel objects or other data structures—always point of capabilities and thread states. For instance, if to an object of the expected type. This is a necessary a Reply capability exists to a thread, this thread condition for safe execution: we need to know that must always be waiting to receive a reply. This is pointers point to well-defined and well-structured a non-local property connecting the existence of an data, not garbage. This is also a dynamic property, object somewhere in memory with a particular state because objects can be deleted and memory can be of another object somewhere else. Other invariants re-typed at runtime. Note that the main invariant is formally describe a general symmetry principle that about potentially used references. We do allow some seL4 follows: if an object x has a reference to another references, such as in ARM page table objects, to be object y, then there is a reference in object y that can temporarily left dangling, as long as we can prove be used to find object x directly or indirectly. This fact is exploited heavily in the delete operation to Haskell/C Isabelle Invariants Proof clean up all remaining references to an object before LOC LOC LOP it is deleted. abst. — 4,900 ∼ 75 110,000 . 5,700 13,000 ∼ 80 The reason this delete operation is safe is compli- 55,000 cated. Here is a simplified, high-level view of the impl. 8,700 15,000 0 chain of invariants that show an efficient local pointer Table 1: Code and proof statistics. test is enough to ensure that deletion is globally safe: using the processor cycle counter. (1) If an object is live (contains references to other objects), there exists a capability to it somewhere Our measurement for seL4 is 224 cycles for one-way in memory. (2) If an untyped capability c covers IPC in an optimised C path, which is approaching 1 the performance of optimised assembly-language IPC a sub-region of another capability c2, then c1 must be a descendant of c according to the capability paths for other L4 kernels on ARM processors. This 2 puts seL4 performance into the vicinity of the fastest derivation tree (CDT). (3) If a capability c1 points to a kernel object whose memory is covered by an L4 kernels. At the time of writing, this optimised IPC C path untyped capability c2, then c1 must be a descendant is not yet in the verified code base, but it is within of c2. With these, we have: If an untyped capability has the verifiable fragment of C and is not fundamentally no children in the CDT (a simple pointer comparison different from the rest of the code. according to additional data structure invariants), 5.2 Verification effort then all kernel objects in its region must be non- The overall code statistics are presented in Table 1. live (otherwise there would be capabilities to them, The project was conducted in three phases. First which in turn would have to be children of the un- an initial kernel with limited functionality (no inter- typed capability). If the objects are not live and no rupts, single address space and generic linear page ta- capabilities to them exist, there is no further refer- ble) was designed and implemented in Haskell, while ence in the whole system that could be made unsafe the verification team mostly worked on the verifi- by the type change because otherwise the symmetry cation framework and generic proof libraries. In a principle on references would be violated. Deleting second phase, the verification team developed the ab- the object will therefore preserve the basic typing stract spec and performed the first refinement while and safety properties. Of course we also have to the development team completed the design, Haskell show that deleting the object preserves all the new prototype and C implementation. The third phase invariants we just used as well. consisted of extending the first refinement step to We have proved over 150 invariants on the differ- the full kernel and performing the second refinement. ent specification levels, most are interrelated, many The overall size of the proof, including framework, are complex. All these invariants are expressed as libraries, and generated proofs (not shown in the formulae on the kernel state and are proved to be table) is 200,000 lines of Isabelle script. preserved over all possible kernel executions. The abstract spec took about 4 person months (pm) to develop. About 2 person years (py) went 5 Experience and Lessons Learnt into the Haskell prototype (over all project phases), 5.1 Performance including design, documentation, coding, and testing. IPC performance is the most critical metric for evalu- The executable spec only required setting up the ation in a microkernel in which all interaction occurs translator; this took 3 pm. via IPC. We have evaluated the performance of seL4 The initial C translation was done in 3 weeks, in by comparing IPC performance with L4, which has total the C implementation took about 2 pm, for a a long history of data points to draw upon [47]. total cost of 2.2 py including the Haskell effort. Publicly available performance for the Intel XScale This compares well with other efforts for develop- PXA 255 (ARMv5) is 151 in-kernel cycles for a one- ing a new microkernel from scratch: The Karlsruhe way IPC [43], and our experiments with OKL4 2.1 team reports that, on the back of their experience [51] on the platform we are using (Freescale i.MX31 from building the earlier Hazelnut kernel, the devel- evaluation board based on a 532 MHz ARM1136JF-S opment of the Pistachio kernel cost about 6 py [17]. which is an ARMv6 ISA) produced 206 cycles as a SLOCCount [68] with the “embedded” profile esti- point for comparison—this number was produced mates the total cost of seL4 at 4 py. Hence, there is using a hand-crafted assembly-language path. The strong evidence that the detour via Haskell did not non-optimised C version in the same kernel took 756 increase the cost, but was in fact a significant net cycles. These are hot-cache measurements obtained cost saver. This means that our development process can be highly recommended even for projects not and resulted in 54 further changes to the code to considering formal verification. aid in the proof. None of the bugs found in the C The cost of the proof is higher, in total about 20 py. verification stage were deep in the sense that the This includes significant research and about 9 py corresponding algorithm was flawed. This is because invested in formal language frameworks, proof tools, the C code was written according to a very precise, proof automation, theorem prover extensions and low-level specification which was already verified in libraries. The total effort for the seL4-specific proof the first refinement stage. Algorithmic bugs found was 11 py. in the first stage were mainly missing checks on user We expect that re-doing a similar verification for supplied input, subtle side effects in the middle of an a new kernel, using the same overall methodology, operation breaking global invariants, or over-strong would reduce this figure to 6 py, for a total (kernel assumptions about what is true during execution. plus proof) of 8 py. This is only twice the SLOCCount The bugs discovered in the second proof from exe- estimate for a traditionally-engineered system with cutable spec to C were mainly typos, misreading the no assurance. It certainly compares favourable to specification, or failing to update all relevant code industry rules-of-thumb of $10k/LOC for Common parts for specification changes. Simple typos also Criteria EAL6 certification, which would be $87M made up a surprisingly large fraction of discovered for seL4, yet provides far less assurance than formal bugs in the relatively well tested executable specifica- verification. tion in the first refinement proof, which suggests that The breakdown of effort between the two refinement normal testing may not only miss hard and subtle stages is illuminating: The first refinement step (from bugs, but also a larger number of simple, obvious abstract to executable spec) consumed 8 py, the faults than one may expect. Even though their cause second (to concrete spec) less than 3 py, almost a was often simple, understandable human error, their 3:1 breakdown. This is a reflection of the low-level effect in many cases was sufficient to crash the kernel nature of our Haskell implementation, which captures or create security vulnerabilities. Other more inter- most of the properties of the final product. This is esting bugs found during the C implementation proof also reflected in the proof size—the first proof step were missing exception case checking, and different contained most of the deep semantic content. 80 % of interpretations of default values in the code. For the effort in the first refinement went into establishing example, the interrupt controller on ARM returns invariants, only 20 % into the actual correspondence 0xFF to signal that no interrupt is active which is proof. We consider this asymmetry a significant used correctly in most parts of the code, but in one benefit, as the executable spec is more convenient place the check was against NULL instead. and efficient to reason about than the C level. Our The C verification also lead to changes in the exe- formal refinement framework for C made it possible to cutable and abstract specifications: 44 of these were avoid proving any invariants on the C code, speeding to make the proof easier; 34 were implementation up this stage of the proof significantly. We proved restrictions, such as the maximum size of virtual only few additional invariants on the executable spec address space identifiers, which the specifications layer to substantiate optimisations in C. should make visible to the user. The first refinement step lead to some 300 changes 5.3 The cost of change in the abstract spec and 200 in the executable spec. An obvious issue of verification is the cost of proof About 50 % of these changes relate to bugs in the maintenance: how much does it cost to re-verify after associated algorithms or design, the rest were intro- changes made to the kernel? It obviously depends on duced for verification convenience. The ability to the nature of the change, specifically the amount of change and rearrange code in discussion with the code it changes, the number of invariants it affects, design team (to predict performance impact) was an and how localised it is. We are not able to quantify important factor in the verification team’s produc- such costs, but our iterative approach to verification tivity. It is a clear benefit of the approach described has provided us with some relevant experience. in Sect. 2.2 and was essential to complete the verifi- The best case are local, low-level code changes, typ- cation in the available time. ically optimisations that do not affect the observable By the time the second refinement started, the ker- behaviour. We made such changes repeatedly, and nel had been used by a number of internal student found that the effort for re-verification was always low projects and the x86 port was underway. Those two and roughly proportional to the size of the change. activities uncovered 16 defects in the implementa- Adding new, independent features, which do not tion before verification had started in earnest, the interact in a complex way with existing features, usu- formal verification has uncovered another 144 defects ally has a moderate effect. For example, adding a new system call to the seL4 API that atomically batches main property to prove. The UCLA project man- a specific, short sequence of existing system calls aged to finish 90 % of their specification and 20 % of took one day to design and implement. Adjusting their proofs in 5 py. They concluded that invariant the proof took less than 1 pw. reasoning dominated the proof effort, which we found Adding new, large, cross-cutting features, such as confirmed in our project. adding a complex new data structure to the kernel PSOS was mainly focussed on formal kernel design supporting new API calls that interact with other and never completed any substantial implementation parts of the kernel, is significantly more expensive. proofs. Its design methodology was later used for the We experienced such a case when progressing from Kernelized Secure (KSOS) [52] by the first to the final implementation, adding inter- Ford Aerospace. The Secure Ada Target (SAT) [30] rupts, ARM page tables and address spaces. This and the Logical Coprocessor Kernel (LOCK) [55] are change cost several pms to design and implement, also inspired by the PSOS design and methodology. and resulted in 1.5–2 py to re-verify. It modified In the 1970s, machine support for theorem prov- about 12 % of existing Haskell code, added another ing was rudimentary. Basic language concepts like 37 %, and re-verification cost about 32 % of the time pointers still posed large problems. The UCLA ef- previously invested in verification. fort reports that the simplifications required to make The new features required only minor adjustments verification feasible made the kernel an order of mag- of existing invariants, but lead to a considerable nitude slower [67]. We have demonstrated that with number of new invariants for the new code. These modern tools and techniques, this is no longer the invariants have to be preserved over the whole kernel case. API, not just the new features. The first real, completed implementation proofs, Unsurprisingly, fundamental changes to existing although for a highly idealised OS kernel are reported features are bad news. We had one example of such for KIT, consisting of 320 lines of artificial, but real- a change when we added reply capabilities for ef- istic assembly instructions [8]. ficient RPC as an API optimisation after the first Bevier and Smith later produced a formalisation of refinement was completed. Reply capabilities are the microkernel [9] without implementations created on the fly in the receiver of an IPC and are proofs. Other formal modelling and proofs for OS treated in most cases like other capabilities. They are kernels that did not proceed to the implementation single-use, and thus deleted immediately after use. level include the EROS kernel [57], the high-level This fundamentally broke a number of properties and analysis of SELinux [5,29] based on FLASK [60], and invariants on capabilities. Creation and deletion of the MASK [48] project which was geared towards capabilities require a large number of preconditions information-flow properties. to execute safely. The operations were carefully con- The VFiasco project [36] and later the Robin strained in the kernel. Doing them on the fly required project [61] attempted to verify C++ kernel imple- complex preconditions to be proved for many new mentations. They managed to create a precise model code paths. Some of these turned out not to be true, of a large, relevant part of C++, but did not verify which required extra work on special-case proofs or substantial parts of a kernel. changes to existing invariants (which then needed Heitmeyer et al [33] report on the verification and to be re-proved for the whole kernel). Even though certification of a “software-based the code size of this change was small (less than 5 % embedded device” featuring a small (3,000 LOC) of the total code base), the comparative amount of . They show data separation only, conceptual cross-cutting was huge. It took about not functional correctness. Although they seem to 1 py or 17 % of the original proof effort to re-verify. at least model the implementation level, they did There is one class of otherwise frequent code not conduct a machine-checked proof directly on the changes that does not occur after the kernel has C-code. been verified: implementation bug fixes. Hardin et al [31] formally verified information-flow 6 Related Work properties of the AAMP7 [53], which We briefly summarise the literature on OS verifica- implements the functionality of a static separation tion. Klein [40] provides a comprehensive overview. kernel in hardware. The functionality provided is The first serious attempts to verify an OS kernel less complex than a general purpose microkernel— were in the late 1970s UCLA Secure Unix [67] and the processor does not support online reconfiguration the Provably Secure Operating System (PSOS) [24]. of separation domains. The proof goes down to a Our approach mirrors the UCLA effort in using re- low-level design that is in close correspondence to finement and defining functional correctness as the the micro code. This correspondence is not proven formally, but by manual inspection. 7 Conclusions A similar property was recently shown for Green We have presented our experience in formally verify- Hills’ Integrity kernel [28] during a Common Criteria ing seL4. We have shown that full, rigorous, formal EAL6+ certification [27]. The Separation Kernel verification is practically achievable for OS microker- Protection Profile [38] of Common Criteria shows nels with very reasonable effort compared to tradi- data separation only. It is a weaker property than tional development methods. full functional correctness. Although we have not invested significant effort A closely related contemporary project is into optimisation, we have shown that optimisations Verisoft [2], which is attempting to verify not only the are possible and that performance does not need OS, but a whole software stack from verified hardware to be sacrificed for verification. The seL4 kernel is up to verified application programs. This includes a practical, usable, and directly deployable, running formally verified, non-optimising compiler for their on ARMv6 and x86. own Pascal-like implementation language. Even if Collateral benefits of the verification include our not all proofs are completed yet, the project has suc- rapid prototyping methodology for kernel design. We cessfully demonstrated that such a verification stack observed a confluence of design principles from the for full functional correctness can be achieved. They formal methods and the OS side, leading to design have also shown that verification of assembly-level decisions such as an event-based kernel that is mostly code is feasible. However, Verisoft accepts two orders non-preemptable and uses interrupt polling. These of magnitude slow-down for their highly-simplified decisions made the kernel design simpler and easier to VAMOS kernel (e.g. only single-level page tables) verify without sacrificing performance. Evidence sug- and that their verified hardware platform VAMP is gests that taking the detour via a Haskell prototype not widely deployed. We deal with real C and stan- increased our productivity even without considering dard tool chains on ARMv6, and have aimed for a verification. commercially deployable, realistic microkernel. Future work in this project includes verification of Other formal techniques for increasing the trustwor- the assembly parts of the kernel, a multi-core ver- thiness of operating systems include static analysis, sion of the kernel, as well as application verification. model checking and shape analysis. Static analysis The latter now becomes much more meaningful than can in the best case only show the absence of certain previously possible: application proofs can rely on classes of defects such as buffer overruns. Model the abstract, formal kernel specification that seL4 is checking in the OS space includes SLAM [6] and proven to implement. BLAST [34]. They can show specific safety prop- Compared to the state of the art in software cer- erties of C programs automatically, such as correct tification, the ultimate degree of trustworthiness we API usage in device drivers. The terminator tool [15] have achieved is redefining the standard of highest increases reliability of device drivers by attempting assurance. to prove termination automatically. Full functional Acknowledgements correctness of a realistic microkernel is still beyond We thank Timothy Bourke, Timothy Roscoe, and the scope of these automatic techniques. Adam Wiggins for valued feedback on drafts of this Implementations of kernels in type-safe languages article. We also would like to acknowledge the contri- such as SPIN [7] and Singularity [23] offer increased bution of the former team members on this verifica- reliability, but they have to rely on traditional “dirty” tion project: Jeremy Dawson, Jia Meng, Catherine code to implement their language runtime, which Menon, and David Tsai. tends to be substantially bigger than the complete NICTA is funded by the Australian Government seL4 kernel. While type safety is a good property as represented by the Department of Broadband, to have, it is not very strong. The kernel may still Communications and the Digital Economy and the misbehave or attempt, for instance, a null pointer Australian Research Council through the ICT Centre access. Instead of randomly crashing, it will report a of Excellence program. controlled exception. In our proof, we show a variant of type safety for the seL4 code. Even though the References kernel deliberately breaks the C type system, it only [1] M. Accetta, R. Baron, W. Bolosky, D. Golub, does so in a safe way. Additionally, we prove much R. Rashid, A. Tevanian, and M. Young. Mach: A more: that there will never be any such null pointer new kernel foundation for UNIX development. In accesses, that the kernel will never crash and that 1986 Summer USENIX, pages 93–112, 1986. the code always behaves in strict accordance with [2] E. Alkassar, M. Hillebrand, D. Leinenbach, the abstract specification. N. Schirmer, A. Starostin, and A. Tsyban. Bal- ancing the load — leveraging a semantics stack for [16] J. Criswell, A. Lenharth, D. Dhurjati, and . Adve. systems verification. JAR, 42(2–4), 2009. Secure virtual architecture: A safe execution envi- [3] E. Alkassar, N. Schirmer, and A. Starostin. Formal ronment for commodity operating systems. In 16th pervasive verification of a paging mechanism. In SOSP, pages 351–366, Oct 2007. C. R. Ramakrishnan and J. Rehof, editors, Tools and [17] U. Dannowski. Personal communication. Alg. for the Construction and Analysis of Systems (TACAS), volume 4963 of LNCS, pages 109–123. [18] W.-P. de Roever and K. Engelhardt. Data Refine- Springer, 2008. ment: Model-Oriented Proof Methods and their Com- parison. Number 47 in Cambridge Tracts in The- [4] J. Alves-Foss, P. W. Oman, C. Taylor, and S. Har- oretical Computer Science. Cambridge University rison. The MILS architecture for high-assurance Press, 1998. embedded systems. Int. J. Emb. Syst., 2:239–247, 2006. [19] P. Derrin, K. Elphinstone, G. Klein, D. Cock, and M. M. T. Chakravarty. Running the manual: An ap- [5] M. Archer, E. Leonard, and M. Pradella. Analyzing proach to high-assurance microkernel development. security-enhanced policy specifications. In In ACM SIGPLAN Haskell WS, Sep 2006. POLICY ’03: Proc. 4th IEEE Int. WS on Policies for Distributed Systems and Networks, pages 158– [20] D. Elkaduwe, P. Derrin, and K. Elphinstone. Kernel 169. IEEE Computer Society, 2003. design for isolation and assurance of physical mem- [6] T. Ball and S. K. Rajamani. SLIC: A specification ory. In 1st IIES, pages 35–40. ACM SIGOPS, Apr language for interface checking. Technical Report 2008. MSR-TR-2001-21, Microsoft Research, 2001. [21] D. Elkaduwe, G. Klein, and K. Elphinstone. Veri- [7] B. N. Bershad, S. Savage, P. Pardyak, E. G. Sirer, fied protection model of the seL4 microkernel. In M. E. Fiuczynski, D. Becker, C. Chambers, and J. Woodcock and N. Shankar, editors, VSTTE 2008 S. Eggers. Extensibility, safety and performance — Verified Softw.: Theories, Tools & Experiments, in the SPIN operating system. In 15th SOSP, Dec volume 5295 of LNCS, pages 99–114. Springer, Oct 1995. 2008. [8] W. R. Bevier. Kit: A study in operating system [22] K. Elphinstone, G. Klein, P. Derrin, T. Roscoe, and verification. IEEE Transactions on Software Engi- G. Heiser. Towards a practical, verified kernel. In neering, 15(11):1382–1396, 1989. 11th HotOS, pages 117–122, May 2007. [9] W. R. Bevier and L. Smith. A mathematical model [23] M. F¨ahndrich, M. Aiken, C. Hawblitzel, O. Hodson, of the Mach kernel: Atomic actions and locks. Tech- G. C. Hunt, J. R. Larus, and S. Levi. Language nical Report 89, Computational Logic Inc., Apr support for fast and reliable message-based commu- 1993. nication in Singularity OS. In 1st EuroSys Conf., [10] I. T. Bowman, R. C. Holt, and N. V. Brewster. Linux pages 177–190, Apr 2006. as a case study: its extracted software architecture. [24] R. J. Feiertag and P. G. Neumann. The foundations In ICSE ’99: Proc. 21st Int. Conf. on Software of a provably secure operating system (PSOS). In Engineering, pages 555–563. ACM, 1999. AFIPS Conf. Proc., 1979 National Comp. Conf., [11] A. Boyton. A verified shared capability model. In Jun 1979. G. Klein, R. Huuck, and B. Schlich, editors, 4th [25] B. Ford, M. Hibler, J. Lepreau, R. McGrath, and WS Syst. Softw. Verification SSV’09, ENTCS, pages P. Tullmann. Interface and execution models in the 99–116. Elsevier, Jun 2009. Fluke kernel. In 3rd OSDI. USENIX, Feb 1999. [12] P. Brinch Hansen. The nucleus of a multiprogram- [26] T. Garfinkel, B. Pfaff, J. Chow, M. Rosenblum, and ming operating system. CACM, 13:238–250, 1970. D. Boneh. Terra: A virtual machine-based platform [13] D. Cock. Bitfields and tagged unions in C: Verifica- for trusted computing. In 19th SOSP, Oct 2003. tion through automatic generation. In B. Beckert and G. Klein, editors, VERIFY’08, volume 372 of [27] , Inc. INTEGRITY- CEUR Workshop Proceedings, pages 44–55, Aug 178B separation kernel security target version 2008. 1.0. http://www.niap-ccevs.org/cc-scheme/st/st vid10119-st.pdf, 2008. [14] D. Cock, G. Klein, and T. Sewell. Secure micro- kernels, state monads and scalable refinement. In [28] Greenhills Software, Inc. Integrity real-time oper- O. A. Mohamed, C. Mu˜noz,and S. Tahar, editors, ating system. http://www.ghs.com/products/rtos/ 21st TPHOLs, volume 5170 of LNCS, pages 167–182. integrity.html, 2008. Springer, Aug 2008. [29] J. D. Guttman, A. L. Herzog, J. D. Ramsdell, and [15] B. Cook, A. Gotsman, A. Podelski, A. Rybalchenko, C. W. Skorupka. Verifying information flow goals and M. Y. Vardi. Proving that programs eventually in security-enhanced Linux. Journal of Computer do something good. In 34th POPL. ACM, 2007. Security, 13(1):115–134, 2005. [30] J. T. Haigh and W. D. Young. Extending the non- [46] J. Liedtke. Towards real microkernels. CACM, interference version of MLS for SAT. IEEE Trans. 39(9):70–77, Sep 1996. on Software Engineering, 13(2):141–150, 1987. [47] J. Liedtke, K. Elphinstone, S. Sch¨onberg, H. H¨artig, [31] D. S. Hardin, E. W. Smith, and W. D. Young. A G. Heiser, N. Islam, and T. Jaeger. Achieved IPC robust machine code proof framework for highly performance (still the foundation for extensibility). secure applications. In ACL2’06: Proc. Int. WS on In 6th HotOS, pages 28–31, May 1997. the ACL2 theorem prover and its applications. ACM, [48] W. B. Martin, P. White, A. Goldberg, and F. S. 2006. Taylor. Formal construction of the mathematically [32] G. Heiser. Hypervisors for consumer electronics. In analyzed separation kernel. In ASE ’00: Proc. 15th 6th IEEE CCNC, 2009. IEEE Int. Conf. on Automated software engineering, pages 133–141. IEEE Computer Society, 2000. [33] C. L. Heitmeyer, M. Archer, E. I. Leonard, and J. McLean. Formal specification and verification [49] Z. Ni, D. Yu, and Z. Shao. Using XCAP to certify of data separation in a separation kernel for an realistic system code: Machine context management. . In CCS ’06: Proc. 13th Conf. In Proc. TPHOLs’07, volume 4732 of LNCS, pages on Computer and Communications Security, pages 189–206. Springer, Sep 2007. 346–355. ACM, 2006. [50] T. Nipkow, L. Paulson, and M. Wenzel. Is- abelle/HOL — A Proof Assistant for Higher-Order [34] T. A. Henzinger, R. Jhala, R. Majumdar, and Logic, volume 2283 of LNCS. Springer, 2002. G. Sutre. Software verification with Blast. In SPIN’03, Workshop on Model Checking Software, [51] OKL4 web site. http://okl4.org. 2003. [52] T. Perrine, J. Codd, and B. Hardy. An overview [35] M. Hohmuth, M. Peter, H. H¨artig,and J. S. Shapiro. of the kernelized secure operating system (KSOS). Reducing TCB size by using untrusted components In Proceedings of the Seventh DoD/NBS Computer — small kernels versus virtual-machine monitors. In Security Initiative Conference, pages 146–160, Sep 11th SIGOPS Eur. WS, Sep 2004. 1984. [53] Rockwell Collins, Inc. AAMP7r1 Reference Manual, [36] M. Hohmuth and H. Tews. The VFiasco approach 2003. for a verified operating system. In 2nd PLOS, Jul 2005. [54] J. M. Rushby. Design and verification of secure systems. In 8th SOSP, pages 12–21, 1981. [37] Iguana. http://www.ertos.nicta.com.au/software/ kenge/iguana-project/latest/. [55] O. Saydjari, J. Beckman, and J. Leaman. Locking computers securely. In 10th National Computer [38] Information Assurance Directorate. U.S. Govern- Security Conference, pages 129–141, Sep 1987. ment Protection Profile for Separation Kernels in [56] A. Seshadri, M. Luk, N. Qu, and A. Perrig. SecVisor: Environments Requiring High Robustness, Jun 2007. A tiny to provide lifetime kernel code Version 1.03. http://www.niap-ccevs.org/cc-scheme/ integrity for commodity OSes. In 16th SOSP, pages pp/pp.cfm/id/pp skpp hr v1.03/. 335–350, Oct 2007. [39] ISO/IEC. Programming languages — C. Techni- [57] J. S. Shapiro, D. F. Faber, and J. M. Smith. State cal Report 9899:TC2, ISO/IEC JTC1/SC22/WG14, caching in the EROS kernel—implementing efficient May 2005. orthogonal peristence in a pure capability system. [40] G. Klein. Operating system verification — an In 5th IWOOOS, pages 89–100, Nov 1996. overview. S¯adhan¯a, 34(1):27–69, Feb 2009. [58] J. S. Shapiro, J. M. Smith, and D. J. Farber. EROS: [41] G. Klein, P. Derrin, and K. Elphinstone. Expe- A fast capability system. In 17th SOSP, Dec 1999. rience report: seL4 — formally verifying a high- [59] L. Singaravelu, C. Pu, H. H¨artig,and C. Helmuth. performance microkernel. In 14th ICFP, Aug 2009. Reducing TCB complexity for security-sensitive ap- [42] R. Kolanski and G. Klein. Types, maps and sepa- plications: Three case studies. In 1st EuroSys Conf., ration logic. In S. Berghofer, T. Nipkow, C. Urban, pages 161–174, Apr 2006. and M. Wenzel, editors, Proc. TPHOLs’09, volume [60] R. Spencer, S. Smalley, P. Loscocco, M. Hibler, 5674 of LNCS. Springer, 2009. D. Andersen, and J. Lepreau. The Flask security architecture: System support for diverse security [43] L4HQ. http://l4hq.org/arch/arm/. policies. In 8th USENIX Security Symp., Aug 1999. [44] X. Leroy. Formal certification of a compiler back-end, [61] H. Tews, T. Weber, and M. V¨olp.A formal model or: Programming a compiler with a proof assistant. of memory peculiarities for the verification of low- In J. G. Morrisett and S. L. P. Jones, editors, 33rd level operating-system code. In R. Huuck, G. Klein, POPL, pages 42–54. ACM, 2006. and B. Schlich, editors, Proc. 3rd Int. WS on Sys- [45] J. Liedtke. Improving IPC by kernel design. In 14th tems Software Verification (SSV’08), volume 217 of SOSP, pages 175–188, Dec 1993. ENTCS, pages 79–96. Elsevier, Feb 2008. [62] H. Tuch. Formal Memory Models for Verifying C Systems 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, Jun 2005. [65] H. Tuch, G. Klein, and M. Norrish. Types, bytes, and separation 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 Standard 15408. http://csrc.nist.gov/cc/. [67] B. J. Walker, R. A. Kemmerer, and G. J. Popek. Specification and verification of the UCLA Unix security 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. Scale and performance in the Denali isolation kernel. In 5th 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. : The kernel of a multiprocessor operating system. CACM, 17:337– 345, 1974.