/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)
PoC verification
Evidence pack
findings/poc/DF-3228 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/fuseiscrw-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)withcred == 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 + sharedfuse_mount_free()) from never-mounted (refcnt==1, no locks, no cred β plainkfree(fmp, M_TEMP));fuse_device_openchecksdevfs_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
fixedPatched 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']
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.
No comments yet.