# DF-3207 VERDICT

**status: reproduced / impact: panic (kernel DoS) / confidence: certain**

## Root cause (line-accurate)

1. `sys/vfs/nullfs/null_vfsops.c:261-267` — `nullfs_unmount()` frees the
   filesystem-private `null_mount` and NULLs `mp->mnt_data` with no
   protection for late callers. This is by itself fine *if* the vfs core
   guarantees no VFS op can be in flight or arrive later — it doesn't
   under `umount -f`.

2. `sys/kern/vfs_syscalls.c:964-1006` — `dounmount(MNT_FORCE)`: when the
   50x250ms retry loop exhausts with `mp->mnt_refs != 1`, it prints
   "forcing unmount", sets `freeok = 0` and **still calls VFS_UNMOUNT**
   (error remains 0). The `struct mount` is intentionally leaked
   (`freeok == 0` skips the `while (mp->mnt_refs > 0)` wait at `:1107-1121`)
   — the kernel's own contract is that the leaked mount may still be
   referenced, yet filesystem-private data is already freed.

3. `sys/kern/vfs_syscalls.c:727-757` — `process_uses_mount()` only scans
   fd tables and cwd/root/jail nhandles. A `struct file` parked in a
   socket receive buffer via `SCM_RIGHTS` keeps `fp->f_nchandle.mount`
   (mount ref) on the nullfs mount invisibly: the `umount -f` SIGINT/SIGKILL
   scans (retries 3 and 7, `:931-947`) never kill its owner.

4. `sys/kern/vfs_syscalls.c:1495-1512` — `kern_fstatfs()` takes
   `mp = fp->f_nchandle.mount` and calls `VFS_STATFS(mp, ...)` with **no**
   `vfs_busy()` / `MNTK_UNMOUNT` interlock. Same gap in `kern_statfs()`
   (`:1428-1436`) and `sys_quotactl()` (`:1244-1247`). Contrast
   `kern_mountctl()` (`:1401`: explicit `MNTK_UNMOUNT` check, "racing
   unmount" warning) and `mountlist_scan()` (`sys/kern/vfs_mount.c:763`:
   `vfs_busy(mp, LK_NOWAIT)`), which both do interlock.

5. `sys/vfs/nullfs/null_vfsops.c:298-310` — `nullfs_statfs()` executes
   `VFS_STATFS(MOUNTTONULLMOUNT(mp)->nullm_vfs, ...)`; with
   `mp->mnt_data == NULL` that is a load from kernel VA 0.

## How it was reproduced (guest, DragonFly 6.5-DEVELOPMENT #0, 6 CPUs)

Deterministic chain (see README): mount_null as root; unpriv user `test`
parks an fd on `/mnt/a/f1` in a socketpair via SCM_RIGHTS; root
`umount -f /mnt/a` exhausts its retry loop (dmesg: "0 namecache refs,
1 mount refs still present" -> "forcing unmount") and frees `null_mount`;
unpriv user recovers the fd with `recvmsg()` and calls `fstatfs(2)`.

Serial console (`panic.txt`):

```
Fatal user address access from kernel mode from ghost at ffffffff807105e2
Fatal trap 12: page fault while in kernel mode
fault virtual address     = 0x0
code segment              = ...
current process           = 6405
Stopped at nullfs_statfs+0x32:     movq (%rax),%rdi
```

`nullfs_statfs+0x32` is exactly the `MOUNTTONULLMOUNT(mp)->nullm_vfs`
load with `%rax == 0` (`mnt_data` NULLed by `nullfs_unmount:262`).
ssh died, `vm.sh status` => down (DDB prompt `db>`).

The brute-force race variant (`spam.c` + `driver.sh`, 2400
mount/umount- -f rounds with 12 unpriv statfs/fstatfs/quotactl threads)
did NOT hit — and the source explains why: an in-flight statfs holds the
mount ref that keeps `dounmount`'s retry loop spinning until the statfs
completes; the straddle window requires a thread stalled >= 12.5s inside
a ~50ns window. The exhausted-force path (above) removes the race
entirely. Both variants are the same missing interlock.

## Why not uid=0

The memory-unsafety primitive is (a) a NULL-deref read (deterministic,
panic) and (b) a read of a freed 88-byte `M_NULLFSMNT` chunk (race
variant). `M_NULLFSMNT` is a nullfs-private malloc type: the only code
that allocates it is `nullfs_mount:120` (`M_ZERO`, then two kernel
pointers). An attacker cannot spray controlled content into a freed
chunk of that type (mount_null requires root unless `vfs.usermount=1`,
and even then the content is M_ZERO + kernel pointers). The stale
`nullm_vfs` read resolves to a valid mount or garbage — worst case a
call through a garbage vfsops pointer (panic), not a controlled call.
No write primitive, no forge -> no escalation chain. Honest ceiling:
kernel panic / local DoS. (With `vfs.usermount=1`, an unprivileged user
who owns the nullfs mount can execute the entire chain including
`umount -f` — still a panic, not privesc.)

## Fix validation (mandatory, done)

`fix.diff` (authored against the read-only `sys/`; applied only inside
the guest's `/usr/src` copy): adds `vfs_busy(mp, LK_NOWAIT)` /
`vfs_unbusy(mp)` interlocks around the VFS calls in `kern_statfs`,
`kern_fstatfs`, `sys_quotactl` — mirroring `mountlist_scan()`. When
`MNTK_UNMOUNT` is set (i.e. after/before a completed or in-progress
unmount) the syscalls fail with EBUSY instead of dereferencing freed
`mnt_data`; the shared `mnt_lock` excludes `dounmount`'s exclusive hold
for in-flight calls.

- Baseline: stock kernel #0 -> panic (above), guest down. (fix_baseline_reproduced = 1)
- Patched: `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (#1, 2026-09-08),
  same exact PoC, 4 rounds: `fstatfs returned errno=16 (EBUSY)`, guest up,
  mount list clean. (fix_patched_reproduced = 0, fix_status = fixed)

Deeper alternative (not needed for this PoC, noted for upstream):
nullfs ops could additionally treat `mp->mnt_data == NULL` as EINVAL, and
`dounmount` could refuse MNT_FORCE when refs belong to non-process
holders (socket-buffered fps / stacked mounts) — but the syscall-side
interlock is the root-cause fix matching existing kernel idiom.

## Kernel references

- sys/vfs/nullfs/null_vfsops.c:261-267 (unmount frees mnt_data)
- sys/vfs/nullfs/null_vfsops.c:294, 308, 341 (unguarded MOUNTTONULLMOUNT derefs)
- sys/kern/vfs_syscalls.c:888-1006 (retry loop + forced VFS_UNMOUNT with refs>1)
- sys/kern/vfs_syscalls.c:727-757 (kill-scan blind to sockbuf-held fps)
- sys/kern/vfs_syscalls.c:1244-1247, 1428-1436, 1495-1512 (missing interlock)
- sys/kern/vfs_syscalls.c:1401 (mountctl's existing check — the model)
- sys/kern/vfs_mount.c:271-305 (vfs_busy / MNTK_UNMOUNT interlock)

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
