# DF-2998 — Server attributes freely retype live vnodes in nfs_loadattrcache

Type-confusion engine: any attribute reply (GETATTR result, LOOKUP /
ACCESS / READDIR post-op attrs, …) applied to an *existing* NFS vnode
retypes it (`nfs_setvtype` + `v_ops` switch + `addaliasu` for VCHR/VBLK)
with no "only at creation" guard — `sys/vfs/nfs/nfs_subs.c:723-733`.

## Threat model

Malicious (or compromised) NFSv3 server + a local user (unprivileged is
enough) touching the share. The server flips the reported type of any
file handle; after the client's attribute cache expires (mount options
pin it to 1s in this PoC), the very next stat/getattr applies the lie to
the LIVE vnode.

What it buys the attacker, demonstrated on the stock kernel:

1. **Live VREG→VDIR retype of an open file** (fstat shows REGULAR →
   DIRECTORY on the same fd) — and a single vnode retypes repeatedly
   (REG→DIR→CHR→DIR).
2. **VCHR injection**: a "file" becomes a visible character device with
   server-chosen rdev/mode/uid (`addaliasu` + `mnt_vn_spec_ops`).
3. **Memory corruption, two independent deterministic panics:**
   - `nfs_removerpc+0x57` GPF: a cookie-cached node left non-VDIR at
     inactive makes `nfs_inactive` read `n_sillyrename`, which ALIASES
     the VDIR cookie-list head (`n_un3` union), and walk a `nfsdmap` as
     a `struct sillyrename`.
   - `panic: bgetvp - overlapping buffer` via
     `nfs_getattrcache → nvtruncbuf`: the VDIR→VREG retype itself sends
     directory buffer-cache state through the regular-file shrink path.
   (Third sink, already persisted under DF-2996: sillyrename + retype →
   reclaim cookie-walk panic.)

Honest limitation established on this DragonFly: injected VCHR nodes
are **inert for device I/O** — NFS special vnodes use
`nfsv2_spec_vops` (`vop_stdnoread`/`vop_stdnowrite`, nfs_vnops.c:162+)
and devfs associates devices by `v_rdev` pointer, which NFS never sets;
open succeeds as a stub, read returns EINVAL. The classic
"inject /dev/disk through NFS attrs" raw-device access of old BSDs is
structurally dead here; the injection remains a namespace/type spoof
(server-chosen st_mode/st_uid/rdev visible to local tooling).

## Build (in the DragonFly guest, as root)

    mkdir -p /root/df2998
    cc -O -o fakesrv2998 fakesrv2998.c
    cc -O -o trigger2998 trigger2998.c        # see build.sh / build.log

## Run (stock kernel; TEST 2 panics — expect the ssh session to die)

    sh run.sh                    # TEST 1 + 1b (retypes) then TEST 2 (panic)

or piecewise (as run.sh does):

    (./fakesrv2998 > /tmp/fakesrv2998.log 2>&1 &)
    mount_nfs -o port=2049,udp,acregmin=1,acregmax=1,acdirmin=1,acdirmax=1 \
        127.0.0.1:/P /mnt
    ./trigger2998 retype /mnt $(stat -f '%Hr %Lr' /dev/vn0)   # TEST 1
    ./trigger2998 dirreg /mnt ; umount /mnt                    # TEST 2 -> panic

Success criteria (stock kernel):

- TEST 1 prints `*** DF-2998 CONFIRMED: live VREG vnode retyped VDIR
  while fd open ***` and `*** ... device node injected ... ***`.
- TEST 2 prints the VDIR→VREG confirmation, then `umount` panics:
  `panic: bgetvp - overlapping buffer` (see panic.bgetvp.txt), or, if
  the node is non-VDIR at inactive without a stat first,
  `Fatal trap 9 ... nfs_removerpc+0x57` (see panic.removerpc.txt).

Fixed kernel (`fix.diff`, single-fix rebuild): all lies are refused
(`fstat: RPC struct is bad` = EBADRPC), no retype, no panic, umounts
clean, and a full honest-server lifecycle (create/write/read/ls/rm/
mkdir/readdir/umount) keeps working — see fix_run.log.

## Rig

`fakesrv2998.c` — userland NFSv3 server (rpcbind :111, mountd :779,
nfs :2049 on 127.0.0.1) with a UDP control channel :7798 (`LIE <fh>
<TYPE> [maj min]`, `RESET`) that flips the per-filehandle attribute
lie at runtime; `trigger2998.c` — drives/observes the retypes.

See manifest.json for artifacts and VERDICT.md for the full analysis.
