DF-3207 / verdict.json
{ "finding_id": "DF-3207", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Deterministic kernel panic reproduced as unprivileged user test: nullfs_unmount (null_vfsops.c:261-267) frees mnt_data during umount -f (dounmount proceeds after its retry loop exhausts, vfs_syscalls.c:964-1006), while kern_statfs/kern_fstatfs/sys_quotactl (vfs_syscalls.c:1428/1495/1244) call into the filesystem with no vfs_busy/MNTK_UNMOUNT interlock. A struct file parked in a socket receive buffer via SCM_RIGHTS keeps an invisible mount ref through the force-unmount kill-scan (process_uses_mount scans only fd tables/cwd, :727-757); recovering the fd afterwards and calling fstatfs(2) executes MOUNTTONULLMOUNT(mp)->nullm_vfs (nullfs_statfs, :308) with mnt_data==NULL -> read at VA 0 -> Fatal trap 12 ... Stopped at nullfs_statfs+0x32. UAF-read variant exists in the race window but M_NULLFSMNT is a nullfs-private malloc type with non-attacker-controlled content, so the honest ceiling is kernel DoS/panic, not uid0 (with vfs.usermount=1 the entire chain including umount -f runs unprivileged). Fix (vfs_busy/vfs_unbusy interlocks in the three syscalls) validated by in-guest nativekernel rebuild: baseline panics, patched returns EBUSY with guest up.", "exploit_chain": "1) root: mount_null /tmp/t /mnt/a; 2) unpriv: open /mnt/a/f1, sendmsg(SCM_RIGHTS) over socketpair, close fd (fp parked in sockbuf holds mount ref invisible to umount -f kill-scan); 3) root: umount -f /mnt/a -> 50x250ms retry loop exhausts (1 mount refs still present/forcing unmount), VFS_UNMOUNT runs -> mp->mnt_data=0, kfree(null_mount), mount struct leaked but alive; 4) unpriv: recvmsg() recovers fd, fstatfs(fd) -> kern_fstatfs takes dead mp from fp->f_nchandle.mount -> nullfs_statfs loads MOUNTTONULLMOUNT(mp)->nullm_vfs from VA 0 -> fatal trap 12, guest in DDB, ssh dead. No escalation: no write primitive, freed chunk content not attacker-controllable.", "evidence": [ "panic.txt lines 40-59: Fatal trap 12: page fault while in kernel mode, fault virtual address = 0x0, current process = 6405, Stopped at nullfs_statfs+0x32: movq (%rax),%rdi", "panic.txt lines 36-38: unmount(/mnt/a): ... 1 mount refs still present -> forcing unmount (retry loop exhausted, VFS_UNMOUNT with refs>1)", "fix_run.log / fix_run2.log: same PoC on patched kernel #1 -> fstatfs returned errno=16 (no panic), guest up, 4 rounds", "ghost.c: SCM_RIGHTS park + recvmsg + fstatfs trigger", "fix.diff: vfs_busy(LK_NOWAIT)/vfs_unbusy interlocks in kern_statfs/kern_fstatfs/sys_quotactl (git apply --check clean)" ], "kernel_refs": [ "sys/vfs/nullfs/null_vfsops.c:261-267", "sys/vfs/nullfs/null_vfsops.c:294", "sys/vfs/nullfs/null_vfsops.c:308", "sys/vfs/nullfs/null_vfsops.c:341", "sys/kern/vfs_syscalls.c:888-1006", "sys/kern/vfs_syscalls.c:727-757", "sys/kern/vfs_syscalls.c:1244-1247", "sys/kern/vfs_syscalls.c:1428-1436", "sys/kern/vfs_syscalls.c:1495-1512", "sys/kern/vfs_syscalls.c:1401", "sys/kern/vfs_mount.c:271-305" ], "poc_changes": "Seed concept only; wrote trigger from scratch. First deterministic attempt had a userland bug (recvmsg on the sending socketpair end sv[0] instead of sv[1] -> blocked in sbwait); fixed and re-run -> immediate panic. Brute-force race variant (spam.c, 2400 mount/umount -f rounds) never hit because dounmounts retry loop self-serializes against in-flight callers holding mount refs; the exhausted-force path with an SCM_RIGHTS-invisible mount ref makes it deterministic.", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 640, "guest_dirty": 0, "build_cmd": "cc -O2 -o /root/ghost /root/ghost.c", "run_cmd": "/root/det_driver.sh 1 (mount_null /tmp/t /mnt/a; unpriv ghost parks fd via SCM_RIGHTS; umount -f /mnt/a; touch fire; unpriv recvmsg+fstatfs)", "code_hash": "047adb0ebbe33d904abc3f486b14d97a311d3c88e032e1a245bc8da5ae4c4a9a", "notes": "Trigger requires one privileged step (umount -f) on default configs; with vfs.usermount=1 (guest default 0) the unprivileged mount owner can run the whole chain. Impact ceiling: kernel panic/DoS (NULL-deref read, deterministic) plus race-window UAF read of an 88-byte nullfs-private chunk whose realloc content is not attacker-controlled - no uid0 route found. Pass-2 negatives for this file (killed with citations) are in the audit JSON negative_notes: DF-3164 remount twin (update path vfs_busy at vfs_syscalls.c:265 serializes), stacking loop (nullm_vfs forced to rootvp->v_mount, null_vfsops.c:145-151; findmount keys on mnt_ncmounton, vfs_cache.c:4099), uninit size/OOB bzero (amd64 std_copyinstr always writes *lencopied in [0,maxlen], support.s:705-715), DF-3191 twin (fresh null_mount per mount), lower-forced-unmount teardown (rootvp vref + vget VRECLAIMED->ENOENT, vfs_lock.c:598-605), statfs echo bounds (bzero terminator null_vfsops.c:219), forwarding recursion (nullm_vfs never a nullfs mount).", "recommended_fix": "Add vfs_busy(mp, LK_NOWAIT)/vfs_unbusy(mp) interlocks around VFS_STATFS/VFS_QUOTACTL in kern_statfs, kern_fstatfs and sys_quotactl (mirroring mountlist_scan), so umount -f cannot free mnt_data under a caller; optionally guard nullfs ops with mnt_data==NULL -> EINVAL as defense in depth." } |