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

fuse_mount() never cross-checks args.fd with the looked-up /dev/fuse name: devfs_get_cdevpriv() returns ANY driver's per-fd private data, so a foreign fd (cuse/netmap/evdev/ipmi/nvmm) is type-confused with struct fuse_mount and a fuse_mount layout is written into the foreign live object

Summary

fuse_mount() name-lookups args.from (:140-150) and access-checks that vnode, but then obtains fmp from args.fd via devfs_get_cdevpriv (:168), which returns whatever private data any driver attached to that file (devfs_core.c:2938-2958). A /dev/cuse fd (cuse_server) - or netmap/evdev/ipmi/nvmm - is then reinterpreted as struct fuse_mount and :176-199 stamp mp, dead, three mtx_init, two TAILQ_INIT, RB_INIT, devvp, cred, refcount_acquire, plus fuse_node_new's tree root into the victim: cross-driver type confusion and an OOB write when the victim allocation is smaller than struct fuse_mount. The victim's own dtor later walks fuse pointers. VERIFIED stock guest (kldload cuse; mount with fd=<cuse>, from=/dev/fuse): 'panic: assertion fmp->refcnt > 0 failed in fuse_mount :186' - fired AFTER the fuse_mount layout was already written into the cuse_server at :176-185, proving the confusion. Root-gated. Fix validated: f_type/f_data == devvp cross-check -> EINVAL before any foreign write.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3192 Β· 10 files
FileTypeDescriptionSize
tc.c β€” 3.0 KB view raw
run.sh β€” 125 B view raw
run.log β€” 725 B view raw
panic.txt β€” 333 B view raw
serial_full.txt β€” 2.1 KB view raw
env.txt β€” 278 B view raw
code_hash.txt β€” 65 B view raw
fix.diff β€” 3.5 KB view raw
fix_run.log β€” 372 B view raw
VERDICT.md β€” 3.6 KB ↓ raw
VERDICT.md
↓ download raw

DF-3192 β€” VERDICT

status: reproduced / impact: panic (cross-driver type confusion + OOB write, root-gated)

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

$ kldload fuse; kldload cuse; cc -O2 -Wall -o tc tc.c && ./tc
opened /dev/cuse fd=3 β€” cdevpriv is now a struct cuse_server*
mount(fuse, /mnt/tc3192, fd=<cuse>) β€” kernel now writes a fuse_mount layout into the cuse_server ...

serial console (run.log / panic.txt):

Cuse v0.1.37 @ /dev/cuse
fuse_init(kldload|831): FUSE ABI 7.28
panic: assertion "fmp->refcnt > 0" failed in fuse_mount at /usr/src/sys/vfs/fuse/fuse_vfsops.c:186
fuse_mount.part.6() at fuse_mount.part.6+0x777
sys_mount() at sys_mount+0x36b

The panic fires at fuse_vfsops.c:186 β€” i.e. after fuse_mount already executed :176-185 against the cuse_server: fmp->mp = mp, fmp->dead = false, mtx_init() Γ—3, TAILQ_INIT() Γ—2, RB_INIT(), fmp->devvp, fmp->cred = crhold(cred) β€” a struct fuse_mount layout stamped over the foreign struct cuse_server (cuse.c:768-777). The KKASSERT reads the fuse-refcnt offset inside cuse_server memory (M_ZERO'd at open) and reads 0 β†’ INVARIANTS panic. This is direct proof of the type confusion; with a victim object whose bytes at that offset are non-zero, initialization continues (node tree inside the victim, INIT IPC on the victim's TAILQs, wakeup(fmp) on a foreign address) and the corrupted object detonates later inside its own driver (e.g. cuse_server_free walking fuse pointers, cv_destroy on a clobbered cv). fuse_mount layout is also larger than struct cuse_server, so the writes at :176-199 are an OOB write into adjacent M_CUSE slab objects.

Why it stops at root

Same privilege ceiling as DF-3191: the mount must pass caps_priv_check(cred, SYSCAP_NOMOUNT_FUSE) (fuse_vfsops.c:153-155); SYSCAP_NOMOUNT_FUSE = 0xA4 carries no __SYSCAP_NOROOTTEST/__SYSCAP_WHEELOK (sys/caps.h:96-97,227; sys/kern/kern_caps.c:328-331), so uid != 0 gets EPERM before reaching the confusion. Not an unprivileged escalation; root-gated cross-driver kernel memory corruption.

Fix validation

fix.diff applied in guest /usr/src, fuse.ko rebuilt, rebooted:

$ ./tc
opened /dev/cuse fd=3 β€” cdevpriv is now a struct cuse_server*
mount(fuse, /mnt/tc3192, fd=<cuse>) ...
mount failed: Invalid argument        <- EINVAL at the fd==devvp check, BEFORE any foreign write
closing the cuse fd -> cuse_server_free dtor on corrupted object
survived close
TC_RC=0

No panic; guest up (fix_run.log). The EINVAL is returned by the new file->f_type != DTYPE_VNODE || file->f_data != devvp check before devfs_get_cdevpriv() is even called, so the cuse_server is never touched (the PoC's canned "corrupted" text is informational only).

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

Patched kernel returns EINVAL for the cuse fd ('mount failed: Invalid argument') before any foreign-object write; close() of the cuse fd runs the intact cuse_server_free; no panic; guest stays up.

fix_run.log, 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

open('/dev/cuse') -> cuse_server_open attaches struct cuse_server as cdevpriv; mount('fuse', mnt, fd=, from='/dev/fuse') -> fuse_mount writes fuse_mount layout into cuse_server (lines 176-199) -> panic at :186 on INVARIANTS, or delayed corruption of the cuse/netmap/evdev/ipmi/nvmm object when the bytes differ.

Evidence (decisive lines)

run.log + panic.txt: 'panic: assertion "fmp->refcnt > 0" failed in fuse_mount at fuse_vfsops.c:186' with sys_mount backtrace, preceded by 'Cuse v0.1.37 @ /dev/cuse'. fix_run.log: EINVAL before any foreign write, close survives, guest up.

PoC changes

Standalone PoC authored in this run (no seed).

Verified recommended fix

In fuse_mount(), require file->f_type == DTYPE_VNODE && file->f_data == devvp (the looked-up /dev/fuse vnode) before using devfs_get_cdevpriv(); return EINVAL otherwise.

Verdict

fuse_mount() name-lookups args.from (/dev/fuse) for the access check but pulls the fuse_mount from args.fd via devfs_get_cdevpriv(), which returns any driver's per-fd private data. A /dev/cuse fd is reinterpreted as struct fuse_mount: lines 176-185 stamp mp/dead/locks/lists/tree/devvp/cred into the live cuse_server (OOB write: fuse_mount is larger), then KKASSERT(fmp->refcnt > 0) reads zeroed foreign bytes and panics. Root-only trigger (same SYSCAP_NOMOUNT_FUSE ceiling as DF-3191).