/* * Copyright (c) 2013, 2015 * The President and Fellows of Harvard College. * Written by David A. Holland. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include "name.h" #include "data.h" #include "pool.h" #include "check.h" /* * Check pass. * * We first replay the workload into a model that keeps track of what * should be on the volume at each step; then we inspect the real * volume and compare it to the model. */ //////////////////////////////////////////////////////////// // model representation #define UNKNOWN_ID ((unsigned)-1) /* * 1. log of changes to a filesystem * (this is not quite fully general but supports only things we do) */ enum fschanges { FC_NEWFS, /* create the volume */ FC_TRUNCATE, /* truncate a file */ FC_WRITE, /* write to a file */ FC_CREAT, /* create a file */ FC_MKDIR, /* create a directory */ FC_RMDIR, /* remove a directory */ FC_UNLINK, /* remove a file */ FC_LINK, /* hardlink a file */ FC_RENAMEFILE, /* rename a file */ FC_RENAMEDIR, /* rename a directory */ }; struct fschange { /* all changes are kept in order on a doubly linked list */ struct fschange *prev; struct fschange *next; /* version number for this change */ unsigned version; /* whether this change reflects a partially committed operation */ int partial; /* the change type, parameters, and metadata */ enum fschanges type; union { struct { /* object id of the root directory */ unsigned rootdirnum; } fc_newfs; struct { /* cached: previous change affecting this file */ struct fschange *prev_thisfile; /* truncate params: file oid and new length */ unsigned file; off_t len; } fc_truncate; struct { /* cached: previous change affecting this file */ struct fschange *prev_thisfile; /* write params: file oid, position, length */ unsigned file; off_t pos; off_t len; /* size of file before the write */ off_t oldfilesize; /* key for generating the write contents */ unsigned code; unsigned seq; } fc_write; struct { /* cached: previous change affecting containing dir */ struct fschange *prev_thisdir; /* creat params: containing dir oid, name, new oid */ unsigned dir; unsigned name; unsigned newfile; } fc_creat; struct { /* cached: previous change affecting containing dir */ struct fschange *prev_thisdir; /* mkdir params: containing dir oid, name, new oid */ unsigned dir; unsigned name; unsigned newdir; } fc_mkdir; struct { /* cached: previous change affecting either dir */ struct fschange *prev_thisdir; struct fschange *prev_victimdir; /* rmdir params */ unsigned dir; unsigned name; unsigned victimdir; } fc_rmdir; struct { /* cached: previous change affecting either object */ struct fschange *prev_thisdir; struct fschange *prev_victimfile; /* unlink params */ unsigned dir; unsigned name; unsigned victimfile; } fc_unlink; struct { /* cached: previous change affecting each object */ struct fschange *prev_fromdir; struct fschange *prev_todir; struct fschange *prev_thisfile; /* link params */ unsigned fromdir; unsigned fromname; unsigned todir; unsigned toname; unsigned file; } fc_link; struct { /* cached: previous change affecting each object */ struct fschange *prev_fromdir; struct fschange *prev_todir; struct fschange *prev_movedfile; /* rename params */ unsigned fromdir; unsigned fromname; unsigned todir; unsigned toname; unsigned movedfile; } fc_renamefile; struct { /* cached: previous change affecting each object */ struct fschange *prev_fromdir; struct fschange *prev_todir; struct fschange *prev_moveddir; /* rename params */ unsigned fromdir; unsigned fromname; unsigned todir; unsigned toname; unsigned moveddir; } fc_renamedir; }; }; /* * 2. representation of a current (visible) filesystem state * * The state starts at the root directory; each directory is a singly * linked list of directory entries, each of which holds another * directory or a file. We don't keep track of file contents, only the * length. * * The identity field is within the union structure because file and * directory object ids are different namespaces. */ struct fsdirent { unsigned name; struct fsobject *obj; struct fsdirent *next; }; struct fsobject { unsigned isdir; unsigned refcount; union { struct { unsigned identity; off_t len; } obj_file; struct { unsigned identity; struct fsdirent *entries; struct fsobject *parent; } obj_dir; }; }; //////////////////////////////////////////////////////////// // allocator pools /* * Allocate from static space instead of using malloc, to help avoid * unnecessary lossage on kernels where malloc doesn't work. */ #define MAXCHANGES 16384 #define MAXOBJECTS 16384 #define MAXDIRENTS 16384 DECLPOOL(fschange, MAXCHANGES); DECLPOOL(fsobject, MAXOBJECTS); DECLPOOL(fsdirent, MAXDIRENTS); static struct fschange * getchange(void) { return POOLALLOC(fschange); } static struct fsobject * getobject(void) { return POOLALLOC(fsobject); } static struct fsdirent * getdirent(void) { return POOLALLOC(fsdirent); } #if 0 static void putchange(struct fschange *fc) { POOLFREE(fschange, fc); } #endif static void putobject(struct fsobject *obj) { POOLFREE(fsobject, obj); } static void putdirent(struct fsdirent *dirent) { POOLFREE(fsdirent, dirent); } //////////////////////////////////////////////////////////// // record constructors for change records static struct fschange * fc_create(enum fschanges type) { struct fschange *fc; fc = getchange(); fc->prev = fc->next = NULL; fc->version = 0; fc->partial = 0; fc->type = type; return fc; } static struct fschange * fc_create_newfs(unsigned rootdirnum) { struct fschange *fc; fc = fc_create(FC_NEWFS); fc->fc_newfs.rootdirnum = rootdirnum; return fc; } static struct fschange * fc_create_truncate(struct fschange *prev_thisfile, unsigned file, off_t len) { struct fschange *fc; fc = fc_create(FC_TRUNCATE); fc->fc_truncate.prev_thisfile = prev_thisfile; fc->fc_truncate.file = file; fc->fc_truncate.len = len; return fc; } static struct fschange * fc_create_write(struct fschange *prev_thisfile, unsigned file, off_t pos, off_t len, off_t oldfilesize, unsigned code, unsigned seq) { struct fschange *fc; fc = fc_create(FC_WRITE); fc->fc_write.prev_thisfile = prev_thisfile; fc->fc_write.file = file; fc->fc_write.pos = pos; fc->fc_write.len = len; fc->fc_write.oldfilesize = oldfilesize; fc->fc_write.code = code; fc->fc_write.seq = seq; return fc; } static struct fschange * fc_create_creat(struct fschange *prev_thisdir, unsigned dir, unsigned name, unsigned newfile) { struct fschange *mdc; mdc = fc_create(FC_CREAT); mdc->fc_creat.prev_thisdir = prev_thisdir; mdc->fc_creat.dir = dir; mdc->fc_creat.name = name; mdc->fc_creat.newfile = newfile; return mdc; } static struct fschange * fc_create_mkdir(struct fschange *prev_thisdir, unsigned dir, unsigned name, unsigned newdir) { struct fschange *mdc; mdc = fc_create(FC_MKDIR); mdc->fc_mkdir.prev_thisdir = prev_thisdir; mdc->fc_mkdir.dir = dir; mdc->fc_mkdir.name = name; mdc->fc_mkdir.newdir = newdir; return mdc; } static struct fschange * fc_create_rmdir(struct fschange *prev_thisdir, struct fschange *prev_victimdir, unsigned dir, unsigned name, unsigned victimdir) { struct fschange *mdc; mdc = fc_create(FC_RMDIR); mdc->fc_rmdir.prev_thisdir = prev_thisdir; mdc->fc_rmdir.prev_victimdir = prev_victimdir; mdc->fc_rmdir.dir = dir; mdc->fc_rmdir.name = name; mdc->fc_rmdir.victimdir = victimdir; return mdc; } static struct fschange * fc_create_unlink(struct fschange *prev_thisdir, struct fschange *prev_victimfile, unsigned dir, unsigned name, unsigned victimfile) { struct fschange *mdc; mdc = fc_create(FC_UNLINK); mdc->fc_unlink.prev_thisdir = prev_thisdir; mdc->fc_unlink.prev_victimfile = prev_victimfile; mdc->fc_unlink.dir = dir; mdc->fc_unlink.name = name; mdc->fc_unlink.victimfile = victimfile; return mdc; } static struct fschange * fc_create_link(struct fschange *prev_fromdir, struct fschange *prev_todir, struct fschange *prev_thisfile, unsigned fromdir, unsigned fromname, unsigned todir, unsigned toname, unsigned file) { struct fschange *mdc; mdc = fc_create(FC_LINK); mdc->fc_link.prev_fromdir = prev_fromdir; mdc->fc_link.prev_todir = prev_todir; mdc->fc_link.prev_thisfile = prev_thisfile; mdc->fc_link.fromdir = fromdir; mdc->fc_link.fromname = fromname; mdc->fc_link.todir = todir; mdc->fc_link.toname = toname; mdc->fc_link.file = file; return mdc; } static struct fschange * fc_create_renamefile(struct fschange *prev_fromdir, struct fschange *prev_todir, struct fschange *prev_movedfile, unsigned fromdir, unsigned fromname, unsigned todir, unsigned toname, unsigned movedfile) { struct fschange *mdc; mdc = fc_create(FC_RENAMEFILE); mdc->fc_renamefile.prev_fromdir = prev_fromdir; mdc->fc_renamefile.prev_todir = prev_todir; mdc->fc_renamefile.prev_movedfile = prev_movedfile; mdc->fc_renamefile.fromdir = fromdir; mdc->fc_renamefile.fromname = fromname; mdc->fc_renamefile.todir = todir; mdc->fc_renamefile.toname = toname; mdc->fc_renamefile.movedfile = movedfile; return mdc; } static struct fschange * fc_create_renamedir(struct fschange *prev_fromdir, struct fschange *prev_todir, struct fschange *prev_moveddir, unsigned fromdir, unsigned fromname, unsigned todir, unsigned toname, unsigned moveddir) { struct fschange *fc; fc = fc_create(FC_RENAMEDIR); fc->fc_renamedir.prev_fromdir = prev_fromdir; fc->fc_renamedir.prev_todir = prev_todir; fc->fc_renamedir.prev_moveddir = prev_moveddir; fc->fc_renamedir.fromdir = fromdir; fc->fc_renamedir.fromname = fromname; fc->fc_renamedir.todir = todir; fc->fc_renamedir.toname = toname; fc->fc_renamedir.moveddir = moveddir; return fc; } //////////////////////////////////////////////////////////// // constructors/destructors for current state objects static struct fsdirent * fsdirent_create(unsigned name, struct fsobject *obj) { struct fsdirent *ret; ret = getdirent(); ret->name = name; ret->obj = obj; ret->next = NULL; return ret; } static void fsdirent_destroy(struct fsdirent *de) { assert(de->obj == NULL); assert(de->next == NULL); putdirent(de); } static struct fsobject * fsobject_create_file(unsigned id) { struct fsobject *ret; ret = getobject(); ret->isdir = 0; ret->refcount = 1; ret->obj_file.identity = id; ret->obj_file.len = 0; return ret; } static struct fsobject * fsobject_create_dir(unsigned id, struct fsobject *parent) { struct fsobject *ret; ret = getobject(); ret->isdir = 1; ret->refcount = 1; ret->obj_dir.identity = id; ret->obj_dir.entries = NULL; ret->obj_dir.parent = parent; return ret; } static void fsobject_incref(struct fsobject *obj) { assert(obj->refcount > 0); obj->refcount++; assert(obj->refcount > 0); } static void fsobject_decref(struct fsobject *obj) { assert(obj->refcount > 0); obj->refcount--; if (obj->refcount > 0) { return; } if (obj->isdir) { assert(obj->obj_dir.entries == NULL); } putobject(obj); } static void fsobject_destroytree(struct fsobject *obj) { struct fsdirent *de; if (obj->isdir) { while (obj->obj_dir.entries != NULL) { de = obj->obj_dir.entries; obj->obj_dir.entries = de->next; de->next = NULL; fsobject_destroytree(de->obj); de->obj = NULL; fsdirent_destroy(de); } } fsobject_decref(obj); } //////////////////////////////////////////////////////////// // operations on current state objects /* * Bail out if something's broken. */ static void die(const char *fmt, ...) { char buf[256]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); errx(1, "Inconsistency: %s", buf); } #if 0 /* not used */ /* * Count a list of directory entries. */ static unsigned fsdirent_count(struct fsdirent *de) { if (de == NULL) { return 0; } return 1 + fsdirent_count(de->next); } #endif /* * Add an entry to a directory. */ static void fsdir_add_entry(struct fsobject *dir, struct fsdirent *nde) { struct fsdirent *ode; assert(dir->isdir); for (ode = dir->obj_dir.entries; ode != NULL; ode = ode->next) { if (ode->name == nde->name) { die("In directory %u, %s already existed", dir->obj_dir.identity, name_get(nde->name)); } } nde->next = dir->obj_dir.entries; dir->obj_dir.entries = nde; } /* * Find an entry in a directory by name. If CROAK is set, bail out if * it's not there. */ static struct fsdirent * fsdir_find_entry(struct fsobject *dir, unsigned name, int croak) { struct fsdirent *de; assert(dir->isdir); for (de = dir->obj_dir.entries; de != NULL; de = de->next) { if (de->name == name) { return de; } } if (croak) { die("In directory %u, did not find %s", dir->obj_dir.identity, name_get(name)); } return NULL; } /* * Remove an entry from a directory. */ static struct fsdirent * fsdir_remove_entry(struct fsobject *dir, unsigned name) { struct fsdirent *de, **prevptr; assert(dir->isdir); prevptr = &dir->obj_dir.entries; for (de = *prevptr; de != NULL; prevptr = &de->next, de = *prevptr) { if (de->name == name) { *prevptr = de->next; de->next = NULL; return de; } } die("In directory %u, did not find %s", dir->obj_dir.identity, name_get(name)); return NULL; } //////////////////////////////////////////////////////////// // apply a change record to a current state static struct fsobject * findsub(struct fsobject *obj, unsigned isdir, unsigned id) { struct fsobject *ret; struct fsdirent *de; unsigned objid; /* Are we on the object we're looking for? */ objid = obj->isdir ? obj->obj_dir.identity : obj->obj_file.identity; if (obj->isdir == isdir && objid == id) { return obj; } /* If the object we're on isn't a dir, can't search any further */ if (!obj->isdir) { return NULL; } for (de = obj->obj_dir.entries; de != NULL; de = de->next) { ret = findsub(de->obj, isdir, id); if (ret != NULL) { return ret; } } return NULL; } #if 0 static struct fsobject * findfile(struct fsobject *rootdir, unsigned id) { struct fsobject *ret; ret = findsub(rootdir, 0/*!isdir*/, id); if (ret == NULL) { die("File %u not found in current state", id); } return ret; } #endif static struct fsobject * findfile_maybe(struct fsobject *rootdir, unsigned id) { return findsub(rootdir, 0/*!isdir*/, id); } static struct fsobject * finddir(struct fsobject *rootdir, unsigned id) { struct fsobject *ret; ret = findsub(rootdir, 1/*isdir*/, id); if (ret == NULL) { die("Directory %u not found in current state", id); } return ret; } /* * Apply change CHANGE to the volume state encoded in/under ROOTDIR. */ static void apply_change(struct fsobject *rootdir, struct fschange *change) { struct fsobject *obj1, *obj2; struct fsdirent *de; off_t endpos; assert(rootdir->isdir); switch (change->type) { case FC_NEWFS: /* * Creating the volume; crosscheck the root directory. */ assert(rootdir->isdir); assert(rootdir->refcount == 1); assert(rootdir->obj_dir.identity == change->fc_newfs.rootdirnum); assert(rootdir->obj_dir.entries == NULL); assert(rootdir->obj_dir.parent == rootdir); break; case FC_TRUNCATE: /* * Truncate a file. (Only files, not directories.) * * Because truncates can and do get posted after a * file is unlinked, we have to tolerate not finding * the file. We don't model unlinked files, because * they aren't visible and our goal is to check the * visible/observable state. */ obj1 = findfile_maybe(rootdir, change->fc_truncate.file); if (obj1 != NULL) { obj1->obj_file.len = change->fc_truncate.len; } break; case FC_WRITE: /* * Write to a file. (Only files, not directories.) * All this actually does is update the file size if * needed. * * We also have to tolerate writes to unlinked files. */ obj1 = findfile_maybe(rootdir, change->fc_write.file); if (obj1 != NULL) { endpos = change->fc_write.pos + change->fc_write.len; if (obj1->obj_file.len < endpos) { obj1->obj_file.len = endpos; } } break; case FC_CREAT: /* * Create a file. This creates a new object and a * directory entry to hold it, then inserts the * directory entry in the containing directory. */ obj1 = finddir(rootdir, change->fc_creat.dir); obj2 = fsobject_create_file(change->fc_creat.newfile); de = fsdirent_create(change->fc_creat.name, obj2); fsdir_add_entry(obj1, de); break; case FC_MKDIR: /* * Create a directory. The same as creating a file, * except the new object is a directory. */ obj1 = finddir(rootdir, change->fc_mkdir.dir); obj2 = fsobject_create_dir(change->fc_mkdir.newdir, obj1); de = fsdirent_create(change->fc_mkdir.name, obj2); fsdir_add_entry(obj1, de); break; case FC_RMDIR: /* * Remove a directory. First take out the directory * entry (by name); then examine the object found; * then delete everything. * * XXX: why do these checks use assert vs. testing and * calling die()? */ obj1 = finddir(rootdir, change->fc_rmdir.dir); de = fsdir_remove_entry(obj1, change->fc_rmdir.name); obj2 = de->obj; de->obj = NULL; assert(obj2->isdir); assert(obj2->obj_dir.entries == NULL); assert(obj2->obj_dir.identity == change->fc_rmdir.victimdir); assert(obj2->obj_dir.parent == obj1); fsdirent_destroy(de); fsobject_decref(obj2); break; case FC_UNLINK: /* * Remove a file. Much the same as removing a directory. */ obj1 = finddir(rootdir, change->fc_unlink.dir); de = fsdir_remove_entry(obj1, change->fc_unlink.name); obj2 = de->obj; de->obj = NULL; assert(!obj2->isdir); assert(obj2->obj_file.identity == change->fc_unlink.victimfile); fsdirent_destroy(de); fsobject_decref(obj2); break; case FC_LINK: /* * Hard-link a file. */ obj1 = finddir(rootdir, change->fc_link.fromdir); de = fsdir_find_entry(obj1, change->fc_link.fromname, 1); obj2 = de->obj; assert(!obj2->isdir); assert(obj2->obj_file.identity == change->fc_link.file); obj1 = finddir(rootdir, change->fc_link.todir); fsobject_incref(obj2); de = fsdirent_create(change->fc_link.toname, obj2); fsdir_add_entry(obj1, de); break; case FC_RENAMEFILE: /* * Rename a file. XXX: this appears to do the wrong * thing if you rename one file over another. */ obj1 = finddir(rootdir, change->fc_renamefile.fromdir); de = fsdir_remove_entry(obj1, change->fc_renamefile.fromname); obj2 = de->obj; assert(!obj2->isdir); assert(obj2->obj_file.identity == change->fc_renamefile.movedfile); obj1 = finddir(rootdir, change->fc_renamefile.todir); de->name = change->fc_renamefile.toname; fsdir_add_entry(obj1, de); break; case FC_RENAMEDIR: /* * Rename a directory. Same as renaming a file, except * that we update the parent of the directory being * moved. XXX: also seems to do the wrong thing if you * rename one directory over another. And, XXX: should * this be updating the refcount as the parent * changes? Shouldn't the refcount be the same as the * on-disk linkcount, and shouldn't the on-disk * linkcount be part of the state we examine and * verify? */ obj1 = finddir(rootdir, change->fc_renamedir.fromdir); de = fsdir_remove_entry(obj1, change->fc_renamedir.fromname); obj2 = de->obj; assert(obj2->isdir); assert(obj2->obj_dir.identity == change->fc_renamedir.moveddir); assert(obj2->obj_dir.parent == obj1); obj1 = finddir(rootdir, change->fc_renamedir.todir); de->name = change->fc_renamedir.toname; obj2->obj_dir.parent = obj1; fsdir_add_entry(obj1, de); break; } } //////////////////////////////////////////////////////////// // global fs state /* * The change records. FIRSTCHANGE is the first change record; CHANGES * is the list root, which given the current linked list implementation * is the *last* (most recent) change record. */ static struct fschange *firstchange; static struct fschange *changes; /* * The current volume state that we're working with. As we add change * records while replaying the workload, we roll this forward so we * can inspect it; for some things this is necessary to be able to * replay the workload fully. */ static struct fsobject *state; /* * The next oids of each type to assign. File and directory ids are * counted independently, which might or might not have been a good * idea. */ static unsigned nextfilenum, nextdirnum; /* * Add a new change record. This allocates the version number; * however, the change record should otherwise be fully filled in * already. * * usage: fc_attach(newrecord); */ static void fc_attach(struct fschange *new) { struct fschange *prev; prev = changes; assert(prev->next == NULL); assert(new->prev == NULL); assert(new->next == NULL); prev->next = new; new->prev = prev; new->version = prev->version + 1; changes = new; apply_change(state, new); } /* * Rewind the volume state kept in the global STATE to the beginning. */ static void rewindstate(void) { /* If we already have a state, throw it away. */ if (state != NULL) { fsobject_destroytree(state); } assert(firstchange->type == FC_NEWFS); state = fsobject_create_dir(firstchange->fc_newfs.rootdirnum, NULL); /* root directory's parent is itself */ state->obj_dir.parent = state; } /* * Roll the volume state kept in STATE forward to a specific change * entry. */ static void advancestateto(struct fschange *targetchange) { struct fschange *change; for (change = firstchange; change != NULL; change = change->next) { apply_change(state, change); if (change == targetchange) { return; } } assert(0); } //////////////////////////////////////////////////////////// // lookup in the fs state // (used during model construction) /* * Find the most recent previous record that mentions a particular file. * * If there isn't one, something's wrong; because we start with a * fresh volume every file that ever exists must have been created at * some point. * * This is used while replaying the workload and accumulating change * records, so it can always just start from the most recent end * instead of from a specific place. */ static struct fschange * changes_findprevfile(unsigned filenum) { struct fschange *here; for (here = changes; here != NULL; here = here->prev) { switch (here->type) { case FC_NEWFS: break; case FC_TRUNCATE: if (here->fc_truncate.file == filenum) { return here; } break; case FC_WRITE: if (here->fc_write.file == filenum) { return here; } break; case FC_CREAT: if (here->fc_creat.newfile == filenum) { return here; } break; case FC_MKDIR: case FC_RMDIR: break; case FC_UNLINK: if (here->fc_unlink.victimfile == filenum) { return here; } break; case FC_LINK: if (here->fc_link.file == filenum) { return here; } break; case FC_RENAMEFILE: if (here->fc_renamefile.movedfile == filenum) { return here; } break; case FC_RENAMEDIR: break; } } die("No previous record for file %u", filenum); return NULL; } /* * Find the most recent previous record that mentions a particular dir. * * Likewise, if there isn't one, something's wrong. */ static struct fschange * changes_findprevdir(unsigned dirnum) { struct fschange *here; for (here = changes; here != NULL; here = here->prev) { switch (here->type) { case FC_NEWFS: if (here->fc_newfs.rootdirnum == dirnum) { return here; } break; case FC_TRUNCATE: case FC_WRITE: break; case FC_CREAT: if (here->fc_creat.dir == dirnum) { return here; } break; case FC_MKDIR: if (here->fc_mkdir.dir == dirnum || here->fc_mkdir.newdir == dirnum) { return here; } break; case FC_RMDIR: if (here->fc_rmdir.dir == dirnum || here->fc_rmdir.victimdir == dirnum) { return here; } break; case FC_UNLINK: if (here->fc_unlink.dir == dirnum) { return here; } break; case FC_LINK: if (here->fc_link.fromdir == dirnum || here->fc_link.todir == dirnum) { return here; } break; case FC_RENAMEFILE: if (here->fc_renamefile.fromdir == dirnum || here->fc_renamefile.todir == dirnum) { return here; } break; case FC_RENAMEDIR: if (here->fc_renamedir.fromdir == dirnum || here->fc_renamedir.todir == dirnum || here->fc_renamedir.moveddir == dirnum) { return here; } break; } } die("No previous record for directory %u", dirnum); return NULL; } #if 0 /* * Back up to the previous record mentioning the same file. * * XXX: No longer used and should be removed. */ static struct fschange * fs_prevforfile(struct fschange *fc, unsigned filenum) { switch (fc->type) { case FC_TRUNCATE: if (fc->fc_truncate.file == filenum) { return fc->fc_truncate.prev_thisfile; } break; case FC_WRITE: if (fc->fc_write.file == filenum) { return fc->fc_write.prev_thisfile; } break; case FC_UNLINK: if (fc->fc_unlink.victimfile == filenum) { return fc->fc_unlink.prev_victimfile; } break; case FC_LINK: if (fc->fc_link.file == filenum) { return fc->fc_link.prev_thisfile; } break; case FC_RENAMEFILE: if (fc->fc_renamefile.movedfile == filenum) { return fc->fc_renamefile.prev_movedfile; } break; default: break; } return fc->prev; } #endif #if 0 /* * Back up to the previous record mentioning the same directory. * * XXX: No longer used and should be removed. */ static struct fschange * fs_prevfordir(struct fschange *fc, unsigned dirnum) { switch (fc->type) { case FC_CREAT: if (fc->fc_creat.dir == dirnum) { return fc->fc_creat.prev_thisdir; } break; case FC_MKDIR: if (fc->fc_mkdir.dir == dirnum) { return fc->fc_mkdir.prev_thisdir; } break; case FC_RMDIR: if (fc->fc_rmdir.dir == dirnum) { return fc->fc_rmdir.prev_thisdir; } if (fc->fc_rmdir.victimdir == dirnum) { return fc->fc_rmdir.prev_victimdir; } break; case FC_UNLINK: if (fc->fc_unlink.dir == dirnum) { return fc->fc_unlink.prev_thisdir; } break; case FC_LINK: if (fc->fc_link.fromdir == dirnum) { return fc->fc_link.prev_fromdir; } if (fc->fc_link.todir == dirnum) { return fc->fc_link.prev_todir; } break; case FC_RENAMEFILE: if (fc->fc_renamefile.fromdir == dirnum) { return fc->fc_renamefile.prev_fromdir; } if (fc->fc_renamefile.todir == dirnum) { return fc->fc_renamefile.prev_todir; } break; case FC_RENAMEDIR: if (fc->fc_renamedir.fromdir == dirnum) { return fc->fc_renamedir.prev_fromdir; } if (fc->fc_renamedir.todir == dirnum) { return fc->fc_renamedir.prev_todir; } if (fc->fc_renamedir.moveddir == dirnum) { return fc->fc_renamedir.prev_moveddir; } break; default: break; } return fc->prev; } /* * Find an object by name and containing directory by searching the * change history. * * No longer used (instead we maintain the current state and search * that, which is a lot simpler) and should probably be removed. */ struct findresult { int isfile; unsigned objnum; }; static int fs_find(unsigned dirnum, unsigned name, struct findresult *res) { struct fschange *here; for (here = fs; here != NULL; here = fs_prevfordir(here, dirnum)) { switch (here->type) { case FC_NEWFS: case FC_TRUNCATE: case FC_WRITE: break; case FC_CREAT: if (here->fc_creat.dir == dirnum && here->fc_creat.name == name) { res->isfile = 1; res->objnum = here->fc_creat.newfile; return 0; } break; case FC_MKDIR: if (here->fc_mkdir.dir == dirnum && here->fc_mkdir.name == name) { res->isfile = 0; res->objnum = here->fc_mkdir.newdir; return 0; } if (here->fc_mkdir.newdir == dirnum) { return -1; } break; case FC_RMDIR: if (here->fc_rmdir.dir == dirnum && here->fc_rmdir.name == name) { return -1; } if (here->fc_rmdir.victimdir == dirnum) { return -1; } break; case FC_UNLINK: if (here->fc_unlink.dir == dirnum && here->fc_unlink.name == name) { return -1; } break; case FC_LINK: if ((here->fc_link.todir == dirnum && here->fc_link.toname == name) || (here->fc_link.fromdir == dirnum && here->fc_link.fromname == name)) { res->isfile = 1; res->objnum = here->fc_link.file; return 0; } break; case FC_RENAMEFILE: if (here->fc_renamefile.fromdir == dirnum && here->fc_renamefile.fromname == name) { return -1; } if (here->fc_renamefile.todir == dirnum && here->fc_renamefile.toname == name) { res->isfile = 1; res->objnum = here->fc_renamefile.movedfile; return 0; } break; case FC_RENAMEDIR: if (here->fc_renamedir.fromdir == dirnum && here->fc_renamedir.fromname == name) { return -1; } if (here->fc_renamedir.todir == dirnum && here->fc_renamedir.toname == name) { res->isfile = 0; res->objnum = here->fc_renamedir.moveddir; return 0; } break; } } return -1; } /* * Find a file by name and containing directory, using fs_find to * search the history. * * No longer used; should be removed. */ static unsigned fs_findfile(unsigned dirnum, unsigned name) { struct findresult res; if (fs_find(dirnum, name, &res) < 0) { die("In directory %u, did not find %s", dirnum, name_get(name)); } if (res.isfile == 0) { die("In directory %u, %s was a directory", dirnum, name_get(name)); } return res.objnum; } /* * Find a directory by name and containing directory, using fs_find to * search the history. * * No longer used; should be removed. */ static unsigned fs_finddir(unsigned dirnum, unsigned name) { struct findresult res; if (fs_find(dirnum, name, &res) < 0) { die("In directory %u, did not find %s", dirnum, name_get(name)); } if (res.isfile == 1) { die("In directory %u, %s was not a directory", dirnum, name_get(name)); } return res.objnum; } /* * Check if a name (in a specific containing directory) is a file, * using fs_find. * * No longer used; should be removed. */ static int fs_isfile(unsigned dirnum, unsigned name) { struct findresult res; if (fs_find(dirnum, name, &res) < 0) { return 0; } return res.isfile; } /* * Check if a name (in a specific containing directory) is a * directory, using fs_find. * * No longer used; should be removed. */ static int fs_isdir(unsigned dirnum, unsigned name) { struct findresult res; if (fs_find(dirnum, name, &res) < 0) { return 0; } return !res.isfile; } /* * Find the parent of a directory by its object id, by searching the * history. * * No longer used; should be removed. */ static unsigned fs_findparent(unsigned dirnum) { struct fschange *here; for (here = fs; here != NULL; here = fs_prevfordir(here, dirnum)) { switch (here->type) { case FC_NEWFS: if (here->fc_newfs.rootdirnum == dirnum) { return dirnum; } break; case FC_TRUNCATE: case FC_WRITE: case FC_CREAT: break; case FC_MKDIR: if (here->fc_mkdir.newdir == dirnum) { return here->fc_mkdir.dir; } break; case FC_RMDIR: if (here->fc_rmdir.victimdir == dirnum) { die("Directory %u was removed", dirnum); } break; case FC_UNLINK: case FC_LINK: case FC_RENAMEFILE: break; case FC_RENAMEDIR: if (here->fc_renamedir.moveddir == dirnum) { return here->fc_renamedir.todir; } break; } } die("Directory %u not found", dirnum); return 0; } #endif /* 0 */ /* * Find a file by name and containing directory, by searching the * volume state. */ static unsigned model_findfile(unsigned dirnum, unsigned name) { struct fsobject *obj; struct fsdirent *de; obj = finddir(state, dirnum); de = fsdir_find_entry(obj, name, 1); if (de->obj->isdir) { die("In directory %u, %s was a directory", dirnum, name_get(name)); } return de->obj->obj_file.identity; } /* * Find a directory by name and containing directory, by searching the * volume state. */ static unsigned model_finddir(unsigned dirnum, unsigned name) { struct fsobject *obj; struct fsdirent *de; obj = finddir(state, dirnum); de = fsdir_find_entry(obj, name, 1); if (!de->obj->isdir) { die("In directory %u, %s was not a directory", dirnum, name_get(name)); } return de->obj->obj_dir.identity; } /* * Find a directory's parent by object id, by searching the volume * state. */ static unsigned model_findparent(unsigned dirnum) { struct fsobject *obj; obj = finddir(state, dirnum); assert(obj->isdir); assert(obj->obj_dir.parent->isdir); return obj->obj_dir.parent->obj_dir.identity; } /* * Check if a name (in a specific containing directory) is a file, by * searching the volume state. */ static unsigned model_isfile(unsigned dirnum, unsigned name) { struct fsobject *obj; struct fsdirent *de; obj = finddir(state, dirnum); de = fsdir_find_entry(obj, name, 0); return de != NULL && !de->obj->isdir; } /* * Check if a name (in a specific containing directory) is a * directory, by searching the volume state. */ static unsigned model_isdir(unsigned dirnum, unsigned name) { struct fsobject *obj; struct fsdirent *de; obj = finddir(state, dirnum); de = fsdir_find_entry(obj, name, 0); return de != NULL && de->obj->isdir; } /* * Get the current size of a file in the current volume state. */ static off_t model_getfilesize(unsigned filenum) { struct fsobject *obj; obj = findfile_maybe(state, filenum); if (obj == NULL) { /* file is unlinked */ return 0; } assert(!obj->isdir); return obj->obj_file.len; } //////////////////////////////////////////////////////////// // model construction (replaying the workload) /* The workload's current directory. */ static unsigned cwd; /* * Set up for replying the workload: allocate the root directory, * generate the newfs record, set up the volume state, initialize * the current directory. */ void check_setup(void) { unsigned rootdir; assert(firstchange == NULL); assert(changes == NULL); assert(state == NULL); assert(nextfilenum == 0); assert(nextdirnum == 0); rootdir = nextdirnum++; firstchange = changes = fc_create_newfs(rootdir); rewindstate(); /* apply the first change (the newfs record) */ apply_change(state, changes); cwd = rootdir; } /* * Workload replay: create a file */ int check_createfile(unsigned name) { struct fschange *prevdir; unsigned filenum; prevdir = changes_findprevdir(cwd); filenum = nextfilenum++; fc_attach(fc_create_creat(prevdir, cwd, name, filenum)); return filenum; } /* * Workload replay: open a file */ int check_openfile(unsigned name) { return model_findfile(cwd, name); } /* * Workload replay: close a file */ void check_closefile(int handle, unsigned name) { /* don't need to do anything */ (void)handle; (void)name; } /* * Workload replay: write to a file * * CODE and SEQ encode the contents to be written. */ void check_write(int handle, unsigned name, unsigned code, unsigned seq, off_t pos, off_t len) { unsigned filenum; struct fschange *prevfile; off_t prevlen; filenum = handle; assert(filenum < nextfilenum); (void)name; prevlen = model_getfilesize(filenum); prevfile = changes_findprevfile(filenum); fc_attach(fc_create_write(prevfile, filenum, pos, len, prevlen, code, seq)); } /* * Workload replay: truncate a file */ void check_truncate(int handle, unsigned name, off_t len) { unsigned filenum; struct fschange *prevfile; filenum = handle; assert(filenum < nextfilenum); (void)name; prevfile = changes_findprevfile(filenum); fc_attach(fc_create_truncate(prevfile, filenum, len)); } /* * Workload replay: create a directory */ void check_mkdir(unsigned name) { struct fschange *prevdir; unsigned dirnum; prevdir = changes_findprevdir(cwd); dirnum = nextdirnum++; fc_attach(fc_create_mkdir(prevdir, cwd, name, dirnum)); } /* * Workload replay: remove a directory */ void check_rmdir(unsigned name) { struct fschange *prevdir, *prevvictim; unsigned victim; prevdir = changes_findprevdir(cwd); victim = model_finddir(cwd, name); prevvictim = changes_findprevdir(victim); fc_attach(fc_create_rmdir(prevdir, prevvictim, cwd, name, victim)); } /* * Workload replay: remove a file */ void check_unlink(unsigned name) { struct fschange *prevdir, *prevvictim; unsigned victim; prevdir = changes_findprevdir(cwd); victim = model_findfile(cwd, name); prevvictim = changes_findprevfile(victim); fc_attach(fc_create_unlink(prevdir, prevvictim, cwd, name, victim)); } /* * Workload replay: hard link a file */ void check_link(unsigned fromname, unsigned toname) { struct fschange *prevdir, *prevfile; unsigned filenum; prevdir = changes_findprevdir(cwd); filenum = model_findfile(cwd, fromname); prevfile = changes_findprevfile(filenum); fc_attach(fc_create_link(prevdir, prevdir, prevfile, cwd, fromname, cwd, toname, filenum)); } /* * Workload replay: rename something */ static void common_rename(unsigned fromdirnum, unsigned fromname, unsigned todirnum, unsigned toname) { struct fschange *prevfromdir, *prevtodir, *prevfrom, *prevto; unsigned fromnum, tonum; struct fschange *fc; int isfile; prevfromdir = changes_findprevdir(fromdirnum); prevtodir = changes_findprevdir(todirnum); if (model_isfile(todirnum, toname)) { isfile = 1; assert(model_isfile(fromdirnum, fromname)); tonum = model_findfile(todirnum, toname); prevto = changes_findprevfile(tonum); fc = fc_create_unlink(prevtodir, prevto, todirnum, toname, tonum); } else if (model_isdir(todirnum, toname)) { isfile = 0; assert(model_isdir(fromdirnum, fromname)); tonum = model_finddir(todirnum, toname); prevto = changes_findprevdir(tonum); fc = fc_create_rmdir(prevtodir, prevto, todirnum, toname, tonum); } else { isfile = model_isfile(fromdirnum, fromname); fc = NULL; } if (fc) { fc->partial = 1; fc_attach(fc); } if (isfile) { fromnum = model_findfile(fromdirnum, fromname); prevfrom = changes_findprevfile(fromnum); fc = fc_create_renamefile(prevfromdir, prevtodir, prevfrom, fromdirnum, fromname, todirnum, toname, fromnum); } else { fromnum = model_finddir(fromdirnum, fromname); prevfrom = changes_findprevdir(fromnum); fc = fc_create_renamedir(prevfromdir, prevtodir, prevfrom, fromdirnum, fromname, todirnum, toname, fromnum); } fc_attach(fc); } /* * Workload replay: rename within the current directory */ void check_rename(unsigned from, unsigned to) { common_rename(cwd, from, cwd, to); } /* * Workload replay: cross-directory rename */ void check_renamexd(unsigned fromdir, unsigned from, unsigned todir, unsigned to) { unsigned fromdirnum, todirnum; /* fromdir/todir are names in cwd */ fromdirnum = model_finddir(cwd, fromdir); todirnum = model_finddir(cwd, todir); common_rename(fromdirnum, from, todirnum, to); } /* * Workload replay: change directory */ void check_chdir(unsigned name) { cwd = model_finddir(cwd, name); } /* * Workload replay: change directory to .. */ void check_chdirup(void) { cwd = model_findparent(cwd); } /* * Workload replay: sync */ void check_sync(void) { /* nothing */ } //////////////////////////////////////////////////////////// // inspection of the fs /* the root of the state we found (as opposed to the state we modeled) */ static struct fsobject *found; /* count of how much we found */ static unsigned found_subdirs, found_files; /* * Wrapper. As of this writing OS/161 provides fstat but not stat... * grr */ static int xstat(const char *path, struct stat *buf) { int fd, ret, serrno; fd = open(path, O_RDONLY); if (fd < 0) { return -1; } ret = fstat(fd, buf); serrno = errno; close(fd); errno = serrno; return ret; } /* * Inspect a directory. PARENTOBJ is the *containing* directory; * PARENTINO is its inode number, and DIRNAMESTR is the name of the * directory to inspect. The arguments are set up this way (rather * than e.g. passing the fsobject of the directory to inspect) to * improve the error reporting, and to allow checking .., and so * forth. */ static struct fsobject * inspectdir(struct fsobject *parentobj, ino_t parentino, const char *dirnamestr) { char subnamestr[NAME_MAX+1]; size_t subnamelen; struct stat dirstat, dotstat, substat; int dirfd; struct fsobject *subobj, *ret; struct fsdirent *contents, *de; /* * Stat the target, and cd into it. */ if (xstat(dirnamestr, &dirstat)) { err(1, "%s: stat", dirnamestr); } assert(S_ISDIR(dirstat.st_mode)); if (chdir(dirnamestr)) { err(1, "%s: chdir", dirnamestr); } /* * Check that . is correct */ if (xstat(".", &dotstat)) { err(1, "In %s: .: stat", dirnamestr); } if (dotstat.st_dev != dirstat.st_dev) { errx(1, "in %s: .: wrong volume id; found %u, expected %u", dirnamestr, dotstat.st_dev, dirstat.st_dev); } if (dotstat.st_ino != dirstat.st_ino) { errx(1, "%s/.: wrong inode number; found %u, expected %u", dirnamestr, dotstat.st_ino, dirstat.st_ino); } /* * Check that .. leads back */ if (xstat("..", &dotstat)) { err(1, "In %s: ..: stat", dirnamestr); } if (dotstat.st_dev != dirstat.st_dev) { errx(1, "In %s: ..: wrong volume id; found %u, expected %u", dirnamestr, dotstat.st_dev, dirstat.st_dev); } if (dotstat.st_ino != parentino) { errx(1, "In %s: ..: wrong inode number; found %u, expected %u", dirnamestr, dotstat.st_ino, parentino); } /* * Create a directory fsobject. */ dirfd = open(".", O_RDONLY); if (dirfd < 0) { err(1, "In %s: .: open", dirnamestr); } ret = fsobject_create_dir(UNKNOWN_ID, parentobj); /* * Read the contents of the target directory and create * directory entries for them. Recurse for ones that are * themselves directories. */ contents = NULL; while (1) { subnamelen = getdirentry(dirfd, subnamestr, sizeof(subnamestr)-1); if (subnamelen == 0) { break; } subnamestr[subnamelen] = 0; if (!strcmp(subnamestr, ".") || !strcmp(subnamestr, "..")) { continue; } if (xstat(subnamestr, &substat)) { err(1, "In %s: %s: stat", dirnamestr, subnamestr); } if (S_ISDIR(substat.st_mode)) { subobj = inspectdir(ret, dirstat.st_ino, subnamestr); found_subdirs++; } else { subobj = fsobject_create_file(UNKNOWN_ID); subobj->obj_file.len = substat.st_size; found_files++; } de = fsdirent_create(name_find(subnamestr), subobj); de->next = contents; contents = de; } /* * Done scanning; cd out, save the contents, and return the * new object. */ close(dirfd); if (chdir("..")) { err(1, "In %s; ..: chdir", dirnamestr); } ret->obj_dir.entries = contents; return ret; } /* * Inspect the whole volume by starting with "." -- we assume that we * were run in the root directory. */ static void inspectfs(void) { struct stat st; if (xstat(".", &st)) { err(1, ".: stat"); } found = inspectdir(NULL, st.st_ino, "."); } //////////////////////////////////////////////////////////// // comparison of state trees /* * Count the number of objects at and below a particular fsobject. */ static unsigned count_subtree(struct fsobject *obj) { struct fsdirent *de; unsigned ret = 1; if (obj->isdir) { for (de = obj->obj_dir.entries; de != NULL; de = de->next) { ret += count_subtree(de->obj); } } return ret; } /* * Compare two fsobjects. Return the matching score. (lower scores are * better matches) */ static unsigned compare_objects(struct fsobject *obja, struct fsobject *objb) { struct fsdirent *enta, *entb; unsigned ret, found; if (obja->isdir != objb->isdir) { /* * One object's a file, the other's a directory. * * Return one point for each name in the missing * subtree. This includes one point for the top dir, * which is mismatched rather than missing. */ if (obja->isdir) { return count_subtree(obja); } assert(objb->isdir); return count_subtree(objb); } if (!obja->isdir) { /* * Both objects are files */ assert(!objb->isdir); if (obja->obj_file.len != objb->obj_file.len) { /* one point for the size being different */ return 1; } return 0; } /* * Both objects are directories. Recurse on all pairs of * entries that have the same name. Return one point for each * name that's present (recursively) in one object but not the * other. * * XXX: sort the entries first or something instead of naively * searching 2*N^2 times. */ assert(obja->isdir); assert(objb->isdir); ret = 0; for (enta = obja->obj_dir.entries; enta != NULL; enta = enta->next) { found = 0; for (entb = objb->obj_dir.entries; entb != NULL; entb = entb->next) { if (enta->name == entb->name) { ret += compare_objects(enta->obj, entb->obj); found = 1; break; } } if (!found) { if (enta->obj->isdir) { /* whole subtree is missing */ ret += count_subtree(enta->obj); } /* one file is missing */ ret += 1; } } for (entb = objb->obj_dir.entries; entb != NULL; entb = entb->next) { found = 0; for (enta = obja->obj_dir.entries; enta != NULL; enta = enta->next) { if (enta->name == entb->name) { found = 1; break; } } if (!found) { if (entb->obj->isdir) { /* whole subtree is missing */ ret += count_subtree(entb->obj); } /* one file is missing */ ret += 1; } } return ret; } /* * Print an indentation. */ static void doindent(unsigned depth) { unsigned i; for (i=0; iisdir); assert(objb->isdir); for (enta = obja->obj_dir.entries; enta != NULL; enta = enta->next) { found = 0; for (entb = objb->obj_dir.entries; entb != NULL; entb = entb->next) { if (enta->name == entb->name) { doindent(indent); printf("%s", name_get(enta->name)); if (enta->obj->isdir && !entb->obj->isdir) { printf(": expected dir, found file;"); printf(" %u names missing.\n", count_subtree(enta->obj) - 1); } else if (!enta->obj->isdir && entb->obj->isdir) { printf(": expected file, found dir;"); printf(" %u extra names.\n", count_subtree(entb->obj) - 1); } else if (!enta->obj->isdir && !entb->obj->isdir) { off_t alen, blen; alen = enta->obj->obj_file.len; blen = entb->obj->obj_file.len; if (alen == blen) { printf("\t\t%lld bytes (ok)\n", alen); } else { printf(": found %lld bytes, " "expected %lld " "bytes.\n", blen, alen); } } else { printf("/\n"); printdiffs(indent + 1, enta->obj, entb->obj); } found = 1; break; } } if (!found) { doindent(indent); printf("%s: missing ", name_get(enta->name)); if (enta->obj->isdir) { printf("subtree with %u names.\n", count_subtree(enta->obj) - 1); } else { printf("file\n"); } } } for (entb = objb->obj_dir.entries; entb != NULL; entb = entb->next) { found = 0; for (enta = obja->obj_dir.entries; enta != NULL; enta = enta->next) { if (enta->name == entb->name) { found = 1; break; } } if (!found) { doindent(indent); printf("%s: extra ", name_get(entb->name)); if (entb->obj->isdir) { printf("subtree with %u names.\n", count_subtree(entb->obj) - 1); } else { printf("file\n"); } } } } //////////////////////////////////////////////////////////// // comparison of file contents /* * Return the version of the oldest change with the same directory * structure as CHANGE. This skips past truncate and write operations * that only change file sizes. */ static unsigned findokversion(struct fschange *change) { while (change != NULL) { switch (change->type) { case FC_TRUNCATE: case FC_WRITE: break; default: return change->version; } change = change->prev; } assert(0); return 0; } /* * Back up to previous change entries until we find one affecting the * contents of the specified file. (That means: truncate, write, or * create.) * * Returns the current (passed-in) change if that matches. */ static struct fschange * backup_for_file(struct fschange *change, unsigned filenum) { while (change != NULL) { switch (change->type) { case FC_TRUNCATE: if (change->fc_truncate.file == filenum) { return change; } break; case FC_WRITE: if (change->fc_write.file == filenum) { return change; } break; case FC_CREAT: if (change->fc_creat.newfile == filenum) { return change; } break; default: break; } change = change->prev; } return NULL; } /* * Expect zeros in a file from START to END. The file is given by FD * and named NAMESTR. Report if we find stuff other than zeros. */ static void checkfilezeros(int fd, const char *namestr, off_t start, off_t end) { char buf[1024]; size_t len, i; ssize_t ret; unsigned poison = 0, trash = 0; off_t origstart = start; printf(" %lld - %lld (expecting zeros)\n", start, end); if (lseek(fd, start, SEEK_SET) == -1) { err(1, "%s: lseek to %lld", namestr, start); } while (start < end) { /* XXX this assumes end - start fits in size_t */ len = end - start; if (len > sizeof(buf)) { len = sizeof(buf); } ret = read(fd, buf, len); if (ret == -1) { err(1, "%s: read %u at %lld", namestr, len, start); } if (ret == 0) { errx(1, "%s: read %u at %lld: Unexpected EOF", namestr, len, start); } for (i=0; i<(size_t)ret; i++) { if ((unsigned char)buf[i] == POISON_VAL) { poison++; } else if (buf[i] != 0) { trash++; } } start += ret; } if (poison > 0 || trash > 0) { printf("ERROR: File %s: expected zeros from %lld to %lld; " "found", namestr, origstart, end); if (poison > 0) { printf(" %u poison bytes", poison); if (trash > 0) { printf(" and"); } } if (trash > 0) { printf(" %u trash bytes", trash); } printf("\n"); } } /* * Read data in from a file, into data.c's read buffer where it will * be checked for correctness. The data.c region involved is * REGIONSTART..REGIONEND; all of that space is needed so the expected * data can be generated correctly. However, we're only looking at the * range CHECKSTART..CHECKEND, which may be only part of the data.c * region. */ static void readfiledata(int fd, const char *namestr, off_t regionstart, off_t checkstart, off_t checkend, off_t regionend) { char *readbuf; size_t bufpos, len; ssize_t ret; /* CHECKSTART..CHECKEND must be within REGIONSTART..REGIONEND */ assert(regionstart <= checkstart); assert(checkstart <= checkend); assert(checkend <= regionend); readbuf = data_mapreadbuf(regionend - regionstart); bufpos = checkstart - regionstart; len = checkend - checkstart; if (lseek(fd, checkstart, SEEK_SET) == -1) { err(1, "%s: lseek to %lld", namestr, checkstart); } while (len > 0) { ret = read(fd, readbuf + bufpos, len); if (ret == -1) { err(1, "%s: read %u at %lld", namestr, len, regionstart + bufpos); } if (ret == 0) { errx(1, "%s: read %u at %lld: Unexpected EOF", namestr, len, regionstart + bufpos); } bufpos += ret; assert(len >= (size_t)ret); len -= ret; } } /* * Check the data found in the file FD named NAMESTR, in the range * CHECKSTART..CHECKEND, which is within or equal to the data.c * region REGIONSTART..REGIONEND. ZEROSTART is the place where the * data can legitimately be zeroed rather than the expected data * (the portion of a write that extended a file) and the data is * generated from CODE and SEQ. */ static void checkfiledata(int fd, const char *namestr, unsigned code, unsigned seq, off_t zerostart, off_t regionstart, off_t checkstart, off_t checkend, off_t regionend) { if (checkstart < regionstart) { checkstart = regionstart; } if (checkend > regionend) { checkend = regionend; } printf(" %lld - %lld\n", checkstart, checkend); readfiledata(fd, namestr, regionstart, checkstart, checkend, regionend); data_check(namestr, regionstart, code, seq, zerostart - regionstart, regionend - regionstart, checkstart - regionstart, checkend - checkstart); } /* * Check a range of the file FD named NAMESTR, from START to END, * against the model state expected as of CHANGE. */ static void checkfilerange(int fd, const char *namestr, struct fschange *change, off_t start, off_t end) { assert(start < end); if (change->type == FC_TRUNCATE) { off_t tlen; struct fschange *prev; /* * The most recent change was a truncate. Anything * beyond the truncate length should read as zeroes; * recurse on the part before (if any) using the * previous change affecting this file. * * We might be checking that a chunk of the file * beyond the truncate length is zero (even though as * of this change it's past EOF) if a later change * made that region into a hole in a sparse file. */ tlen = change->fc_truncate.len; prev = change->fc_truncate.prev_thisfile; if (tlen < start) { checkfilezeros(fd, namestr, start, end); } else if (tlen < end) { checkfilerange(fd, namestr, prev, start, tlen); checkfilezeros(fd, namestr, tlen, end); } else { checkfilerange(fd, namestr, prev, start, end); } } else if (change->type == FC_WRITE) { off_t wstart, wend; struct fschange *prev; unsigned code, seq; off_t oldfilesize, zerostart; /* * The most recent change was a write. */ wstart = change->fc_write.pos; wend = change->fc_write.pos + change->fc_write.len; prev = change->fc_write.prev_thisfile; code = change->fc_write.code; seq = change->fc_write.seq; oldfilesize = change->fc_write.oldfilesize; /* * ZEROSTART (where we begin to allow the file to * contain zeros) should be the point at the write * where we began to extend the file, if any. */ if (oldfilesize < wstart) { zerostart = wstart; } else if (oldfilesize < wend) { zerostart = oldfilesize; } else { zerostart = wend; } /* * Six cases for the range overlap: * (1) (2) (3) (4) (5) (6) * ** *** ****** * *** ** * *** *** *** *** *** *** * * We call checkfilerange recursively using the * previous change for this file for the * non-overlapping parts, because this write didn't * touch those so they must reflect the previous file * state; and checkfiledata for the overlapping parts * that this write did touch. */ if (end <= wstart || start >= wend) { /* cases 1 and 6 */ checkfilerange(fd, namestr, prev, start, end); } else { /* cases 2-5 */ if (start < wstart) { /* case 2 or 3 */ checkfilerange(fd, namestr, prev, start, wstart); } checkfiledata(fd, namestr, code, seq, zerostart, wstart, start, end, wend); if (end > wend) { /* cases 3 or 5 */ checkfilerange(fd, namestr, prev, wend, end); } } } else if (change->type == FC_RENAMEFILE) { struct fschange *prev; /* * The most recent change was a rename. As rename * doesn't affect contents, recurse on the previous * change affecting the file. */ prev = change->fc_renamefile.prev_movedfile; checkfilerange(fd, namestr, prev, start, end); } else { assert(change->type == FC_CREAT); /* * The most recent change was the file being created. * Like with truncate, check that the range is zero * even though it's past EOF, as a later write might * have converted past-EOF space into a hole in a * sparse file. */ checkfilezeros(fd, namestr, start, end); } } /* * Check if a change CHANGE to a file is present in the observed file * FD, whose name is NAMESTR and size is FILESIZE. * * For writes it checks the data, so should be accurate; for truncate * all it can check is the size, so it can readily be fooled by * sequences of truncates or multiple truncates to the same length. */ static int change_is_present(int fd, const char *namestr, off_t filesize, struct fschange *change) { off_t pos, len, oldfilesize, zerostart; unsigned code, seq; switch (change->type) { case FC_TRUNCATE: return filesize == change->fc_truncate.len; case FC_WRITE: pos = change->fc_write.pos; len = change->fc_write.len; code = change->fc_write.code; seq = change->fc_write.seq; oldfilesize = change->fc_write.oldfilesize; if (oldfilesize < pos) { zerostart = 0; } else if (oldfilesize < pos + len) { zerostart = oldfilesize - pos; } else { zerostart = len; } readfiledata(fd, namestr, pos, pos, pos+len, pos+len); return data_matches(namestr, pos, code, seq, zerostart, len, 0, len); case FC_CREAT: return 1; default: break; } assert(0); return 0; } /* * Check the contents of the file called NAMESTR, which is the model * file FILE, as of change CHANGE. */ static void checkonefilecontents(const char *namestr, struct fsobject *file, struct fschange *change) { unsigned okversion; int fd; /* * Open the file. */ assert(!file->isdir); fd = open(namestr, O_RDONLY); if (fd < 0) { err(1, "%s: open", namestr); } /* * Find the oldest version that has the same directory * structure as CHANGE, and thus reflects the earliest * contents we can legitimately find in the file. * * XXX: the matching we do to pick the change to examine takes * file sizes into account, but findokversion specifically * doesn't stop going backwards if just the file size changes. * This seems wrong: if we match on file size, ok versions to * see are only those that have the same file size. */ okversion = findokversion(change); /* * Find the first change (going backwards) that affects this * file. There should always be at least the create. This * change might be before or after OKVERSION. */ change = backup_for_file(change, file->obj_file.identity); if (change == NULL) { die("File %s was never even created?", namestr); } if (file->obj_file.len == 0) { /* * The model expects the length to be 0. * * XXX: I think the code here was written thinking * FILE is from the inspection results; but it's not, * it's the model from the workload replay. So these * checks appear to be wrong. */ if (change->type == FC_CREAT) { /* file was created empty and never written to */ return; } if (change->type == FC_TRUNCATE) { /* if the length is wrong we shouldn't get this far */ assert(change->fc_truncate.len == 0); /* so, nothing to check */ close(fd); return; } assert(change->type == FC_WRITE); printf("ERROR: File %s is zero length but was expected to " "contain at least %lld bytes at offset %lld!\n", namestr, change->fc_write.pos, change->fc_write.len); close(fd); return; } /* XXX: this check is wrong too. */ if (change->type == FC_CREAT) { printf("ERROR: File %s was never written to but has " "length %lld\n", namestr, file->obj_file.len); close(fd); return; } /* * If CHANGE isn't reflected in the file, back up. If this * causes us to back up past OKVERSION, complain that the * change should be present. * * XXX: the call to change_is_present should be using the * observed file size, not the modeled file size. * * XXX: More seriously, however, this logic is not really * right. If the workload contains multiple writes that don't * affect the file length, interspersed with truncates that * are all to the same length, we'll stop looking back at the * first truncate (because the length matches) regardless of * which writes are present. None of the workloads currently * does anything like this, but it's still wrong. * * I think the right thing to do (especially since the version * matching checks file lengths) is to check only writes for * presence and ignore truncates; and probably, rather than * back up based on what writes appear to be present and then * call checkfilerange, which will then complain loudly if * parts of an older write didn't make it out, divide the file * into ranges based on blocks and writes and figure out which * version the data found in each such range corresponds to, * then object only to the ones that are crazy and warn about * ones that reflect data buffers not making it out. But this * is a fairly big rewrite. */ while (!change_is_present(fd, namestr, file->obj_file.len, change)) { if (change->version < okversion) { printf("File %s: change for version %u is missing\n", namestr, change->version); } change = backup_for_file(change->prev,file->obj_file.identity); if (change == NULL) { printf("File %s: no matching version found\n", namestr); close(fd); return; } } /* * Now we've found a version that we think the file contents * should correspond to; check that it's what we actually * have. * * XXX: should this use the model length or the observed * length? */ checkfilerange(fd, namestr, change, 0, file->obj_file.len); close(fd); } /* * Check the contents of all files under DIR with respect to the * change CHANGE. Recurses on subdirectories. */ static void checkallfilecontents(struct fsobject *dir, struct fschange *change) { struct fsdirent *de; const char *namestr; assert(dir->isdir); for (de = dir->obj_dir.entries; de != NULL; de = de->next) { namestr = name_get(de->name); if (de->obj->isdir) { printf(" >>> Entering %s\n", namestr); if (chdir(namestr)) { err(1, "%s: chdir", namestr); } checkallfilecontents(de->obj, change); printf(" <<< Leaving %s\n", namestr); if (chdir("..")) { err(1, "..: chdir"); } } else { printf("%s...\n", namestr); checkonefilecontents(namestr, de->obj, change); } } } //////////////////////////////////////////////////////////// // model validation /* * Compare the on-disk state we see to the model we've built. */ void checkfs(void) { struct fschange *change, *best; unsigned score, bestscore; /* * We just built the model; talk about it. */ printf("Established %u versions across %u directories and %u files\n", changes->version + 1, nextdirnum, nextfilenum); /* * Inspect the volume state we've got. Initializes the global * FOUND holding the found volume state. */ inspectfs(); printf("Found %u subdirs and %u files on the volume\n", found_subdirs, found_files); /* * Rewind the model state to the beginning. */ rewindstate(); /* * Loop through all the changes; apply each one to the model * state, and score the model state against the found state. * Keep track of the best matching version. * * XXX: there might be several versions with the same score, * in which case we'll blindly take the most recent one. If * there are several versions that match exactly (which can * easily happen for workloads that don't explicitly avoid it) * we'll also blindly take the most recent one, and then the * matching of file contents can blow up. We should collect * all the versions with the same score, and score each one * on file contents too. Or something like that. */ change = firstchange; assert(change->type == FC_NEWFS); best = NULL; bestscore = 0; while (change != NULL) { apply_change(state, change); score = compare_objects(state, found); if (best == NULL || score <= bestscore) { best = change; bestscore = score; } //printf("version %u score %u\n", change->version, score); change = change->next; } assert(best != NULL); /* * Set the model state to the best matching state we found. */ rewindstate(); advancestateto(best); if (bestscore > 0) { /* * We didn't get an exact match, so print how the * differences. XXX: this results in not checking file * data... */ printf("FAILURE: Directory tree does not match on any " "version.\n"); printf("Best version is %u; describing differences:\n", best->version); printdiffs(1, state, found); return; } /* * Ok, we did get an exact match. Print it. */ printf("Directory tree matched in version %u.\n", best->version); if (best->partial) { printf("WARNING: this is a version from a partially committed " "operation.\n"); } /* * XXX we should check hard links here; that is, that all the * files that are supposed to be hard links of each other are, * and that no other files are randomly hardlinked. * * However, if things are wrong it should result in bad * contents. It would be very unlikely for even a very broken * filesystem to, on its own, copy files that are supposed to * be hardlinked. */ /* now check the file contents */ printf("Checking file contents...\n"); checkallfilecontents(state, best); printf("Done.\n"); }