1 : const char *dir = "/mnt/sbd0/test-dir";
2 : const char *file = "/mnt/sbd0/test-file";
3 : static void do
fsync(const char *fn) {
4 : int fd = open(fn, O
RDONLY);
5 : fsync(fd);
6 : close(fd);
7 : }
8 : void FsChecker::mutate(void) {
9 : switch(choose(4)) {
10: case 0: systemf("mkdir %s%d", dir, choose(5)); break;
11: case 1: systemf("rmdir %s%d", dir, choose(5)); break;
12: case 2: systemf("rm %s", file); break;
13: case 3: systemf("echo \"test\" > %s", file);
14: if(choose(2) == 0)
15: sync();
16: else {
17: do
fsync(file);
18: // fsync parent to commit the new directory entry
19: do
fsync("/mnt/sbd0");
20: }
21: check
crash now(); // invokes check() for each crash
22: break;
23: }
24: }
25: void FsChecker::check(void) {
26: ifstream in(file);
27: if(!in)
28: error("fs", "file gone!");
29: char buf[1024];
30: in.read(buf, sizeof buf);
31: in.close();
32: if(strncmp(buf, "test", 4) != 0)
33: error("fs", "wrong file contents!");
34: }
Figure 2: Example file system checker. We omit the class initialization
code and some sanity checks.
Checkers range from aggressively system-specific (or
even code-version specific) to the fairly generic. Their
size scales with the complexity of the invariants checked,
from a few tens to many thousands of lines.
Figure 2 shows a file system checker that checks a
simple correctness property: a file that has been syn-
chronously written to disk (using either the fsync or
sync system calls) should persist after a crash. Mail
servers, databases and other application storage systems
depend on this behavior to prevent crash-caused data
obliteration. While simple, the checker illustrates com-
mon features of many checkers, including the fact that it
catches some interesting bugs.
The mutate method calls choose(4) (line 9) to
fork and do each of four possible actions: (1) create a
directory, (2) delete it, (3) create a test file, or (4) delete
it. The first two actions then call choose(5) and cre-
ate or delete one of five directories (the directory name is
based on choose’s return value). The file creation ac-
tion calls choose(2) (line 14) and forces the test file to
disk using sync in one child and fsync in the other. As
Figure 3 shows, one mutate call creates thirteen chil-
Figure 3: Choices made by one invocation of the mutate method in
Figure 2’s checker. It creates thirteen children.
dren.
The checker calls EXPLODE to check crashes. While
other code in the system can also initiate such check-
ing, typically it is the mutate method’s responsibil-
ity: it issues operations that change the storage sys-
tem, so it knows the correct system state and when
this state changes. In our example, after mutate
forces the file to disk it calls the EXPLODE routine
check
crash now(). EXPLODE then generates all
crash disks at the exact moment of the call and invokes
the check method on each after repairing and mounting
it using the underlying storage component (see § 3.3).
The check method checks if the test file exists (line 27)
and has the right contents (line 32). While simple, this
exact checker catches an interesting bug in JFS where
upon crash, an fsync’d file loses all its contents trig-
gered by the corner-case reuse of a directory inode as a
file inode (§7.3 discusses a more sophisticated version of
this checker).
So far we have described how a single mutate call
works. The next section shows how it fits in the check-
ing process. In addition, checking crashes at only a sin-
gle code point is crude; Section 6 describes the routines
EXPLODE provides for more comprehensive checking.
3.3 Setting up checked code: Storage components
Since EXPLODE checks live storage systems, these sys-
tems must be up and running. For each storage subsys-
tem involved in checking, clients provide a storage com-
ponent that implements five methods:
1. init: one-time initialization, such as formatting a
file system partition or creating a fresh database.
2. mount: set up the storage system so that operations
can be performed on it.
3. unmount: tear down the storage system; used by
EXPLODE to clear the storage system’s state so it can
explore a different one (§5.2).
4. recover: repair the storage system after an EX-
PLODE-simulated crash.
5. threads: return the thread IDs for the storage
system’s kernel threads. EXPLODE reduces non-
determinism by only running these threads when it
wants to (§5.2).