- 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 variableusecount
to 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_USE
to 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_open
returns -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_assume
tells SATABS to ignore all traces that do not adhere to the given assumption. Therefore, whenever the lock is held,dummy_read
will return a value between 0 andmax
. If the lock is not held, thendummy_read
returns -1.dummy_release
: (in driver.c) If the lock is held, thendummy_release
decreases 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
).