#!/bin/sh # Mutation-test the SchFS suites: inject a bug, confirm the tests report it, # restore. A test that cannot fail is worse than no test, because it is # believed. Run by hand; not part of the normal build. # # Every mutation is verified to have actually changed the file before the # suite runs. A sed pattern that silently stops matching -- after a refactor, # say -- would otherwise look exactly like a passing mutation test, which is # the same failure this script exists to catch. set -e # Run from anywhere. This used to `cd /mnt/d/DEV/SchOS` -- an absolute WSL path # from the Windows machine this was written on, which meant the script could not # run on any other checkout at all. script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) repo=$(CDPATH= cd -- "$script_dir/../.." && pwd) cd "$repo" # Where the host tools land. These moved to build/tools/ in the bt/ -> tools/ # migration and this script kept the old names; make then answered "No rule to # make target", which the harness reported as "rejected at compile time". Every # mutation looked handled and not one of them ever reached a test. MKSCHFS=build/tools/mkschfs FSCK=build/tools/schfs_fsck OBJT=build/tools/schfs_objtest OBJ=kernel/fs/schfs/obj.c OBJW=kernel/fs/schfs/objw.c SPACE=kernel/fs/schfs/space.c MK=tools/mkschfs.c cp $OBJ /tmp/obj.orig cp $OBJW /tmp/objw.orig cp $SPACE /tmp/space.orig cp $MK /tmp/mk.orig restore() { cp /tmp/obj.orig $OBJ cp /tmp/objw.orig $OBJW cp /tmp/space.orig $SPACE cp /tmp/mk.orig $MK } trap restore EXIT FAILED=0 # Quote the line that says why, falling back to the last thing the suite said. # Two mutations used to report a bare "caught:" with nothing after it -- the # suite had failed, but nothing in it matched the patterns below, so the # evidence was an exit status and an empty string. why() { line=$(grep -aE 'FAIL|Only in|differ|loop|error|assert' "$1" | head -1) [ -n "$line" ] || line=$(grep -av '^[[:space:]]*$' "$1" | tail -1) printf '%s' "$line" | sed 's/^ *//' } # try [suite] # # suite is "image" (the default: build an image and diff it against the source # tree) or "obj" (the in-memory object-layer test). They catch different things: # the image round-trip cannot see a bug that only shows up after a delete, # because it never deletes anything. try() { desc=$1; file=$2; expr=$3; suite=${4:-image} restore before=$(cksum < "$file") sed -i "$expr" "$file" after=$(cksum < "$file") echo "mutation: $desc" if [ "$before" = "$after" ]; then echo " BROKEN MUTATION -- the pattern no longer matches anything" FAILED=1 return fi if [ "$suite" = obj ]; then if ! make "$OBJT" > /tmp/mk.log 2>&1; then echo " rejected at compile time" sed -n '$p' /tmp/mk.log | sed 's/^/ /' return fi if "./$OBJT" > /tmp/rt.log 2>&1; then echo " NOT CAUGHT -- the suite passed with this bug present" FAILED=1 else echo " caught: $(why /tmp/rt.log)" fi return fi if ! make "$MKSCHFS" "$FSCK" > /tmp/mk.log 2>&1; then echo " rejected at compile time" sed -n '$p' /tmp/mk.log | sed 's/^/ /' return fi if sh tools/posix/schfs_roundtrip.sh "./$MKSCHFS" "./$FSCK" \ > /tmp/rt.log 2>&1; then echo " NOT CAUGHT -- the suite passed with this bug present" FAILED=1 else echo " caught: $(why /tmp/rt.log)" fi } # Reads run past end-of-file, so every file comes back padded. try "file_read ignores the end-of-file clamp" $OBJ \ 's|if (off + n > in.size)|if (0)|' # The dirent walk skips forward too far, losing entries in any packed list. try "dirent_walk advances past the next entry" $OBJ \ 's| off += step;| off += step + 8;|' # Directory lookup returns the neighbouring object. try "dir_lookup returns the wrong objectid" $OBJ \ 's| f->ino = d->objectid;| f->ino = d->objectid + 1;|' # Every extent points one block past its data. # # Note this replaced an earlier mutation that shortened the extent by a block. # That one was NOT caught, and correctly so: an extent covering too little just # leaves the tail unmapped, and schfs_file_append then allocates it separately, # so the file still reads back byte for byte. The design absorbs it. Pointing # at the wrong block cannot be absorbed. try "extents point one block past their data" $OBJW \ 's| e.disk_block = first;| e.disk_block = first + 1;|' # Copy-on-write splits an extent into head, replacement and tail. Losing the # tail leaves everything after the rewritten block unmapped, so it silently # reads back as zeros -- data loss that looks exactly like a sparse file. try "an overwrite drops the tail of the extent it split" $OBJW \ 's| if (fblk + 1 < start + e.nblocks) {| if (0) {|' obj # Truncating down and growing again must read zeros. Skipping the zeroing of # the partial last block hands back whatever the file used to hold there -- # which, across a delete and a reallocation, is somebody else's data. try "truncate leaves stale bytes in the last partial block" $OBJW \ 's| memset((char \*)dblk + tail, 0, SCHFS_BLKSIZE - tail);| /* mutated */;|' obj # Deferred frees never rejoin the free map, so every delete leaks. Nothing that # only reads or only writes can see this; it takes a test that deletes # everything and then checks the free count. try "committed frees are never returned to the free map" $SPACE \ 's| inuse\[w\] &= ~pend\[w\];| /* mutated */;|' obj # Hard links are not counted, so dedup is indistinguishable from duplication # and only the explicit nlink assertion can tell the difference. try "hard links do not bump nlink" $MK \ 's|^ in.nlink++;| /* mutated */;|' # Data checksums cover only the head of a block, so tail corruption is missed. try "data checksum covers only the first 16 bytes" $OBJW \ 's|uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);|uint32_t c = schfs_crc32c(0, data, 16);|' restore make build/mkschfs build/schfs_fsck > /dev/null 2>&1 echo if [ $FAILED -eq 0 ]; then echo "all mutations were caught" else echo "SOME MUTATIONS WERE NOT CAUGHT -- the suite has a blind spot" exit 1 fi