# DF-3191 — VERDICT

**status: reproduced / impact: panic (kernel memory corruption machinery, root-gated)**

## Baseline run (stock INVARIANTS kernel #0, fuse.ko stock)

```
$ cc -O2 -Wall -o dm dm.c && kldload fuse && ./dm
[995] opened /dev/fuse fd=3
[995] daemon loop on fd 3
[995] REQ op=26 unique=0          <- mount A FUSE_INIT
[995] REQ op=17 unique=1          <- mount A mount-time FUSE_STATFS
[996] mount(/mnt/dfa) OK
[995] REQ op=26 unique=2          <- mount B FUSE_INIT on the SAME fd (no guard)
[995] REQ op=17 unique=3          <- mount B mount-time FUSE_STATFS
```
serial console (run.log / panic.txt):
```
fuse_mount(dm|996): FUSE UABI 7.28
fuse_mount(dm|996): FUSE UABI 7.28
panic: assertion "fmp->rfnp->vp == NULL" failed in fuse_unmount at /usr/src/sys/vfs/fuse/fuse_vfsops.c:281
fuse_unmount() at fuse_unmount+0x419
sys_unmount() at sys_unmount+0x1f2 ...
```

Exactly the analyzed chain: both mounts succeed on one `fmp`
(sys/vfs/fuse/fuse_vfsops.c:168-199 — no re-association guard); mount A's
`umount` then inspects/frees **mount B's** root node (`fmp->rfnp` was
overwritten at fuse_vfsops.c:194): the KKASSERT at fuse_vfsops.c:281
trips because B's post-mount path already instantiated B's root vnode.
Guest wedged in ddb (`vm.sh status` ⇒ down); fresh state each run —
repeated twice (identical panic both times, see run.log and the fix-phase
baseline below).

## Why it is memory corruption, not just a panic

With file activity on mount A before the second mount, A's nodes sit in
the tree that mount B's `RB_INIT()` (fuse_vfsops.c:183) detaches. Mount
A's unmount then reclaims A's vnodes; `fuse_vop_reclaim` exempts only
`fmp->rfnp` — which is B's root now — so A's nodes go through
`fuse_node_free()` → `RB_REMOVE()` on a tree they are no longer in.
DFly's `RB_REMOVE` (sys/sys/tree.h:585-656) does **not** search the tree;
it splices on the node's own stale links, and when the stale
`RB_PARENT(elm) == NULL` it executes `RB_ROOT(head) = child`
(tree.h:651-652), **replacing mount B's live tree root with a stale
subtree pointer**. Nodes in that subtree are subsequently freed via
objcache and recycled → every later lookup/walk on mount B is a UAF.
The deterministic panic observed here is the simplest manifestation; the
splice corruption is reachable by adding lookups on /mnt/dfa between the
mounts.

## Exploit chain (why it stops at root)

The trigger requires two mount(2) calls, i.e. mount privilege.
`fuse_mount()` requires `caps_priv_check(cred, SYSCAP_NOMOUNT_FUSE)`
(fuse_vfsops.c:153-155); `SYSCAP_NOMOUNT_FUSE = __SYSCAP_GROUP_10 | 4 =
0xA4` (sys/caps.h:227) has neither `__SYSCAP_NOROOTTEST` (0x40000) nor
`__SYSCAP_WHEELOK` (0x80000), so `caps_priv_check` returns EPERM for any
cred with uid != 0 (sys/kern/kern_caps.c:328-331) — even with
`vfs.usermount=1` a non-root user cannot reach this code. No
unprivileged→root chain; severity is "root-gated kernel memory
corruption + DoS".

## Fix validation

- `fix.diff` (authored after reproduction, applied to the guest's
  /usr/src, fuse.ko rebuilt in /usr/src/sys/vfs/fuse, installed, reboot):
  baseline re-verified (panic reproduced again on the unpatched boot),
  then patched run:
```
[855] mount(/mnt/dfa) OK
[855] mount(/mnt/dfb) FAILED: Device busy      <- EBUSY from the guard
[855] second mount refused (FIXED?)
```
  No panic; guest stays up (fix_run.log, fix_build_tail.log).

## Kernel references

- sys/vfs/fuse/fuse_vfsops.c:152-199 (no re-association guard; re-init of live locks/lists/tree; rfnp overwrite at :194)
- sys/vfs/fuse/fuse_vfsops.c:266-294 (unmount frees the *other* mount's rfnp; KKASSERT :281)
- sys/vfs/fuse/fuse_vnops.c:1786-1801 (fuse_vop_reclaim exempts only fmp->rfnp)
- sys/sys/tree.h:585-656 (RB_REMOVE splice without membership check; root overwrite at :651-652)
- sys/kern/vfs_syscalls.c:344-349 (`hasmount` guards the mountpoint, not the device)
- sys/kern/kern_caps.c:328-331 + sys/caps.h:227 (mount is uid-0-only)

## 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.
