# DF-3236 — devfs vn_lock(NULL) via unknown-ino file handle (fhopen)

## What

`devfs_vfs_fhtovp` (sys/vfs/devfs/devfs_vfsops.c:202-221) and
`devfs_vfs_vget` (:244-256) pass a caller-controlled inode number to
`devfs_inode_to_vnode()` (sys/vfs/devfs/devfs_core.c:959-976). When the devfs
core worker finds no node with that ino, `devfs_iterate_topology()` returns
NULL (devfs_core.c:630-649) and the core assigns `msg->mdv_ino.vp = NULL`
(devfs_core.c:1373-1378). `devfs_inode_to_vnode()` then executes
`vn_lock(vp, LK_EXCLUSIVE | LK_RETRY)` on that NULL **before returning it**
(devfs_core.c:971-972) — the callers' own NULL checks
(devfs_vfsops.c:216-217, 251-252) are unreachable. `vn_lock(NULL)` →
VOP_LOCK → `lockmgr_exclusive` dereferences NULL: fatal trap 12.

## Reachability / privilege

- Local: `fhopen(2)`/`fhstat(2)`/`fhstatfs(2)`/`fhget` are all gated by
  `SYSCAP_RESTRICTEDROOT` (kern/vfs_syscalls.c:4782, 4832, 5013, ...) —
  **root required** locally (verified: unprivileged run returns EPERM and
  never reaches devfs).
- Remote: the kernel NFS server calls `VFS_VGET(mp, vp, dp->d_ino, &nvp)`
  with wire-supplied directory inode numbers (nfs_serv.c:3417, 3454) — an
  exported devfs would turn this into an unauthenticated remote panic, but
  exporting devfs requires explicit admin action.

## Files

- `fhdevfs.c` — PoC: takes a real devfs file handle via `getfh("/dev/null")`,
  swaps `fid_ino` to 0x41414141, calls `fhopen(2)`.
- `build.sh` / `run.sh` — exact build/run commands.
- `panic.txt` — serial console capture of the baseline panic.
- `fix.diff` — one-line NULL guard in `devfs_inode_to_vnode()`.
- `VERDICT.md` / `manifest.json` / `verdict.json` — full verdict + fix
  validation results.

## Reproduce

```
./build.sh     # cc -O -o fhdevfs fhdevfs.c   (in-guest)
./run.sh       # unprivileged: EPERM (gate works) ; root: kernel panic
```

Expected baseline (root): `Fatal trap 12: page fault while in kernel mode`,
`fault virtual address = 0x0`, `Stopped at lockmgr_exclusive+0x5d`,
current process = fhdevfs. Guest dies → `vm.sh status` ⇒ down.

Expected after fix: `survived: fd=-1 errno=2 (No such file or directory)`,
guest stays up.
