used (ARMv6). These are explicit in the model, and we can
therefore detect violations. Foundationally means that we
do not just axiomatize the behavior of C on a high level, but
we derive it from first principles as far as possible. For example, in our model of C, memory is a primitive function from
addresses to bytes without type information or restrictions.
On top of that, we specify how types like unsigned int are
encoded, how structures are laid out, and how implicit and
explicit type casts behave. We managed to lift this low-level
memory model to a high-level calculus that allows efficient,
abstract reasoning on the type-safe fragment of the kernel.
We generate proof obligations assuring 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
typically easy to discharge. We generate similar obligations
for all restrictions the C99 standard demands.
We treat a very large, pragmatic subset of C99 in the verification. It is a compromise between verification convenience
and the hoops the kernel programmers were willing to jump
through in writing their source. The following paragraphs
describe what is not in this subset.
We do not allow the address-of operator & on local variables, because, for better automation, we make the assumption that local variables are separate from the heap. This could
be violated if their address was available to pass on. It is the
most far-reaching restriction we implement, because it is common in C to use local variable references for return parameters
to avoid returning large types on the stack. We achieved compliance with this requirement by avoiding reference parameters as much as possible, and where they were needed, used
pointers to global variables (which are not restricted).
One feature of C that is problematic for verification
(and programmers) is the unspecified order of evaluation
in expressions with side effects. To deal with this feature
soundly, we limit how side effects can occur in expressions.
If more than one function call occurs within an expression
or the expression otherwise accesses global state, a proof
obligation is generated to show that these functions are side-effect free. This proof obligation is discharged automatically.
We do not allow function calls through function pointers.
(We do allow handing the address of a function to assembler
code, e.g., for installing exception vector tables.) We also do
not allow goto statements, or switch statements with fall-through cases. We support C99 compound literals, making
it convenient to return structs from functions, and reducing
the need for reference parameters. We do not allow compound literals to be lvalues. Some of these restrictions could
be lifted easily, but the features were not required in seL4.
We did not use unions directly in seL4 and therefore do
not support them in the verification (although that would be
possible). Since the C implementation was derived from a
functional program, all unions in seL4 are tagged, and many
structs are packed bitfields. Like other kernel implementors, we do not trust GCC to compile and optimize bitfields
predictably for kernel code. Instead, we wrote a small tool
that takes a specification and generates C code with the necessary shifting and masking for such bitfields. The tool helps
us to easily map structures to page table entries or other
hardware-defined memory layouts. The generated code can
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;
}
be inlined and, after compilation on ARM, the result is more
compact and faster than GCC’s native bitfields. The tool not
only generates the C code, it also automatically generates
Isabelle/HOL specifications and proofs of correctness.
Figure 5 shows part of the implementation of the scheduling functionality described in the previous sections. It is
standard C99 code with pointers, arrays and structs. The
thread_state functions used in Figure 5 are examples of
generated bitfield accessors.
4. 4. the proof
This section describes the main theorem we have shown
and how its proof was constructed.
As mentioned, the main property we are interested in is
functional correctness, which we prove by showing formal
refinement. We have formalized this property for general
state machines in Isabelle/HOL, and we instantiate each of
the specifications in the previous sections into this state-machine framework.
We have also proved the well-known reduction of refinement to forward simulation, illustrated in Figure 6 where
the solid arrows mean universal quantification and the
dashed arrows existential: To show that a concrete state
machine M2 refines an abstract one M1, it is sufficient to
show that for each transition in M2 that may lead from
an initial state s to a set of states s¢, there exists a corresponding transition on the abstract side from an abstract
state s to a set s ¢ (they are sets because the machines may
be nondeterministic). The transitions correspond if there
exists a relation R between the states s and s such that for
each concrete state in s¢ there is an abstract one in s ¢ that
makes R hold between them again. This has to be shown
for each transition with the same overall relation R. For
externally visible state, we require R to be equality. For
each refinement layer in Figure 2, we have strengthened
and varied this proof technique slightly, but the general
idea remains the same.
We now describe the instantiation of this framework to
the seL4 kernel. We have the following types of transition in