- Daniel Kroening
- SMT Lists/Sets/Maps
- Google groups:
SATABS – Predicate Abstraction with SAT
Tutorials
Example: Reference Counting in Linux Device Drivers
Microsoft's SLAM toolkit has been successfully used to find bugs in Windows device drivers. SLAM automatically verifies device driver whether a device driver adheres to a set of specifications. SLAM provides a test harness for device drivers that calls the device driver dispatch routines in a non-deterministic order. Therefore, the Model Checker examines all combinations of calls. Motivated by the success this approach, we provide a toy example based on Linux device drivers. For a more complete approach to the verification of Linux device drivers, consider DDVerify.
Dynamically loadable modules enable the Linux Kernel to load device drivers
on demand and to release them when they are not needed anymore. When a
device driver is registered, the kernel provides a major number that is used
to uniquely identify the device driver. The corresponding device can be
accessed through special files in the filesystem; by convention, they are
located in the /dev directory. If a process accesses a device file
the kernel calls the corresponding open, read and
write functions of the device driver. Since a driver must not be
released by the kernel as long as it is used by at least one process, the
device driver must maintain a usage counter (in more recent Linux kernels, this
is done automatically, however, drivers that must maintain backward
compatibility have to adjust this counter).
We provide a skeleton of such a driver. Download the files spec.c, driver.c, driver.h, kdev_t.h, and modules.h.
The driver contains following functions:
register_chrdev: (in spec.c) Registers a character device. In our implementation, the function sets the variableusecountto zero and returns a major number for this device (a constant, if the user provides 0 as argument for the major number, and the value specified by the user otherwise).int usecount; int register_chrdev (unsigned int major, const char* name) { usecount = 0; if (major == 0) return MAJOR_NUMBER; return major; }unregister_chrdev: (in spec.c) Unregisters a character device. This function asserts that the device is not used by any process anymore (we use the macroMOD_IN_USEto check this).int unregister_chrdev (unsigned int major, const char* name) { if (MOD_IN_USE) { ERROR: assert (0); } else return 0; }dummy_open: (in driver.c) This function increases theusecount. If the device is locked by some other processdummy_openreturns -1. Otherwise it locks the device for the caller.dummy_read: (in driver.c) This function "simulates" a read access to the device. In fact it does nothing, since we are currently not interested in the potential buffer overflow that may result from a call to this function. Note the usage of the functionnondet_int: This is an internal SATABS-function that nondeterministically returns an arbitrary integer value. The function__CPROVER_assumetells SATABS to ignore all traces that do not adhere to the given assumption. Therefore, whenever the lock is held,dummy_readwill return a value between 0 andmax. If the lock is not held, thendummy_readreturns -1.dummy_release: (in driver.c) If the lock is held, thendummy_releasedecreases theusecount, releases the lock, and returns 0. Otherwise, the function returns -1.
We now want to check if any valid sequence of calls of the
dispatch functions (in driver.c) can lead to the violation
of the assertion (in spec.c).
Obviously, a call to dummy_open that is immediately followed
by a call to unregister_chrdev violates the assertion.
The function main in spec.c gives an example of
how these functions are called. First, a character device
"dummy" is registered. The major number is stored
in the inode structure of the device. The values
for the file structure are assigned non-deterministically.
We rule out invalid sequences of calls by ensuring
that no device is unregistered while it is still locked.
We use the following model checking harness for calling the
dispatching functions:
random = nondet_uchar ();
__CPROVER_assume (0 <= random && random <= 3);
switch (random)
{
case 1:
rval = dummy_open (&inode, &my_file);
if (rval == 0)
lock_held = TRUE;
break;
case 2:
__CPROVER_assume (lock_held);
count = dummy_read (&my_file, buffer, BUF_SIZE);
break;
case 3:
dummy_release (&inode, &my_file);
lock_held = FALSE;
break;
default:
break;
}
The variable random is assigned non-deterministically.
Subsequently, the value of random
is restricted to be 0 &le random ≤ 3 by a call to
__CPROVER_assume. Whenever the value of random is
not in this interval, the corresponding execution trace is simply
discarded by SATABS. Depending on the value of random, the
harness calls either dummy_open, dummy_read or
dummy_close. Therefore,
if there is a sequence of calls to these three
functions that leads to a violation of the assertion in
unregister_chrdev, then SATABS
will eventually consider it.
If we ask SATABS to show us the properties it verifies with
satabs driver.c spec.c --show-properties
for our example, we obtain
Claim unregister_chrdev.1:
file spec.c line 18 function unregister_chrdev
MOD_IN_USE in unregister_chrdev
FALSEClaim dummy_open.1:
file driver.c line 15 function dummy_open
i_rdev mismatch
(unsigned int)inode->i_rdev >> 8 == (unsigned int)dummy_major
It seems obvious that the property dummy_open.1 can never be violated. SATABS confirms this assumption: We call
satabs driver.c spec.c --property dummy_open.1
and SATABS reports VERIFICATION SUCCESSFUL after a few iterations.
If we try to verify property unregister_chrdev.1, SATABS
reports that the property in line 18 in file spec.c is violated (i.e., the
assertion does not hold, therefore the VERIFICATION FAILED).
Furthermore, SATABS provides a detailed description of the problem in the
form of a counterexample (i.e., an execution trace that violates the
property). On this trace, dummy_open is called twice,
leading to a usecount of 2. The second call of course fails
with rval=-1, but the counter is increased nevertheless:
int dummy_open (struct inode *inode, struct file *filp)
{
__CPROVER_assert(MAJOR (inode->i_rdev) == dummy_major,
"i_rdev mismatch");
MOD_INC_USE_COUNT;
if (locked)
return -1;
locked = TRUE;
return 0; /* success */
}
Then, dummy_release is called to release the lock on the
device. Finally, the loop is left and the call to
unregister_chrdev results in a violation of the assertion
(since usecount is still 1, even though locked=0).

