β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3191

fuse_mount() lacks a double-mount guard: one /dev/fuse fd mounted at two mount points shares one struct fuse_mount β€” second mount re-initializes the live object (locks/IPC lists/RB tree/rfnp) and the first unmount frees the second mount's root and splices stale nodes into its tree (root-clobber UAF; guaranteed panic)

Summary

fuse_mount() pulls the per-fd struct fuse_mount via devfs_get_cdevpriv (:168) and never checks it is already associated with a mount; sys_mount's hasmount guard only covers the mountpoint, not the device. Mounting the same fd a second time re-runs the init block on the live object: mtx_init over possibly-held locks, TAILQ_INIT orphaning in-flight IPCs, RB_INIT orphaning mount A's nodes, spin_init/bioq re-init under the running io thread, a second fuse_io_thread, and fmp->rfnp overwritten with mount B's root (:194). The first fuse_unmount then KKASSERTs on and frees mount B's root node (:281-283) and its vflush reclaims A's orphaned nodes via fuse_node_free -> RB_REMOVE, which never verifies membership and with stale parent==NULL overwrites the live tree root (tree.h:651-652) - every later walk of B's tree can traverse freed fuse_node objects. VERIFIED stock guest: two mounts + umount A + stat B -> 'panic: assertion fmp->rfnp->vp == NULL failed in fuse_unmount :281' reproduced on fresh boots; with file activity on A before mount B the RB_REMOVE splice clobbers B's tree root -> UAF walks. Root-gated (SYSCAP_NOMOUNT_FUSE has no NOROOTTEST/WHEELOK - non-root cannot mount FUSE). Fix validated: atomic_cmpset_ptr claim + EBUSY on second mount, no panic.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3191 Β· 12 files
FileTypeDescriptionSize
dm.c β€” 8.3 KB view raw
run.sh β€” 111 B view raw
run.log β€” 1.1 KB view raw
panic.txt β€” 492 B view raw
serial_full.txt β€” 2.2 KB view raw
env.txt β€” 278 B view raw
code_hash.txt β€” 65 B view raw
fix.diff β€” 3.5 KB view raw
fix_build.log β€” 1.6 KB view raw
fix_build_tail.log β€” 48 B view raw
fix_run.log β€” 207 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
VERDICT.md
↓ download raw

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

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied fix.diff in guest /usr/src, rebuilt fuse.ko, rebooted: baseline (unpatched boot) panicked identically; patched kernel refuses the second mount with EBUSY ('mount(/mnt/dfb) FAILED: Device busy'), no panic, guest up.

fix_run.log (EBUSY line), fix_build_tail.log (module link), fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (patched fuse.ko, module rebuild)

Confirmed kernel references

Detail

Exploit chain

mount(fd)@A; mount(fd)@B (same fd, EBUSY absent); umount A -> frees B's rfnp + RB_REMOVE-splices A's orphaned nodes (tree.h:651 root clobber) -> any lookup on B panics (KASSERT 'no root node') or walks freed nodes (UAF). Stops at root: mounting FUSE requires uid 0 (fuse_vfsops.c:155 + kern_caps.c:328).

Evidence (decisive lines)

run.log + panic.txt: 'panic: assertion "fmp->rfnp->vp == NULL" failed in fuse_unmount at fuse_vfsops.c:281' with fuse_unmount/dounmount backtrace on serial console; both mounts' INIT/STATFS exchanges precede it. fix_run.log: second mount -> EBUSY, guest survives.

PoC changes

Self-contained daemon+mounter adapted from the DF-0895 harness; no fixes to the seed were needed (finding authored in this run).

Verified recommended fix

Atomically claim fmp->mp (atomic_cmpset_ptr) in fuse_mount and return EBUSY on re-association; release the claim on post-claim error paths.

Verdict

Deterministic kernel panic (and node-tree corruption machinery) from mounting one /dev/fuse fd at two mount points: fuse_mount() has no re-association guard, so the second mount re-initializes the live struct fuse_mount and steals fmp->rfnp; the first unmount then KKASSERTs on / frees the second mount's root node. Root-only trigger (SYSCAP_NOMOUNT_FUSE has no NOROOTTEST/WHEELOK), so panic/DoS + root-gated heap corruption, not an escalation boundary.