An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
136 lines
5.2 KiB
Shell
136 lines
5.2 KiB
Shell
#!/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 <description> <file> <sed-expression> [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
|