DF-2842 / verdict.json
{ "finding_id": "DF-2842", "status": "not_reproduced", "reproduced": 0, "impact": "none", "confidence": "likely", "verdict": "vnode_pager_reference() (sys/vm/vnode_pager.c:203-210) is the only vp->v_object entry point with no interlock: it loads vp->v_object unlocked and then does vm_object_reference_quick()'s unlocked atomic_add on ref_count (vm_object.c:527-536). A concurrent forced reclaim (umount -f / revoke -> vclean_vxlocked vfs_subr.c:1285 -> vm_object_terminate vm_object.c:746 -> vnode_pager_dealloc vnode_pager.c:213 -> vm_object_drop -> kfree_obj vm_object.c:369) that reaches kfree while the mmap thread sits between the two statements corrupts the ref_count of freed (typically recycled) vm_object memory. The code defect is certain from source; on the stock guest the kernel-side UAF did not land in bounded stress (10+ cycler cycles, ~1e6 mmap iterations, ~2560 object terminations raced): the window is a few instructions wide, the only reclaimers that touch referenced vnodes are root-gated on default config (vfs.usermount=0 verified), and dounmount()'s kill loop (vfs_syscalls.c:925-950) SIGKILLs fd-holders before the termination sweep, fighting the staging. The userspace leg of the same missing interlock DID reproduce reliably: racer (uid 1002) mmap'd vnodes mid-vclean 40/40 times, installed the dying object, and SIGSEGV'd on first touch (dmesg: 'pid (racer), uid 1002: exited on signal 11'). Fix mirrors vnode_pager_alloc()'s protocol: v_token + vm_object_hold + OBJ_DEAD/v_object recheck before the quick reference.", "exploit_chain": "not developed (primitive not reproduced): would be unpriv mmap loop racing owner-run force-unmount of a usermount (vfs.usermount=1) or root-cooperated umount -f with a thread blocked in VOP_GETATTR on a slow FS to survive the kill-before-sweep; hit yields ref_count++ on a recycled vm_object -> premature termination of an unrelated object -> UAF cascade -> potential uid0", "evidence": [ "VERDICT.md: full analysis, run results, staging blockers", "dmesg.txt: 40x 'racer uid 1002 exited on signal 11' (mmap-of-dying-object leg)", "run.log / run.2.log: cycler output, cycles and forced-umount warnings", "racer.c / cycler.sh: the harness", "sys/vm/vnode_pager.c:203-210 vs :127-137 (alloc interlock) and sys/kern/vfs_subr.c:1375-1393 (clean interlock)" ], "kernel_refs": [ "sys/vm/vnode_pager.c:203", "sys/vm/vnode_pager.c:207", "sys/vm/vnode_pager.c:213", "sys/vm/vm_mmap.c:1410", "sys/vm/vm_object.c:527", "sys/vm/vm_object.c:367", "sys/kern/vfs_subr.c:1375", "sys/kern/vfs_syscalls.c:931" ], "poc_changes": "seed harness rewritten: added multi-file fd round-robin (256 tmpfs files), fork children, touch-after-mmap to force pager activity; cycler restarts racer each umount cycle; guest-side su -m/csh quoting issue fixed by running racer via /tmp/runracer.sh with plain sh", "attempts": 3, "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": 570, "guest_dirty": 0, "build_cmd": "cc -O2 -o /tmp/racer racer.c", "run_cmd": "sh cycler.sh test 150 (root; racer runs as uid 1002)", "code_hash": "fd86acd069c522dc24f8bca633cbfb944c806bd475d795e7a7b488e1a749e66a", "notes": "impact 'none' = no kernel corruption demonstrated; userspace SIGSEGV manifestation is 40/40 reliable. vfs.usermount=0 on guest (default) keeps the unpriv path gated. Kill-before-sweep ordering (unmount_allproc_cb SIGKILL at retry 7 vs sweep at retry 16) is the practical staging blocker for fd-holding racers.", "recommended_fix": "take vp->v_token and vm_object_hold() with OBJ_DEAD/v_object recheck before vm_object_reference_quick() in vnode_pager_reference(), mirroring vnode_pager_alloc()", "fix_status": "not_testable", "fix_kernel_uname": "", "fix_baseline_reproduced": 0, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff authored against sys/vm/vnode_pager.c after verification (hold+recheck protocol); not kernel-built/validated because the corruption primitive itself did not reproduce on the stock guest within the bounded run - a patched build cannot be distinguished from baseline by this harness", "fix_evidence": ["fix.diff"] } |