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

Post-unmount fstatfs(2) on a still-open fd NULL-derefs in fuse_statfs: unguarded VFSTOFUSE(mp) after fuse_unmount clears mnt_data feeds fuse_ipc_get(NULL) -> fetch-add at 0x160 -> fatal trap 12 (deterministic panic)

Summary

fuse_unmount() stores mp->mnt_data = NULL as its last action (fuse_vfsops.c:294). kern_fstatfs takes the mount from fp->f_nchandle.mount with no vnode ops, so a dead-vnode guard cannot intercept; fuse_statfs (:388-393) has no NULL/dead entry guard and passes fmp=NULL into fuse_ipc_get(), whose atomic_fetchadd_long(&fmp->unique,1) faults at NULL+0x160. VERIFIED 3/3 on stock guest ('Stopped at fuse_ipc_get+0x63: lock xaddq %rax,0x160(%r13)'), twice incidentally during DF-3229 staging and once with the minimal race PoC (mount -> dir fd -> umount thread racing a fstatfs loop; no timing precision needed - the NULL window is inside the umount syscall). Standard admin-unmounts-under-user-traffic DoS: umount side needs mount owner/root, fstatfs side is any process holding an fd. Distinct from DF-3193 (freed-fmp walk on the WAKE path); this is the ENTRY path. Fix validated (NULL/dead entry guard in fuse_statfs/statvfs): identical run returns ENOTCONN, guest up.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3230 Β· 9 files
FileTypeDescriptionSize
df3230.c β€” 5.9 KB view raw
build.sh β€” 230 B view raw
run.sh β€” 156 B view raw
panic.txt β€” 748 B view raw
serial_full.txt β€” 19.8 KB view raw
fix.diff β€” 744 B view raw
verdict.json β€” 3.7 KB view raw
VERDICT.md β€” 3.1 KB ↓ raw
env.txt β€” 188 B view raw
VERDICT.md
↓ download raw

DF-3230 β€” post-unmount fstatfs(2) NULL-deref panic in fuse_statfs β†’ fuse_ipc_get

Status: REPRODUCED (deterministic, minimal PoC + 2 prior in-staging occurrences, identical signature) β€” panic β€” fix VALIDATED in-guest.

Root cause

  • fuse_unmount() stores mp->mnt_data = NULL at fuse_vfsops.c:294 as its last substantive action before returning to userspace.
  • An fstatfs(2) on a still-open fd whose file was on the fuse mount reaches kern_fstatfs (kern/vfs_syscalls.c:1495-1496), which takes the mount from fp->f_nchandle.mount (or the vnode's v_mount) β€” the struct mount is still valid and referenced; no vnode ops run, so no dead-vnode guard can intercept.
  • fuse_statfs() (fuse_vfsops.c:386-414) does fmp = VFSTOFUSE(mp) β€” now NULL β€” with no NULL / dead guard, and calls fuse_ipc_get(fmp, 0).
  • fuse_ipc_get() (fuse_ipc.c:100) executes atomic_fetchadd_long(&fmp->unique, 1) β†’ fetch-add at NULL+0x160 β†’
Fatal user address access from kernel mode from df3230 at ffffffff826094b3
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x160
fault code      = supervisor write data, page not present
instruction pointer = 0x8:0xffffffff826094b3
current process     = 1058 (df3230)
Stopped at      fuse_ipc_get+0x63:      lock xaddq      %rax,0x160(%r13)

Distinct from DF-3193 (freed-fmp walk on the wake path of an in-flight statfs): this is the entry path with a NULL fmp after a completed unmount, and it is deterministic β€” any fstatfs on an fd that outlives its fuse mount.

Reproducibility

  • 2 incidental occurrences during DF-3229 staging (full-rate fstatfs hammer crossing mnt_data=NULL; identical fuse_ipc_get+0x63 / 0x160 signature both times), plus
  • the minimal df3230.c (mount β†’ open dir fd β†’ umount thread racing a fstatfs loop): panic on the first run β€” the NULL window is inside the umount syscall, so no timing precision is needed (panic.txt, serial_full.txt).

Trust boundary: the umount side needs the mount owner/root (or usermount policy); the fstatfs side is any process holding an fd on the mount β€” a root-triggered unprivileged-user-DoS-able panic in the standard "user has the fs mounted/opens files while admin unmounts" scenario.

Fix (fix.diff, validated)

Guard fuse_statfs() and fuse_statvfs() entries:

    if (fmp == NULL || fuse_test_dead(fmp))
        return ENOTCONN;

Validation: fix (+ DF-3229/3227 fixes) built into the in-guest fuse.ko, module reloaded, identical PoC rerun β†’ no panic, guest up (the race loop completes; fstatfs now returns ENOTCONN). Baseline: 3/3 panics on stock.

Files

  • df3230.c β€” minimal reproducer
  • panic.txt / serial_full.txt β€” captured serial console (this boot)
  • fix.diff β€” validated guard
  • verdict.json / manifest.json

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

Baseline: 3/3 stock panics (identical signature). Patched module (fuse_vfsops.c guards): identical PoC rerun -> race loop completes with ENOTCONN returns, no panic, guest up.

fix.diff; validation session output: patched run 'mount fd opened; umount thread + fstatfs race loop' then clean completion, guest up (vs panic.txt baseline)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 + in-guest rebuilt fuse.ko (fix.diff composed with DF-3229/DF-3227 fixes)

Confirmed kernel references

Detail

Evidence (decisive lines)

['panic.txt / serial_full.txt: full captured panic on the serial console from the minimal df3230.c run (current-boot capture, not reconstructed)', "run.log: 'mount fd opened; umount thread + fstatfs race loop' followed by the panic on stock", 'fuse_vfsops.c:294 (mnt_data=NULL) + :393 (unguarded VFSTOFUSE->fuse_ipc_get) + fuse_ipc.c:100 (fetchadd on fmp->unique)']

PoC changes

Original discovery was incidental (two DF-3229 staging panics, signature fuse_ipc_get+0x63 fault 0x160). Minimal PoC written from scratch; the naive sequential form (umount then fstatfs) wedged in vflush, so the final form races a fstatfs loop against a umount thread, which catches the mnt_data==NULL window inside the umount syscall deterministically.

Verified recommended fix

Guard fuse_statfs() and fuse_statvfs() entries: if (fmp == NULL || fuse_test_dead(fmp)) return ENOTCONN; (fix.diff)

Verdict

Deterministic kernel NULL-deref panic, reproduced 3x on the stock INVARIANTS guest (2 incidental during DF-3229 staging + 1 with the minimal PoC), identical signature every time: fuse_unmount() clears mp->mnt_data (fuse_vfsops.c:294); an fstatfs(2) on a still-open fd reaches fuse_statfs() via kern_fstatfs's fp->f_nchandle.mount with no vnode ops, where the unguarded VFSTOFUSE(mp) NULL feeds fuse_ipc_get() (fuse_ipc.c:100) -> atomic_fetchadd at NULL+0x160 -> 'Fatal trap 12, supervisor write, page not present / Stopped at fuse_ipc_get+0x63'. The NULL window lives inside the umount syscall, so a plain fstatfs race loop catches it with no timing precision. Distinct from DF-3193 (freed-fmp walk on the wake path of an in-flight statfs); this is the entry path with a NULL fmp. Fix validated in-guest: NULL/dead guard on fuse_statfs+fuse_statvfs entries, module rebuilt, identical PoC rerun -> no panic, guest up.