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

/dev/fuse open/close without mounting leaks struct fuse_mount β€” cdevpriv dtor skips the free on the ENODEV path

Summary

fuse_device_open kmallocs a fuse_mount (refcnt=1) per open (:53-57); fuse_cdevpriv_dtor frees only when fuse_cdevpriv_close returns 0, but that returns ENODEV for a never-mounted fd (:84-88) - so every open('/dev/fuse')+close() without a mount leaks ~256B M_TEMP. MEASURED on guest: 100k-cycle loop grows temp 335K -> 20.3MB (~256B/cycle, linear, unbounded); devfs_set_cdevpriv's return also ignored (:57). Group operator or root (device 0660 root:operator): slow unbounded kernel-heap exhaustion DoS. Fix v2 validated over 20k cycles (v1's naive always-free hit crfree(NULL) since cred is only set at mount - regression documented in fix_v1_panic.txt).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3228 Β· 10 files
FileTypeDescriptionSize
df3228.c β€” 2.0 KB view raw
build.sh β€” 112 B view raw
run.log β€” 1.5 KB view raw
fix_run.log β€” 122 B view raw
fix_run2.log β€” 320 B view raw
env.txt β€” 318 B view raw
fix.diff β€” 991 B view raw
verdict.json β€” 3.4 KB view raw
manifest.json β€” 765 B view raw
VERDICT.md β€” 2.5 KB ↓ raw
VERDICT.md
↓ download raw

DF-3228 β€” VERDICT

Status: REPRODUCED (deterministic) β€” impact: dos (slow unbounded kernel heap exhaustion, semi-privileged) β€” fix VALIDATED in-guest (v2).

Root cause

fuse_device_open() (fuse_device.c:48-61) kmallocs struct fuse_mount (M_TEMP, refcnt=1) per open and installs it as cdevpriv; the return of devfs_set_cdevpriv() is ignored (fuse_device.c:57). The dtor fuse_cdevpriv_dtor() (fuse_device.c:38-45) frees only if (!fuse_cdevpriv_close(fmp)) β€” but fuse_cdevpriv_close() returns ENODEV when the fd was never mounted (!fmp->devvp, fuse_device.c:84-88). Never-mounted open/close therefore drops nothing: the fmp allocated at open leaks on every cycle. Nothing else holds or frees it (locks are only mtx_init'd at mount time, fuse_vfsops.c:178-180; cred only assigned at mount, fuse_vfsops.c:185).

Guest run (stock #0 INVARIANTS kernel)

  • baseline vmstat -m | grep temp: 472 allocs / 335K
  • after ./df3228 100000 (loop still running when sampled): 46.1K allocs / 20.3MB β€” ~256 B leaked per cycle (β‰ˆ sizeof(struct fuse_mount)), linear and unbounded (run.log).
  • Reachability: /dev/fuse is crw-rw---- root operator 0660 (fuse_device.c:313-314) β€” operator group or root; slow memory-exhaustion DoS (CWE-401). Low severity.

Fix iteration (documented honestly)

  • v1 ("always fuse_mount_free()"): WRONG β€” validated to panic: fuse_mount_free() β†’ crfree(fmp->cred) with cred == NULL (never-mounted) β†’ Fatal trap 12 ... crfree+0xc (fix_run.log + serial log, guest reset). This is itself evidence that the pre-fix code could never have taken this path.
  • v2 (fix.diff): dtor distinguishes fmp->devvp (mounted: kill + shared fuse_mount_free()) from never-mounted (refcnt==1, no locks, no cred β†’ plain kfree(fmp, M_TEMP)); fuse_device_open checks devfs_set_cdevpriv() and frees on failure.
  • v2 validated: 20000 cycles β†’ temp identical (472 allocs/335K) before/after, no panic (fix_run2.log); fuse module otherwise regression-checked by the DF-3227 patched runs (mount/INIT/statfs/ read/reply/unmount all healthy on the same module build).

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 fuse.ko: 20000 open+close cycles leave vmstat -m 'temp' byte-identical to baseline (472 allocs/335K) with no panic; baseline leaked ~256B/cycle.

['findings/poc/DF-3228/fix_run2.log', 'findings/poc/DF-3228/fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (fuse.ko module rebuilt in-guest with fix.diff v2)

Confirmed kernel references

Detail

Exploit chain

operator/root account: loop open+close on /dev/fuse without mounting -> ~256B M_TEMP leak per cycle -> unbounded kernel heap exhaustion -> memory-pressure DoS for all kernel subsystems.

Evidence (decisive lines)

['findings/poc/DF-3228/run.log β€” baseline temp 472/335K -> 46.1K/20.3MB after 100k cycles', 'findings/poc/DF-3228/fix_run2.log β€” patched: 20000 cycles, temp identical 472/335K before/after, no panic', 'findings/poc/DF-3228/fix_run.log β€” fix v1 regression proof: plain fuse_mount_free() on never-mounted fmp panics in crfree(NULL) (Fatal trap 12 crfree+0xc) β€” why fix v2 uses plain kfree']

PoC changes

none β€” trivial loop written fresh; format warnings (int vs long) in the printf summary are cosmetic and do not affect the leak measurement.

Verified recommended fix

fuse_cdevpriv_dtor: if (fmp->devvp) { fuse_cdevpriv_close(fmp); fuse_mount_free(fmp); } else kfree(fmp, M_TEMP); plus check devfs_set_cdevpriv() return in fuse_device_open (see fix.diff).

Verdict

REPRODUCED on the stock guest: fuse_cdevpriv_dtor (fuse_device.c:38-45) drops the open-time reference only when fuse_cdevpriv_close() returns 0; for a never-mounted fd it returns ENODEV (fuse_device.c:84-88) and the dtor skips fuse_mount_free() β€” every open('/dev/fuse')+close() without a mount leaks sizeof(struct fuse_mount). Measured: vmstat -m 'temp' grew 472 allocs/335K -> 46.1K allocs/20.3MB within ~5 min of a 100k-cycle loop (still running when sampled), linear and unbounded. Semi-privileged primitive: /dev/fuse is 0660 root:operator (fuse_device.c:313-4), so group operator / root; slow kernel-heap exhaustion (CWE-401). devfs_set_cdevpriv()'s return is also ignored at fuse_device.c:57.