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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3027 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

DF-3027 verdict β€” REPRODUCED (kernel-stack info leak, unprivileged)

Root cause (path:line)

  • sys/vfs/fuse/fuse_vnops.c:312-316 β€” fuse_vop_getattr returns 0 with *a_vap untouched for dead mounts and ENOSYS-latched FUSE_GETATTR.
  • sys/vfs/fuse/fuse_vnops.c:329-338 β€” -ENOSYS reply converted to success (error = 0) without filling *a_vap.
  • sys/kern/vfs_vnops.c:833-845 β€” vn_stat stack-allocates struct vattr vattr; uninitialized, and on success copies vap fields into the user's struct 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)

  1. root mounts a synthetic FUSE fs via fuse_daemon normal /mnt/fuse.
  2. unprivileged user maxx opens /mnt/fuse/target (attrs served normally).
  3. daemon is SIGKILLed -> cdevpriv dtor -> fuse_mount_kill -> dead=1.
  4. maxx grooms his kernel stack: fstat(marker_fd) puts a valid va_type (VREG) into the exact vn_stat vattr slot; the su/shell machinery (and readlink in ptrs-mode) leaves kernel pointers in adjacent slot bytes.
  5. maxx fstat(fuse_fd) -> fuse_vop_getattr returns 0 instantly (dead path) -> vn_stat copies 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (fuse.ko rebuilt with fix.diff in-guest)

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).