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)
PoC verification
Evidence pack
findings/poc/DF-3192 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- sys/vfs/fuse/fuse_vfsops.c:140-168 (name looked up, fd used, never cross-checked)
- sys/vfs/fuse/fuse_vfsops.c:176-199 (fuse_mount layout written into the foreign object)
- sys/vfs/devfs/devfs_core.c:2938-2958 (devfs_get_cdevpriv returns any driver's cdevpriv)
- sys/dev/misc/cuse/cuse.c:768-777 (cuse_server attached to /dev/cuse at open)
- sys/dev/virtual/nvmm/nvmm_dragonfly.c:315, net/netmap/netmap_freebsd.c:387, dev/misc/evdev/cdev.c:150, dev/misc/ipmi/ipmi.c:99 (other cdevpriv victims)
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
fixedPatched 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
Confirmed kernel references
Detail
Exploit chain
open('/dev/cuse') -> cuse_server_open attaches struct cuse_server as cdevpriv; mount('fuse', mnt, fd=
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).
No comments yet.