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)
PoC verification
Evidence pack
findings/poc/DF-3191 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 (
hasmountguards 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.
Fix verification
fixedApplied 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
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.
No comments yet.