#!/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 cd /mnt/d/DEV/Schunix 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 # 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 build/schfs_objtest > /tmp/mk.log 2>&1; then echo " rejected at compile time" return fi if ./build/schfs_objtest > /tmp/rt.log 2>&1; then echo " NOT CAUGHT -- the suite passed with this bug present" FAILED=1 else echo " caught: $(grep -aE 'FAIL' /tmp/rt.log | head -1 | sed 's/^ *//')" fi return fi if ! make build/mkschfs build/schfs_fsck > /tmp/mk.log 2>&1; then echo " rejected at compile time" return fi if sh tools/schfs_roundtrip.sh ./build/mkschfs ./build/schfs_fsck \ > /tmp/rt.log 2>&1; then echo " NOT CAUGHT -- the suite passed with this bug present" FAILED=1 else echo " caught: $(grep -aE 'FAIL|Only in|differ|loop' /tmp/rt.log | head -1 | sed 's/^ *//')" 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