Protecting Commodity Operating System Kernels from ..., Lecture notes of Operating Systems

Rutgers University Rutgers University University of Wisconsin-Madison Rutgers University. Abstract. Device drivers on commodity operating systems execute.

Typology: Lecture notes

2022/2023

Uploaded on 05/11/2023

leonpan
leonpan 🇺🇸

4

(12)

286 documents

1 / 14

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
This is an expanded version of an identically-titled paper published in ACSAC’09
Proceedings of the 25th Annual Computer Security Applications Conference, Honolulu, Hawaii, December 2009.
Protecting Commodity Operating System Kernels from Vulnerable Device Drivers
Shakeel Butt Vinod Ganapathy Michael M. Swift Chih-Cheng Chang
Rutgers University Rutgers University University of Wisconsin-Madison Rutgers University
Abstract
Device drivers on commodity operating systems execute
with kernel privilege and have unfettered access to kernel
data structures. Several recent attacks demonstrate that such
poor isolation exposes kernel data to exploits against vulner-
able device drivers, for example through buffer overruns in
packet processing code. Prior architectures to isolate kernel
data from driver code either sacrifice performance, execute
too much driver code with kernel privilege, or are incompat-
ible with commodity operating systems.
In this paper, we present the design, implementation
and evaluation of a novel security architecture that bet-
ter isolates kernel data from device drivers without sac-
rificing performance or compatibility. In this architec-
ture, a device driver is partitioned into a small, trusted
kernel-mode component and an untrusted user-mode com-
ponent. The kernel-mode component contains privileged
and performance-critical code. It communicates via RPC
with the user-mode component which contains the rest of the
driver code. A RPC monitor mediates all control and data
transfers between the kernel- and user-mode components.
In particular, it verifies that all data transfers from the un-
trusted user-mode component to the kernel-mode component
preserve kernel data structure integrity. We also present a
runtime technique to automatically infer such integrity spec-
ifications. Our experiments with a Linux implementation of
this architecture show that it can prevent compromised de-
vice drivers from affecting the integrity of kernel data and do
so without impacting common-case performance.
1. Introduction
Device drivers execute with kernel privilege in most com-
modity operating systems and have unrestricted access to
kernel data structures. Because the kernel is part of the
Trusted Computing Base (TCB) of the system, vulnerabili-
ties in driver code can jeopardize the entire system.
Several studies indicate that device drivers are rife with
exploitable security holes. A recent study of user/kernel bugs
in the Linux kernel found that 9 out of 11 of these bugs
were in device drivers [23]. An audit of the Linux kernel by
Coverity also found that over 50% of bugs were in device
drivers [12]. Our own analysis of vulnerability databases
revealed several device drivers that are vulnerable to mal-
Supported by NSF awards 0831268, 0915394 and 0931992.
formed input from untrusted user-space applications, allow-
ing an attacker to execute arbitrary code with kernel privi-
lege [4, 30]. Similarly, device drivers by their very nature
copy untrusted data from devices to kernel memory. Because
the kernel does not restrict the memory locations accessible
to devices, a compromised driver can write arbitrary values
to sensitive kernel data structures. For example, a compro-
mised driver could overwrite the table of interrupt handlers in
the operating system with pointers to attacker-defined code.
As demonstrated by recently published exploits against wire-
less device drivers in Windows XP [7, 8] and Mac OS X [27],
vulnerabilities in drivers are an increasingly attractive target
for attackers.
Microkernels [26, 40, 42] offer one way to isolate ker-
nel data from vulnerable device drivers. They execute de-
vice drivers as user-mode processes and can prevent ma-
licious modifications to kernel data by enforcing domain-
specific rules, e.g., as done in Nexus [40]. However, mi-
crokernels restructure the operating system, and the protec-
tion mechanisms that they offer are not applicable to com-
modity operating systems, which are structured as macro-
kernels. Moreover, enforcing security policies on device
drivers may impose significant performance overhead. For
example, Nexus reports CPU overheads of 2.5×on a CPU-
intensive media streaming workload. User-mode driver
frameworks [3, 10, 15, 24, 28, 38] allow commodity oper-
ating systems to execute device drivers in user mode. How-
ever, porting drivers to these frameworks often requires com-
plete rewrites of device drivers and the resulting performance
overheads are often significant [3, 38].
This paper extends prior work on Microdrivers [20] and
proposes a security architecture that offers commodity op-
erating systems the benefits of executing device drivers in
user mode without affecting common-case performance. In
this architecture, each device driver is composed of a trusted
kernel-level component, called a k-driver, and an untrusted
user-level component, called a u-driver. The k-driver con-
tains code that requires kernel privilege (e.g., interrupt pro-
cessing functions) and performance-critical code (e.g., func-
tions on the I/O path). The rest of the code, which contains
functions to initialize, shutdown, and configure the device,
neither requires kernel privilege nor is on the critical path and
executes as a user mode process. The combination of the u-
driver and the k-driver is called a microdriver. A prior study
with 297 Linux device drivers comprising network, sound
and SCSI drivers showed that as much as 65% of driver code
can execute in user mode without requiring kernel privilege
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe

Partial preview of the text

Download Protecting Commodity Operating System Kernels from ... and more Lecture notes Operating Systems in PDF only on Docsity!

This is an expanded version of an identically-titled paper published in ACSAC’ Proceedings of the 25th^ Annual Computer Security Applications Conference, Honolulu, Hawaii, December 2009.

Protecting Commodity Operating System Kernels from Vulnerable Device Drivers ∗

Shakeel Butt Vinod Ganapathy Michael M. Swift Chih-Cheng Chang

Rutgers University Rutgers University University of Wisconsin-Madison Rutgers University

Abstract

Device drivers on commodity operating systems execute with kernel privilege and have unfettered access to kernel data structures. Several recent attacks demonstrate that such poor isolation exposes kernel data to exploits against vulner- able device drivers, for example through buffer overruns in packet processing code. Prior architectures to isolate kernel data from driver code either sacrifice performance, execute too much driver code with kernel privilege, or are incompat- ible with commodity operating systems. In this paper, we present the design, implementation and evaluation of a novel security architecture that bet- ter isolates kernel data from device drivers without sac- rificing performance or compatibility. In this architec- ture, a device driver is partitioned into a small, trusted kernel-mode component and an untrusted user-mode com- ponent. The kernel-mode component contains privileged and performance-critical code. It communicates via RPC with the user-mode component which contains the rest of the driver code. A RPC monitor mediates all control and data transfers between the kernel- and user-mode components. In particular, it verifies that all data transfers from the un- trusted user-mode component to the kernel-mode component preserve kernel data structure integrity. We also present a runtime technique to automatically infer such integrity spec- ifications. Our experiments with a Linux implementation of this architecture show that it can prevent compromised de- vice drivers from affecting the integrity of kernel data and do so without impacting common-case performance.

1. Introduction

Device drivers execute with kernel privilege in most com- modity operating systems and have unrestricted access to kernel data structures. Because the kernel is part of the Trusted Computing Base (TCB) of the system, vulnerabili- ties in driver code can jeopardize the entire system. Several studies indicate that device drivers are rife with exploitable security holes. A recent study of user/kernel bugs in the Linux kernel found that 9 out of 11 of these bugs were in device drivers [23]. An audit of the Linux kernel by Coverity also found that over 50% of bugs were in device drivers [12]. Our own analysis of vulnerability databases revealed several device drivers that are vulnerable to mal-

∗Supported by NSF awards 0831268, 0915394 and 0931992.

formed input from untrusted user-space applications, allow- ing an attacker to execute arbitrary code with kernel privi- lege [4, 30]. Similarly, device drivers by their very nature copy untrusted data from devices to kernel memory. Because the kernel does not restrict the memory locations accessible to devices, a compromised driver can write arbitrary values to sensitive kernel data structures. For example, a compro- mised driver could overwrite the table of interrupt handlers in the operating system with pointers to attacker-defined code. As demonstrated by recently published exploits against wire- less device drivers in Windows XP [7, 8] and Mac OS X [27], vulnerabilities in drivers are an increasingly attractive target for attackers. Microkernels [26, 40, 42] offer one way to isolate ker- nel data from vulnerable device drivers. They execute de- vice drivers as user-mode processes and can prevent ma- licious modifications to kernel data by enforcing domain- specific rules, e.g., as done in Nexus [40]. However, mi- crokernels restructure the operating system, and the protec- tion mechanisms that they offer are not applicable to com- modity operating systems, which are structured as macro- kernels. Moreover, enforcing security policies on device drivers may impose significant performance overhead. For example, Nexus reports CPU overheads of 2.5× on a CPU- intensive media streaming workload. User-mode driver frameworks [3, 10, 15, 24, 28, 38] allow commodity oper- ating systems to execute device drivers in user mode. How- ever, porting drivers to these frameworks often requires com- plete rewrites of device drivers and the resulting performance overheads are often significant [3, 38]. This paper extends prior work on Microdrivers [20] and proposes a security architecture that offers commodity op- erating systems the benefits of executing device drivers in user mode without affecting common-case performance. In this architecture, each device driver is composed of a trusted kernel-level component, called a k-driver, and an untrusted user-level component, called a u-driver. The k-driver con- tains code that requires kernel privilege (e.g., interrupt pro- cessing functions) and performance-critical code (e.g., func- tions on the I/O path). The rest of the code, which contains functions to initialize, shutdown, and configure the device, neither requires kernel privilege nor is on the critical path and executes as a user mode process. The combination of the u- driver and the k-driver is called a microdriver. A prior study with 297 Linux device drivers comprising network, sound and SCSI drivers showed that as much as 65% of driver code can execute in user mode without requiring kernel privilege

or affecting common-case performance [20]. A u-driver and its corresponding k-driver communicate via an RPC-like interface. When the k-driver receives a re- quest from the kernel to execute functionality implemented in the u-driver, such as initializing or configuring the de- vice, it forwards this request to the u-driver. Similarly, the u-driver may also invoke the k-driver to perform privileged operations or to invoke functions that are implemented in the kernel. However, the u-driver is untrusted and all requests that it sends to the k-driver must be monitored. For example, a u-driver that has been compromised by exploiting a buffer overrun vulnerability may potentially send spurious updates to kernel data structure in its requests to the k-driver. Be- cause the k-driver applies these updates to kernel data struc- tures, the compromised u-driver may affect the security of the entire operating system. We present a RPC monitor to interpose upon all commu- nication between the u-driver and the k-driver, and to ensure that each message conforms to a security policy. The RPC monitor checks both data values and function call targets in these messages. Data values in messages may contain up- dates to data structures that the u-driver shares with the k- driver. The RPC monitor enforces integrity constraints on updates to kernel data structures initiated by the u-driver. In our implementation, these integrity constraints are specified as data structure invariants—constraints that must always be satisfied by the data structure. For example, one such invari- ant may state that the list of network devices must not change during an invocation of a u-driver function to obtain device configuration settings. We present an approach to automati- cally extract such data structure invariants using Daikon [18], a state-of-the-art invariant inference tool. Similarly, the RPC monitor also ensures that k-driver function calls that are in- voked by the u-driver via RPC are allowed by a control trans- fer policy that is extracted using static analysis of the driver. This paper makes two key contributions over prior work on Microdrivers [20]. First, it presents the design and imple- mentation of the RPC monitor to mediate u-driver/k-driver communication. In prior work on Microdrivers, all commu- nication between a u-driver and a k-driver was unchecked, thereby poorly isolating kernel data from untrusted u-drivers. Second, it presents a technique to automatically infer data structure integrity constraints to be enforced by the RPC monitor. The key property of these constraints is that they ex- press invariants over heap data structures, thereby constrict- ing the updates that a compromised u-driver can apply to ker- nel data structures. The security architecture proposed in this paper offers several benefits over prior isolation architectures.

  • Reduction of kernel-mode driver code. Isolation architec- tures such as Nooks [35], Mondrix [43] and SafeDrive [41] execute drivers in kernel mode and do not monitor driver- initiated updates to kernel data structures. Consequently, the kernel can be compromised by exploiting vulnerabilities that these architectures do not protect against, e.g., race condi- tions and double-free bugs. In contrast, our architecture ex- ecutes a large fraction of driver code in user space (as u-

drivers) and monitors kernel data structure updates initiated by u-drivers.

  • Compatibility with commodity operating systems. A k- driver interfaces with the kernel in much the same way as a traditional device driver. Kernel calls to functions imple- mented in the u-driver are transparently forwarded by the k- driver to the u-driver. Therefore, in contrast to prior work on microkernels, our security architecture is compatible with commodity operating systems.
  • Good common-case performance. User-mode driver frameworks have often sacrificed performance for se- curity [3, 38]. In contrast, our architecture executes performance-critical functionality in the kernel thereby im- posing no runtime overhead for the common case.
  • Flexibility. Rather than offering a rigid definition of trusted and untrusted components, our architecture offers the flexi- bility in choosing which portions of the driver execute with kernel privilege. Although performance-critical functions must preferably be executed in the k-driver, our architec- ture does not enforce such restrictions. Thus, for instance, the kernel can be protected from zero-day attacks by rele- gating code with newly-discovered vulnerabilities to the u- driver until the driver vendor issues a patch. Despite these benfits, our security architecture is not a panacea and cannot completely prevent a compromised u- driver from hijacking the kernel. Nevertheless, our experi- ments show that it can prevent a significant fraction of attacks from propagating to and hijacking the kernel. We have implemented our security architecture in the Linux-2.6.18.1 kernel and have applied it to four device drivers. Experiments show that our architecture can protect against compromised u-drivers and do so without affecting common-case performance.

2. Background and scope

Device drivers for commodity operating systems execute in the same protection domain as the rest of the kernel to achieve good performance and easy access to hardware. This architecture does not isolate kernel data from vulnerabilities in device drivers, which are written in C by third-party ven- dors. Such vulnerabilities, especially in packet-processing code and ioctl handlers, can be exploited by malicious user- space applications. For example, recent work [7, 8] shows that a remote attacker can hijack control of Windows ma- chine by exploiting a buffer overflow in beacon and probe response processing code in an 802.11 device driver. In- deed, our study of vulnerability databases revealed several exploitable buffer overrun and memory allocation vulnera- bilities in driver code [4, 30]. The threats posed to kernel data by compromised device drivers can broadly be classified into two categories.

  • Threats at the kernel/driver interface. Kernel data struc- tures are routinely updated by device drivers, and the kernel imposes no restrictions on the memory regions accessible to drivers or devices. This freedom can be misused by com- promised drivers in a variety of ways. Compromised device

ments marshaling/unmarshaling protocols to transfer data. (2) Object tracking. Splitting a device driver into a k-driver and u-driver results in data structures being copied between address spaces. The runtime tracks and synchronizes the k- driver’s and the u-driver’s versions of driver data structures. Specifically, it is responsible for propagating the k-driver’s changes to a driver data structure to a u-driver upon an upcall, and for propagating the u-driver’s changes to the k-driver when the upcall returns or when the u-driver makes a down- call into the k-driver. There are several key challenges that must be addressed by the runtime. For example, it must ensure that the u-driver and the k-driver can never simultaneously lock a data struc- ture, and that when the lock is released, the copies of the data structure in the u-driver and the k-driver are synchronized. It must also correctly allocate and deallocate memory in user and kernel space in response to allocation/deallocation re- quests by the u-driver and the k-driver. We refer the inter- ested reader to the Microdrivers paper [20], which describes mechanisms to deal with these challenges in detail.

3.1. RPC monitor

As discussed above, the runtime ensures that driver data structure changes made by the u-driver are propagated to the k-driver, either when an upcall returns or when the u- driver issues a downcall. Because the u-driver is untrusted, all data and control transfers initiated by the u-driver must be checked against a security policy. This is the task of the RPC monitor, shown in Figure 1, which mediates all RPC messages from the u-driver to the k-driver. Note that control and data transfers from the k-driver to the u-driver need not be mediated because the k-driver is trusted. Because our ar- chitecture seeks to protect the integrity of kernel data (rather than its secrecy), the RPC monitor need only monitor writes to kernel data structures. The RPC monitor is implemented as a kernel module that enforces security policies before con- trol and data are transferred to the k-driver. Monitoring data transfer. A compromised u-driver can ma- liciously modify kernel data structures by passing corrupt data. The RPC monitor must therefore detect and prevent malicious data transfers. When a u-driver returns control to its k-driver follow- ing an upcall, or when the u-driver invokes functionality implemented in the kernel or the k-driver via a downcall, data structures in the k-driver are synchronized with their u-driver counterparts using the marshaling protocol. The RPC monitor ensures that each such update conforms to a driver-specific security policy. Intuitively, the goal of the se- curity policy is to ensure that kernel data structures are not updated maliciously, i.e., each update must preserve kernel data structure integrity. For instance, an update must not allow a compromised u-driver access to kernel/device mem- ory regions that a benign u-driver does not normally access. Similarly, an update must not allow a compromised u-driver to execute arbitrary code with kernel privilege. Specifying such integrity constraints is challenging be- cause of the quantity and heterogeneity of kernel data struc-

tures updated by device drivers. In addition, our security ar- chitecture splits device drivers to ensure good performance; consequently, several driver-specific data structures may be copied across the user/kernel boundary. For example, Linux represents network devices using a per-driver net device data structure. In a network microdriver, this data structure may be modified by the u-driver, for example, when the de- vice is initialized or configured. It is also important to mon- itor updates to such driver-specific data structures because these updates propagate to the kernel. Specifying integrity constraints for driver data structures often requires domain- specific knowledge, therefore making manual specification of such integrity constraints cumbersome and error-prone. To overcome these challenges, we present an approach that automatically infers integrity constraints by monitoring driver execution. In our architecture, these constraints are expressed as data structure invariants—properties that the data structure must always satisfy. For example, an invari- ant may state that a function pointer to the packet-send func- tion (e.g., the hard start xmit pointer in the net device data structure in Linux) of a network driver must not change after being initialized. Our approach infers such invariants during training; these are checked during enforcement. During the training phase, we execute the u-driver on sev- eral benign workloads, and use Daikon [18] to infer data structure invariants automatically. Daikon does so by ob- serving the values of data structures that cross the user/kernel boundary and hypothesizing invariants. During the enforce- ment phase, the RPC monitor enforces these invariants on data structures received from a u-driver; it first copies these data structures to a vault area in the kernel, and checks that the invariants hold. If they do, it updates kernel data struc- tures with values from the vault. The kernel itself never uses data structures directly from the vault before they are checked by the RPC monitor. By monitoring data transfers from the u-driver to the k-driver, the RPC monitor prevents compromised u-drivers from affecting kernel data integrity. Monitoring control transfer. The RPC monitor checks u- driver to k-driver control transfers to prevent the u-driver from making unauthorized calls to kernel functions. As the u-driver services an upcall, it may invoke the k- driver via a downcall, either to call a k-driver function or to execute a function implemented in the kernel. Downcalls are implemented using ioctl system calls that are handled in the k-driver. Because the u-driver is untrusted, these down- calls must be verified to be legitimate, e.g., that a downcall is not initiated by a code injection attack on a compromised u- driver. Such unauthorized downcalls can be maliciously used by the u-driver, e.g., to cause denial of service by invoking the kernel function to unregister a device. To avoid such at- tacks, we statically analyze the u-driver and extract the set of downcalls that a u-driver can issue in response to an upcall (static analysis is performed before the driver is loaded). The RPC monitor enforces this statically extracted policy when it receives a downcall from the u-driver. Having checked both data and control integrity, the RPC monitor transfers control to the k-driver, which can now re-

sume execution on newly-updated kernel data structures.

4. Implementation

We extended the implementation of the Microdrivers ar- chitecture on the Linux-2.6.18.1 kernel with support to moni- tor data and control transfers from the u-driver to the k-driver. In this implementation, the k-driver, the kernel runtime and the RPC monitor are implemented as a kernel module while the u-driver and the user runtime execute as a multi-threaded user-space process.

4.1. Background on Microdrivers

A microdriver begins operation when its kernel module is loaded and the user-space process is started. The main thread of the user-space process makes an ioctl call into the kernel module and blocks. The kernel module unblocks this thread when it needs to invoke functions in the u-driver. The u-driver and k-driver exchange data and control us- ing an RPC-like mechanism, shown in Figure 2. To invoke the u-driver using an an upcall (Figure 2(a)), the k-driver ( 1 ) registers the k-driver function that initiates the upcall with the RPC monitor; ( 2 ) marshals data structures that will be read/modified by the u-driver; and ( 3 ) unblocks the thread of the u-driver’s user-space process. This transfers control to the u-driver, which in turn ( 4 ) consults the object tracker and unmarshals the data structures into its address space; and ( 5 ) invokes the appropriate u-driver function on the unmar- shaled data structure. The object tracker is a bi-directional ta- ble responsible for maintaining the correspondence between kernel- and user-mode pointers of data structures shared be- tween the k-driver and the u-driver. As the u-driver runtime unmarshals data received from the k-driver into its address space, it uses the object tracker to identify u-driver objects that correspond to kernel-mode pointers received from the k- driver. If the runtime is unable to find such an object, e.g., be- cause the k-driver or the kernel created a new object that the u-driver is unaware of, the u-driver can allocate a new object and enter a new mapping into the object tracker. When an upcall returns, or when the u-driver invokes functions in the k-driver via an ioctl system call (i.e., a downcall), data is marshaled by the u-driver and unmarshaled in the kernel, as shown in Figure 2(b). The main difference in this case is that a RPC monitor interposes on these requests before they are forwarded to the k-driver. The RPC monitor has two key responsibilities—(i) to check control transfers; and (ii) to check data structure integrity. The RPC moni- tor uses a statically-extracted control flow policy to check control transfers—this policy statically determines the set of allowed downcalls for each upcall. For each downcall, the RPC monitor uses the k-driver function registered with it (in step (1) of Figure 2(a)) to ensure that the downcalls are al- lowed. If this downcall is allowed, the RPC monitor checks the integrity of data structures received from the u-driver. To do so, it unmarshals the data received from the u-driver into a vault area. This area is not accessed by the k-driver and is only used by the RPC monitor to check data structure in- tegrity. The RPC monitor checks that each variable that was unmarshaled satisfies a set of invariants; if so, it uses the data

from the vault area to update kernel data structures and frees any data structures the vault. DriverSlicer. To allow existing device drivers on com- modity operating systems to benefit from our architec- ture, we extended DriverSlicer, a device driver partitioning tool [20], to generate security enforcement code. Driver- Slicer is implemented as a plugin to CIL [31], a source code transformation tool, and consists of about 11,000 lines of Ocaml code. Given a small number of annotations, Driver- Slicer automatically partitions a device driver into a k-driver and a u-driver. It also generates code for the k-driver and u-driver runtimes, and the RPC monitor, including code to check control transfers from the u-driver to the k-driver and code to monitor data structure integrity. DriverSlicer consists of two parts: a splitter and a code generator. The splitter analyzes a device driver and identi- fies functions that must execute in the kernel, i.e., those that require kernel privilege to access the device or those that are performance critical. To do so, it uses programmer-supplied specifications in the form of type signatures, to identify such functions. For example, it identifies interrupt handlers based upon their function prototypes; in Linux interrupt handlers always return a value of type irqreturn t. Similarly, func- tions responsible for transmitting network packets typically have two parameters: a pointer to an sk buff structure, and a pointer to a net device structure. Such type signatures need only be supplied once per family of drivers, e.g., one set of type signatures suffices to identify performance critical and privileged functions for most network drivers. The splitter uses a statically-extracted call-graph of the device driver to mark ( 1 ) all functions that match these type signatures; and ( 2 ) all functions potentially called (transitively) by such func- tions as those that must execute in the k-driver; the remaining functions are relegated to the u-driver. DriverSlicer’s code generator uses the output of the split- ter to partition the driver into a k-driver and a u-driver, and generates RPC code to transfer control and data. In doing so, it may require programmer-supplied annotations to clar- ify the semantics of pointers. For example, to generate code to marshal an object referenced by a void * pointer, the code generator must be supplied with an annotation that de- termines the type of the object. Similarly, the code genera- tor also requires annotations to determine whether a pointer refers to one instance of an object or to an array of instances. DriverSlicer currently uses eight kinds of annotations, details of which appear elsewhere [20]. DriverSlicer uses these an- notations to generate RPC code that minimizes the amount of data copied between the u-driver and the k-driver; it does so by using static analysis to determine variables and data struc- ture fields that are read/modified by the u-driver and only generating marshaling code to copy these variables and fields using RPC.

4.2. Monitoring kernel data structure updates

This section describes an anomaly detection-based ap- proach to infer and enforce invariants on kernel data struc- tures. The approach has two phases: a training phase, in

Function Invariant rtl8139 init module (entry) rtl8139 intr mask == C07F, rtl8139 norx intr mask == C02E rtl8139 init module (exit) rlt8139 intr mask == O(rtl8139 intr mask) rtl8139 norx intr mask == O(rtl8139 norx intr mask) rtl8139 rx config == O(rtl8139 rx config) rtl8139 tx config == O(rtl8139 tx config) rtl8139 get ethtool stats (exit) rtl chip info has only one value rtl8139 get link (exit) dev->hard start xmit has only one value rtl8139 open (entry/exit) dev->base addr ∈ {0x0531C468, 0x06520468} rtl8139 get link (exit) L(dev->mc list) == O(L(dev->mc list))

Figure 3. Examples of invariants extracted from the 8139too driver.

ple, it determines that the value of rtl8139 intr mask is un- changed by a call to rtl8139 init module. Enforcing such an invariant constrains the values of rtl8139 intr mask that can otherwise be modified by a compromised u-driver to ini- tiate I/O to unauthorized ports.

( 3 ) Ranges/sets of values. In several cases, a variable may not be a constant, but acquire one of a small set of values. As Figure 3 shows, Daikon infers such invariants as well; for example, it infers that the dev->base addr, which repre- sents the base address of I/O memory, can only acquire one of two values during driver execution. This constraint must be enforced when the k-driver’s copy of dev->base addr is synchronized with the u-driver’s copy; for otherwise, a com- promised u-driver could coerce the k-driver into writing to arbitrary I/O memory addresses belonging to other devices.

( 4 ) Linked list invariants. The Linux kernel uses linked lists extensively to manage several critical data structures. Prior work demonstrates that kernel linked lists can be stealthily modified to achieve malicious goals [33]. Unfor- tunately, Daikon’s C front end does not support inference of invariants on linked lists. We therefore extended Daikon to infer invariants on linked lists. In particular, we augmented the marshaling protocol with code that records the contents of linked lists that cross the user/kernel boundary. Daikon then processes this trace of values and hypothesizes invariants. Our imple- mentation currently supports inference of invariants that in- dicate that the length of a linked list is unmodified by a func- tion call. Figure 3 presents one such invariant, which states that the linked list dev->mc list is unmodified by a call to rtl8139 get link. A key feature of the above invariants is their ability to monitor the integrity of both control and non-control data in the kernel. For example, by inferring the constancy of function pointers, Daikon can detect attacks that hijack con- trol flow by modifying function pointers to attacker-defined code. Similarly, Daikon can detect attacks that modify I/O memory addresses and allow a rogue driver to write to arbi- trary memory locations, thereby preventing this non-control data attack. Daikon’s dynamic analysis approach enables it to infer several kinds of invariants that would be difficult to discover using static analysis of the driver. For example, static analysis is ill-suited to infer invariants on lengths of linked lists. Similarly, in pointer-intensive code (as is com- mon in device drivers), it is hard to statically infer whether a

heap object is unmodified by a function call without access to precise aliasing information. One of the challenges that we faced during development was the sheer quantity of data recorded by Daikon’s front end during the execution of a u-driver. This in turn resulted in two problems. First, Daikon’s inference engine took longer to in- fer invariants, and sometimes even exhausted the memory available on the machine. Second, Daikon inferred several hundred invariants per function, which resulted in increased memory consumption during enforcement. For example, consider the 8139cp network microdriver: Daikon inferred an average of 878 invariants at the exit of each function in the u-driver. Worse, several of these invariants were serendipi- tous, i.e., they were overly specific to the workloads used during inference and were not satisfied by other workloads, thereby resulting in false positives during enforcement. To overcome these problems, we incorporated two key op- timizations. First, we configured Daikon’s front end to only record values transmitted to u-driver functions that commu- nicate directly with the k-driver via upcalls and downcalls, and do not record values for functions internal to the u-driver. Second, we configured the front end so that only the values of variables that are accessed in the u-driver are recorded. For example, if a u-driver function only reads/modifies cer- tain fields of an otherwise large C struct, we only record the values of the fields that are read/modified by that func- tion. To implement this optimization, we employed a con- servative static analysis of the u-driver to determine the fields read/modified by functions in the u-driver. Because Driver- Slicer’s code generator emits marshaling and unmarshaling code only for variables and fields of data structures that are read/modified by the u-driver, as discussed in Section 4.1, malicious modifications by the u-driver on other variables and data structure fields will not be synchronized with the k-driver; hence, they need not be monitored. These optimizations drastically reduce the number of in- variants that Daikon infers, which in turn reduces the mem- ory consumption of the invariant table (described below) dur- ing enforcement. For example, in the 8139cp network mi- crodriver, the average number of invariants at function exits drops over forty-fold. We expect that inferring invariants would be a one-time activity, accomplished either during driver development (if the driver is developed as a microdriver), or when a legacy driver is split with DriverSlicer; these invariants can be dis- tributed by vendors along with drivers. Note, however, that

some invariants inferred by Daikon must be modified to be widely applicable across multiple installations and configu- rations. For example, the invariant for dev->base addr in Figure 3 refers to specific I/O memory addresses, and is not applicable across multiple installations (the other invariants in Figure 3 are portable across multiple installations). To be portable, this invariant would have to be modified to gener- ically state that dev->base addr has only two values, rather than referring to specific I/O memory addresses, e.g., as with the invariant for dev->hard start xmit in Figure 3.

4.2.2 Enforcing data structure integrity constraints

The invariants inferred by Daikon are enforced by the RPC monitor when the k-driver receives marshaled data from the u-driver. The RPC monitor unmarshals this data into a vault area in the kernel’s address space. Data structures in the vault area are only accessed by the RPC monitor and not by the kernel. The RPC monitor itself is implemented as a kernel mod- ule that manages two tables: an invariant table and a vault table. The invariant table stores the set of invariants indexed by the u-driver variable(s) that it is associated with, and is initialized when the microdriver is loaded. The vault table stores pointers to data structures in the vault area and is filled by the RPC monitor when it populates the vault area. The RPC monitor enforces invariants on data received from the u-driver by first unmarshaling this data into the vault area and inserting pointers to these resulting data structures in the vault table. This unmarshaling code is automatically generated by DriverSlicer’s code generator. The marshaling code emitted by the code generator also makes a copy of the original values of variables before an upcall to support in- variants that refer to the O value of a variable. To enforce invariants, the RPC monitor retrieves the invariants associ- ated with each variable in the vault table using the invariant table, and verifies that the invariant is satisfied. For invariants on variables that point to the head of a linked list, the RPC monitor traverses the list and ensures that the invariant is sat- isfied. Any failures raise an alert and can trigger recovery mechanisms, such as restarting the u-driver. If all invariants are satisfied, then the marshaling procotol synchronizes ker- nel data structures by overwriting them with their copies in the vault area.

4.3. Monitoring control transfers

This section describes the techniques used to extract and enforce policies on control transfers from the u-driver to the k-driver. A u-driver may issue downcalls as it serves an upcall from the k-driver. The RPC monitor enforces (Sec- tion 4.3.2) a statically extracted control transfer policy (Sec- tion 4.3.1) to ensure that the downcall is permitted. Extract- ing and enforcing such control transfer policies is necessary to prevent code injection attacks via a compromised u-driver; for example, an attacker with control over a u-driver can is- sue a downcall to a kernel function that unregisters a device, thereby causing denial of service.

4.3.1 Extracting control transfer policies To extract a control transfer policy, we employ static analysis of the u-driver. We first use DriverSlicer to statically extract a call graph of the u-driver. This call graph contains one node for each function in the u-driver; an edge f →g indicates that f can potentially call g (possibly indirectly, via a function pointer). We resolve function pointers using a simple type- based pointer analysis: each function pointer can refer to any function whose address is taken, and whose type signature matches that of the function pointer. DriverSlicer’s splitter identifies potential entrypoints into the u-driver; its code gen- erator also includes an RPC stub in the k-driver for each such entrypoint via which upcalls are issued. For each entrypoint, we use the call graph to identify the set of downcalls that the entrypoint can potentially issue—this set of downcalls associated with each entrypoint serves as the control transfer policy. Associating an upcall with a set of downcalls can result in a permissive policy that can potentially admit mimicry attacks [39]. However, we note that in order to compro- mise kernel data structures, a compromised u-driver issuing a downcall must also send appropriate data with the downcall. As discussed in Section 4.2, the RPC monitor checks the va- lidity of this data in addition to monitoring control transfer, thereby constraining the attacker. Nevertheless, our architec- ture admits the enforcement of more complex control trans- fer policies, such as the sequence of downcalls that can fol- low an upcall. Prior work has developed techniques to extract such control transfer policies (e.g., [21]); we plan to extend our architecture with such support in future work. 4.3.2 Enforcing control transfer policies The RPC monitor enforces the control transfer policy ex- tracted above. When a function in the k-driver makes an upcall into the u-driver, the k-driver registers the entrypoint invoked with the RPC monitor, which in turn pushes this en- trypoint on a stack. When the u-driver issues a downcall, the RPC monitor interposes on this request and ensures that the downcall is allowed by the control transfer policy associated with the entrypoint at the head of the stack. The RPC monitor pops the stack when the upcall returns. It is important to use a stack to track the currently-active entrypoint because an upcall into the u-driver can possibly result in multiple control transfers between the user and the kernel. DriverSlicer’s splitter ensures that there is at most one upcall along any path in the static call-graph of the driver. However, in response to an upcall, the u-driver may need to invoke a function that is implemented in the operating system kernel (e.g., the register netdev function to register a network device; note that this is a kernel function, not a k-driver function). In turn, the kernel function may call back into the driver and the relevant function may be implemented in the u-driver, thus resulting in multiple control transfers.

5. Evaluation

In this section, we report on experiments conducted on four drivers secured using our architecture. We ported the

Size of K-driver Size of U-driver Number of Annotations Driver SLOC # Functions SLOC # Functions Kernel header Driver specific 8139too 545 (33.7%) 11 (21.6%) 1070 (66.2%) 40 (78.4%) 34 8 8139cp 735 (44.7%) 21 (36.8%) 908 (55.3%) 36 (63.1%) 18 16 ens1371 890 (59.7%) 28 (43.7%) 599 (40.3%) 36 (56.3%) 7 7 uhci-hcd 2060 (81.8%) 60 (87.0%) 457 (18.2%) 9 (13.0%) 27 146

Figure 4. Sizes of the k-driver and the u-driver, and the number of annotations needed by DriverSlicer.

Driver # Funcs. in u-driver # Funcs. covered 8139too 40 35 8139cp 36 33 ens1371 36 14 uhci-hcd 9 7

Figure 5. Function coverage (in the u- driver) obtained by the training workload.

Driver # Invariants Inv. tab. Vault tab. 8139too 2607 247,661 65, 8139cp 212 17,217 14, ens1371 750 70,218 3, uhci-hcd 163 12,888 7,

Figure 6. Memory consumption (in bytes) of the invariant and vault tables.

Original driver Driver in our architecture Driver Workload Throughput CPU (%) Throughput CPU (%) 8139too TCP-send 63.39Mbps 99.76% 61.20Mbps (-3.45%) 99.86% (0%) 8139too TCP-receive 91.96Mbps 34.84% 90.35Mbps (-1.8%) 34.96% (0%) 8139cp TCP-send 64.02Mbps 99.88% 64.51Mbps (+0.7%) 99.94% (0%) 8139cp TCP-receive 90.88Mbps 31.82% 91.66Mbps (+0.8%) 29.94% (-5.9%) uhci-hcd Copy 585.84Kbps 4.92% 578.95Kbps (-1.1%) 7.01% (+42%)

Figure 7. Performance of unmodified network and USB drivers and drivers in our security architecture.

function pointer modifications within the u-driver and pre- vent control hijacking attacks.

  • Non-control data attacks. Sensitive scalar values, such as I/O memory addresses, interrupt masks and configura- tion parameters, that are marshaled between the u-driver and the k-driver can be maliciously modified by a compromised u-driver. For example, scalars that represent I/O memory address ranges, e.g., dev->base addr, which represents the base address of the driver’s I/O memory region, are set by the kernel when the driver is loaded. These values must not normally be modified by the driver because it will allow the driver write access to memory regions that it does not own, e.g., to the I/O memory regions of other devices. Yet, a compromised u-driver can maliciously modify such sensi- tive scalar values; when these values are marshaled into the k-driver, they will maliciously update kernel data as well. We simulated such an attack by modifying several non- control data values. For instance, we modified the value of rtl8139 intr mask within the u-driver. This variable repre- sents a 16-bit mask; copying this value unchecked into the k-driver will allow the driver to write to an undesired I/O port. We were able to successfully detect this attack using invariants that expressed relationships between the value of the scalar before an upcall and the value after the upcall, e.g., rtl8139 intr mask = O(rtl8139 intr mask). We also implemented an attack that modified a kernel linked list (dev->mc list) within the u-driver, and were successfully able to detect this attack using linked list invariants.
  • False positives and negatives. It is well-known that Daikon can possibly infer serendipitous invariants, i.e., those that are overly specific to the training workload. To determine

whether such invariants result in false positives during en- forcement, we ran the drivers with several benign test work- loads that called functions in the u-driver (the training work- load used to infer invariants was the same as the one in Sec- tion 5.2). We did not observe any false positives during this experiment. While it is unclear whether the same result will hold for other drivers as well, we note that in a real de- ployment, false positives could be eliminated by manually inspecting and refining the invariants. To evaluate false negatives, i.e., cases where invariants fail to detect a compromised u-driver, we conducted fault- injection experiments using the 8139too and 8139cp drivers. (We could not conduct these experiments on the ens1371 and uhci-hcd drivers because of limitations of our prototype in- frastructure.) We used an off-the-shelf fault injector [43] to inject 400 random faults in the u-driver of each microdriver. We measured the number of faults that propagated to the ker- nel (via RPC) and the number of these faults that were de- tected by our invariants. Note that our prototype currently lacks a recovery subsystem. Therefore, faults that propagate to the kernel crash the system, i.e., the RPC monitor can de- tect data corruption, but cannot prevent or recover from a system crash. Our experimental methodology was therefore to inspect system logs following each system crash to deter- mine whether the RPC monitor detected the crash. Figure 8 presents the results of this study. As this figure shows, there were several cases in which the system did not crash and in which the faults were contained within the u- driver (the #NoCrash and #UD columns, respectively). The remaining faults, which constituted the majority, propagated to the kernel, thereby showing the need for an RPC monitor

Driver Faults NoCrash UD Clear InLog Detect 8139too 400 49 26 212 113 95 (84%) 8139cp 400 134 14 147 105 64 (61%)

Figure 8. Results from fault injection.

to inspect kernel data structure updates initiated by the u- driver. As discussed above, we used system logs to determine whether the RPC monitor detected a crash. In several cases (shown in the #Clear column), we observed that the system log had been cleared following the crash. In these cases, we could not determine whether the RPC monitor would have detected the crash. Nevertheless, there were several cases in which we observed a crash for which we could inspect our logs to determine the effectiveness of invariants (shown in the #InLog column). The #Detect column shows the number of #InLog crashes that were detected by the RPC monitor. As these results indicate, the RPC monitor could detect 84% of the injected faults in the 8139too driver and 61% of the faults in the 8139cp driver. These results also show that the RPC monitor can effectively thwart a significant fraction of attacks enabled by a compromised u-driver.

5.3. Performance

We measured both the throughput and CPU utilization of the two network drivers and the USB driver using our QEMU testbed. While QEMU does not provide an accurate repre- sentation of performance on real hardware, it allows us to measure differences in performance. If the driver has lower performance, it will be reflected either as higher CPU utiliza- tion or low throughput. If neither changes, the performance on real hardware should be unchanged. We measured throughput and CPU utilization of the net- work drivers using netperf [14]. We transmitted packets be- tween our QEMU test environment and a client machine. The netperf tests used TCP receive and send buffer sizes of 87KB and 16KB, respectively. To test the USB driver, we copied a 140MB file into a USB disk. All our measurements are averaged over 10 runs, and are presented in Figure 7. As this Figure shows, our security architecture minimally im- pacts common-case performance (the minor speedups that we observed are within the margin of experimental error). This is because the code to transmit packets is in the k-driver; sending a packet does not involve any user/kernel transitions. For the sound driver, we compared the CPU utilization of both the original driver and the split driver as they played a 256-Kbps MP3; CPU utilization in both cases was zero. However, uncommon functionality, such as device ini- tialization, shutdown and configuration, resulted in several user/kernel transitions and took almost thrice as long. During the training phase of the experiments reported in Section 5.2, we used several benign workloads that exercised such func- tionality implemented in the u-driver of each device driver. Figure 9 presents the number of user/kernel transitions and the amount of data transferred in upcalls and downcalls dur- ing this training phase.

Driver KBytes sent/received # upcalls # downcalls 8139too 813 200 160 8139cp 9.5 206 124 ens1371 15.6 395 777 uhci-hcd 25.1 36 126

Figure 9. Data movement in up and downcalls.

6. Related Work

Hardware-based isolation techniques, such as Nooks [35] and Mondrix [41], rely on memory protection at the page level (Nooks) or with fine-grained segments (Mondrix) to isolate device driver failures. There are two main differences between Nooks/Mondrix and our work. First, both Nooks and Mondrix execute device drivers in kernel mode. Second, they do not enforce integrity specifications on kernel data structure updates, because doing so is likely to impose sig- nificant performance overheads. The consequence of these differences is that while Nooks and Mondrix can improve re- liability with benign but vulnerable drivers, they cannot pro- tect against compromised drivers that attempt to subvert the kernel. For example, they cannot protect against buffer over- flow exploits that maliciously modify kernel data structures. Virtual machine-based techniques isolate device drivers by running a set of device drivers within their own virtual ma- chine e.g., [16, 19, 25]. In principle, this approach offers all the benefits of our architecture. However, in practice, there are two key difficulties. First, these techniques require the use of a VMM. Although VMMs have seen wide deployment for server-class systems, they are still not in wide use on per- sonal desktops—platforms that support a wide variety of de- vices and hence, drivers. Second, VM-based techniques must provide a front-end driver within the guest VM that commu- nicates requests between the device driver (running on a sep- arate VM) and I/O requests from applications in the guest. Although such front-ends can be developed easily for stan- dard classes of drivers (e.g., network, sound, SCSI), devel- oping front-ends for other one-of-a-kind drivers, e.g., those that support non-standard ioctl interfaces, is cumbersome. Thus, while the VMM-based approach has several benefits, it is not applicable to a wide variety of devices and drivers. SafeDrive [43] and XFI [17] are language-based mech- anisms to isolate device drivers. SafeDrive is an adapta- tion of CCured [32] to protect against type-safety violations in device drivers. While SafeDrive offers low-performance overhead and compatibility, device drivers protected with SafeDrive still execute with kernel privilege. Moreover, SafeDrive only protects against type-safety violations; in contrast, our RPC monitor can protect against violations that transcend type-safety, such as requests by the u-driver to al- locate large amounts of memory, which may lead to memory exhaustion. Similarly XFI ensures control-flow integrity for device drivers. Our security architecture allows the use of any user-space security mechanism to be applied to a large fraction of device driver code without investing the effort needed to adapt these mechanisms to kernel code.

[21] J. T. Giffin, S. Jha, and B. P. Miller. Efficient context- sensitive intrusion detection. In NDSS, 2004.

[22] S. Hangal and M. S. Lam. Tracking down software bugs using automatic anomaly detection. In ICSE, 2002.

[23] Rob Johnson and David Wagner. Finding user/kernel pointer bugs with type inference. In USENIX Security Symposium, 2004.

[24] B. Leslie, P. Chubb, N. Fitzroy-Dale, S. Gotz, C. Gray, L. Macpherson, D. Potts, Y. Shen, K. Elphinstone, and G. Heiser. User-level device drivers: Achieved perfor- mance. Jour. Comp. Sci. and Tech., 20(5), 2005.

[25] J. LeVasseur, V. Uhlig, J. Stoess, and S. Gotz. Unmod- ified device driver reuse and improved system depend- ability via virtual machines. In OSDI, 2004.

[26] J. Liedtke. On μ-kernel construction. In ACM SOSP,

[27] D. Maynor. Os X kernel-mode exploitation in a weekend. http://uninformed.org/index.cgi?v= 8 &a=4.

[28] Microsoft. Architecture of the user-mode driver frame- work, 2006.

[29] Microsoft Inc. Microsoft interface definition language.

[30] Linux device driver vulnerabilities from the MITRE database. CVEs 2007-4571, 2007-05, 2007-4308, 2008-0007, 2005-0504, 2006-2935, 2006-2936, 2005- 3180, 2004-1017, 2007-4997, 2006-1368.

[31] G. C. Necula, S. McPeak, S. P. Rahul, and W. Weimer. CIL: Intermediate languages and tools for analysis and transformation. In Compiler Construction, 2002.

[32] George C. Necula, Scott McPeak, and Westley Weimer. CCured: Type-safe retrofitting of legacy code. In Sym- posium Principles of Programming Languages, 2002.

[33] N. L. Petroni, T. Fraser, A. Walters, and W. Arbaugh. An architecture for specification-based detection of se- mantic integrity violations in kernel dynamic data. In USENIX Security Symposium, 2006.

[34] N. L. Petroni and M. W. Hicks. Automated detection of persistent kernel control-flow attacks. In ACM CCS,

[35] Michael M. Swift, Brian N. Bershad, and Henry M. Levy. Improving the reliability of commodity operat- ing systems. ACM Transactions on Computer Systems, 23(1), 2005.

[36] L. Tan, E. M. Chan, R. Farivar, N. Mallick, J. C. Car- lyle, F. M. David, and R. C. Campbell. iKernel: Isolat- ing buggy and malicious device drivers using hardware virtualization support. In IEEE Intl. Symp. on Depend- able, Autonomic and Secure Computing, 2007.

struct cp private { char * IOMem regs; struct cp desc * Array(64) rx ring; ... } struct net device { void * Opaque(struct cp private) priv; struct net device * Sentinel(next,0) next; ... }

Figure 10. Structure definition from the 8139cp driver, illustrating the IOM , A , O and S annotations.

[37] L. Torvalds. UIO: Linux patch for user-mode I/O, 2007. [38] K. T. Van Maren. The Fluke device driver framework. Master’s thesis, Dept. of Computer Science, Univ. of Utah, 1999.

[39] D. Wagner and P. Soto. Mimicry attacks on host-based intrusion detection systems. In ACM CCS, 2002. [40] Dan Williams, Patrick Reynolds, Kevin Walsh, Emin Gun Sirer, and Fred B. Schneider. Device driver safety through a reference validation mechanism. In OSDI, 2008. [41] E. Witchel, J. Rhee, and K. Asanovic. Mondrix: Mem- ory isolation for linux. In ACM SOSP, 2005.

[42] M. Young, M. Accetta, R. Baron, W. Bolosky, D. Golub, R. Rashid, and A. Tevanian. Mach: A new kernel foundation for UNIX development. In Summer USENIX Conference, 1986. [43] F. Zhou, J. Condit, Z. Anderson, I. Bagrak, R. Ennals, M. Harren, G. Necula, and E. Brewer. SafeDrive: Safe and recoverable extensions using language-based tech- niques. In OSDI, 2006.

A Examples of annotations used by Driver-

Slicer

Figure 10 presents four kinds of annotations used by DriverSlicer using an example from the 8139cp network driver. These annotations are applied to structure definitions and formal parameters of functions. DriverSlicer supports eight kinds of annotations in total; these are described in de- tail in prior work on Microdrivers [20].

  • The IOM annotation, applied to the regs field of the cp private structure, informs DriverSlicer that regs points to device I/O memory. Pointers to I/O memory must be han- dled differently than pointers to kernel/device memory by the object tracker. DriverSlicer uses this annotation to gener- ate appropriate marshaling code for pointers to I/O memory pointers.
  • The A annotation informs DriverSlicer that the pointer- valued rx ring field of the cp private structure points to an array of 64 cp desc objects. DriverSlicer uses this annota- tion to marshal the entire array of objects instead of simply marshaling the object instance that rx ring points to.
  • The O annotation, which is applied to the void * point- ers, helps DriverSlicer identify the type of the object that the priv of the net device data structure points to in the 8139cp driver. DriverSlicer uses this annotation to generate marshal- ing code for the cp private data structure when it marshals the priv field of the net device structure.
  • The S annotation is applied to recursive data struc- tures such as the next field of a linked list. The annota- tion shown in Figure 10 also contains the predicate used to end linked list traversal (checking that the next field is non- NULL). DriverSlicer uses this annotation to generate code to marshal the entire linked list of elements, using next, 0 as the condition to terminate traversal. // RPC stub in the k-driver containing code for invariant enforcement. int rtl8139 init one (struct pci dev *dev, ...) { void *mbuf, *dmbuf; ... // Marshal values into mbuf. marshal (mbuf, "pdev->hdr type", pdev->hdr type); add vault tab ("pdev->hdr type", &pdev->hdr type, ORIGVAL); marshal (mbuf, "pdev->devfn", pdev->devfn); add vault tab ("pdev->devfn", &pdev->devfn, ORIGVAL); ... // Call the u-driver with marshaled data. dmbuf = do upcall ("rtl8139 init one", mbuf); // Demarshaling: copy from vault. if (checkinv rtl8139 init one(dmbuf)) { copy from vault ("pdev->hdr type", &pdev->hdr type); copy from vault ("pdev->devfn", &pdev->devfn); ... } } // RPC monitor function that unmarshals data into the vault // and checks invariants int checkinv rtl8139 init one(void *unmarshbuf) { void *ptr; ptr = unmarsh to vault (unmarshbuf, "pdev->hdr type"); add vault tab ("pdev->hdr type", ptr, MODIFVAL); ptr = unmarsh to vault (unmarshbuf, "pdev->devfn"); add vault tab ("pdev->devfn", ptr, MODIFVAL); ... if (check invariants()) return 1; else { //trigger recovery } }

Figure 11. Code snippet from the 8139too mi- crodriver showing marshaling protocol modi- fied to check for data structure invariants.

B Marshaling protocol

Figure 11 shows an example of the marshaling pro- tocol augmented to check data structure invariants. As this Figure shows, the marshaling protocol is augmented to record the original values of variables in the vault ta- ble; this is required to enforce invariants of the form var=O(var). The unmarshaling protocol (implemented in checkinv rtl8139 init one) copies values received from the u-driver into the vault area and verifies that invariants are satisfied. If so, kernel data structures are updated with values from the vault using the copy from vault function, which copies the value of a data structure/field from the vault area to the kernel.