fuse_vop_getattr returns success without initializing *a_vap (dead mount / ENOSYS-latched GETATTR) β unprivileged kernel-stack info leak incl. kernel pointers via stat()/fstat()
| Field | Value |
|---|---|
| ID | DF-3027 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L |
| CWE | CWE-908 / CWE-200 |
| File | sys/vfs/fuse/fuse_vnops.c |
| Lines | 312-338 (consumer: vfs_vnops.c:833-845) |
| Area | vfs/fuse |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
fuse_vop_getattr returns 0 with the caller's *a_vap untouched when fuse_test_dead() or fuse_test_nosys(FUSE_GETATTR) is set, and converts a βENOSYS reply to success without filling the vattr. vn_stat uses an UNINITIALIZED stack struct vattr and copies its fields into the user's struct stat on success, so st_ino/st_uid/st_gid/st_size/st_nlink/ st_{a,m,c}timespec are raw kernel stack bytes. The garbage vattr also feeds nlookup permission evaluation (observed EACCES/EBADF misbehavior). Any unprivileged user able to stat/fstat a file on a FUSE mount whose daemon died (crash/exit β common) or whose daemon replied βENOSYS to GETATTR once (latches globally via fuse_set_nosys). ~100 bytes of kernel stack per call, repeatable, attacker-groomable content (kernel pointers demonstrated) β KASLR/heap-address oracle usable to assist other exploits. VERIFIED on the guest as unprivileged user on a dead mount: fstat returned kernel heap pointers (0xfffff801xxxxxxxx) in 3/3 runs plus a deterministic marker-echo proof; patched module returns zeroed attrs.
Timeline
- 2026-09-02 Discovered during pass-2 audit of fuse_vnops.c (GLM 5.3); unpriv stack leak reproduced 3/3 + fix validated on rebuilt module.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3027 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fuse_daemon.c | β | 16.5 KB | view raw | |
| stat_leak.c | β | 5.7 KB | view raw | |
| build.sh | β | 126 B | view raw | |
| run.sh | β | 1.6 KB | view raw | |
| run.log | β | 1.6 KB | view raw | |
| env.txt | β | 203 B | view raw | |
| fix.diff | β | 1000 B | view raw | |
| fix_run.log | β | 487 B | view raw | |
| fix_build.log | β | 54 B | view raw | |
| VERDICT.md | β | 2.7 KB | β raw |
DF-3027 verdict β REPRODUCED (kernel-stack info leak, unprivileged)
Root cause (path:line)
sys/vfs/fuse/fuse_vnops.c:312-316βfuse_vop_getattrreturns 0 with*a_vapuntouched for dead mounts and ENOSYS-latched FUSE_GETATTR.sys/vfs/fuse/fuse_vnops.c:329-338β-ENOSYSreply converted to success (error = 0) without filling*a_vap.sys/kern/vfs_vnops.c:833-845βvn_statstack-allocatesstruct vattr vattr;uninitialized, and on success copiesvapfields into the user'sstruct stat(st_ino/st_uid/st_gid/st_size/st_nlink/st_{a,m,c}timespec).
How it was reproduced (guest DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC)
- root mounts a synthetic FUSE fs via
fuse_daemon normal /mnt/fuse. - unprivileged user
maxxopens/mnt/fuse/target(attrs served normally). - daemon is SIGKILLed -> cdevpriv dtor ->
fuse_mount_kill->dead=1. - maxx grooms his kernel stack:
fstat(marker_fd)puts a validva_type(VREG) into the exactvn_statvattr slot; thesu/shell machinery (andreadlinkin ptrs-mode) leaves kernel pointers in adjacent slot bytes. - maxx
fstat(fuse_fd)->fuse_vop_getattrreturns 0 instantly (dead path) ->vn_statcopies the stale stack slot to userspace -> success with kernel pointer fields.
Observed (3/3 runs, values differ per run β live kernel data):
mt=fffff80118c0b7d8.fffff8011712d300 ct=fffff80117172780.fffff80118c0b8f8 mt=fffff801166db7d8.fffff8011712ff00 ct=fffff80117172c00.fffff801166db8f8 mt=fffff801183df7d8.fffff80117132780 ct=fffff80117173080.fffff801183df8f8
0xfffff801.... / 0xfffff800.... are kernel virtual addresses (KVA) β
heap/vm-object pointers left by earlier syscall frames. An earlier variant
also demonstrated a deterministic marker echo: the fuse file reported the
previous fstat's attributes verbatim (probe4, evidence in transcript), the
cleanest possible proof of the uninitialized read.
Additional observation: the same uninitialized vattr feeds permission
evaluation β ls: /mnt/fuse: Bad file descriptor (vn_stat default EBADF on
garbage va_type) and stat: Permission denied (garbage va_mode) were both
observed, i.e. the garbage also drives access decisions.
Impact
Unprivileged kernel-stack disclosure, ~100 bytes/call, repeatable at will, content groomable (kernel pointers demonstrated). KASLR-defeating primitive on kernels with KASLR; heap-address oracle for other exploits.
Fix validation
fix.diff (zero + type-fill *a_vap on every success-without-attrs path)
applied in the guest, module rebuilt, same PoC re-run: LEAK returns zeroed
attributes (ino=0 sz=0 ptrlike=0), no marker echo, guest healthy.
fix_status=fixed.
Fix verification
fixedPatched module re-run of the identical PoC: fstat succeeds with zeroed attributes (ino=0 uid=0 sz=0 times=0, ptrlike=0), no marker echo; leak eliminated. Guest stable.
fix_run.log, fix.diff
Confirmed kernel references
Detail
Exploit chain
root (or usermount owner) mounts fuse with any daemon; daemon dies/EXITs (or replies -ENOSYS once); unprivileged user opens a file before death, then grooms kernel stack with fstat(marker)+deep syscalls and fstats the held fd -> ~100 bytes of kernel stack per call incl. kernel pointers (KASLR/heap oracle)
Evidence (decisive lines)
['run.log: LEAK lines mt=fffff80118c0b7d8 ct=fffff80117172780 across 3 runs', 'fix_run.log: patched kernel returns zeroed attrs ptrlike=0', 'VERDICT.md: full trace']
PoC changes
pass-1 DF-0780 harness adapted: added daemon modes (normal/enosys-file-getattr/vbad-lookup) with a synthetic name namespace; final trigger is two-phase (open while alive -> daemon killed -> groom+ fstat) because unpriv traversal of a dead mount requires a pre-opened fd and the deterministic proof needs no nlookup between groom and read
Verified recommended fix
Zero-fill *a_vap and set va_type=vp->v_type on every early-success return of fuse_vop_getattr (as done for the existing ENOTCONN&&VROOT case).
Verdict
fuse_vop_getattr returns success without initializing the caller's *a_vap on dead mounts and on ENOSYS-latched/converted FUSE_GETATTR (fuse_vnops.c:312-316,329-338); vn_stat copies its uninitialized stack vattr into the user's stat structure. Reproduced as unprivileged user maxx on a dead FUSE mount: fstat returned kernel stack contents including kernel heap pointers (mt/ct/sz = 0xfffff801xxxxxxxx), 3/3 runs with per-run-varying values, plus a deterministic marker-echo proof. The same garbage vattr also feeds permission evaluation (observed EBADF/EACCES side effects).
No comments yet.