DF-2686 / verdict.json
{ "finding_id": "DF-2686", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "REPRODUCED on stock DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC): when a page fault resolves to a BACKING object (fs->ba != fs->first_ba, e.g. a COW shadow over a vnode object) and the pager returns an error, the error tail of vm_fault_object() calls vm_object_pip_wakeup(object) on the TERMINAL object (vm_fault.c:2241) and unlock_things()->cleanup_fault() then wakes the SAME object again (vm_fault.c:277) - paging_in_progress wraps below zero - while the pip added on fs->first_ba->object (vm_fault.c:1859) is never released (+1 leak). Demonstrated with a loopback soft-NFS mount and mid-flight server kill (nfs_softterm reports EINTR == 4 == VM_PAGER_ERROR, sys/vm/vm_pager.h:87): dmesg shows 'vm_fault: pager read error, pid 907' followed by 'warning: refcount_wait objtrm1: long wait' repeating every 60s, and ps shows the process permanently wedged '907 D1E objtrm1' (uninterruptible, unkillable) 10+ minutes later - vm_object_terminate()'s pip_wait (vm_object.c:762) never returns because _refcount_wait (kern_refcount.c:59-77) never gives up. The vnode object's wrapped 0xFFFFFFFF count additionally wedges its later termination in the vnode-recycle path. FIX VALIDATED: one-line change to wake fs->first_ba->object (consistent with all 10 other wakeup sites in the function); on the rebuilt kernel (#1 Aug 30 19:03:41) the same trigger sequence still surfaces the genuine pager error (SIGSEGV to the victim) but the process exits completely - no objtrm1 wchan, zero long-wait warnings - across multiple pager-error cycles.", "exploit_chain": "Unprivileged user mmaps a file on a soft NFS mount MAP_PRIVATE, forces a COW shadow (write fault), then reads a not-yet-resident page while the NFS server errors (classic EIO/soft-timeout): vm_fault_object's pager-error tail double-decrements the vnode object's pip and leaks +1 on the shadow; on process exit the shadow's vm_object_terminate sleeps forever in objtrm1 -> permanent unkillable D-state process per fault; the wrapped vnode-object count wedges vnode recycling. Local DoS; no memory corruption or info leak.", "evidence": [ "findings/poc/DF-2686/baseline_stock.txt - stock dmesg chain ('not responding' -> 'vnode_pager_getpage: I/O read error' -> 'vm_fault: pager read error, pid 907' -> triple 'warning: refcount_wait objtrm1: long wait') plus ps line '907 D1E objtrm1 victim' still hung 10+ min later", "findings/poc/DF-2686/patched_run.txt - patched kernel: same pager-error surface, 'CHILD killed by signal 11', 'NO HANG: no victim processes remain', long-wait count 0", "findings/poc/DF-2686/build.log - validated kernel identity and build/install record", "findings/poc/DF-2686/VERDICT.md - full line-level pip accounting trace" ], "kernel_refs": [ "sys/vm/vm_fault.c:2241", "sys/vm/vm_fault.c:277", "sys/vm/vm_fault.c:1859", "sys/vm/vm_fault.c:2347", "sys/vm/vm_object.c:762", "sys/vm/vm_object.c:295", "sys/kern/kern_refcount.c:59", "sys/vm/vm_pager.h:87", "sys/vfs/nfs/nfs_socket.c:1994", "sys/vm/vnode_pager.c:476", "sys/vm/vnode_pager.c:510" ], "poc_changes": "Self-written for this verification (no seed). Loopback NFS used because umount -f SIGKILLs fd holders (vfs_syscalls.c:929-940) - the victim closes its fd after mmap to stay off the kill list; signals blocked during the fault to survive the server-death window (victim3 variant unblocked for the patched run to demonstrate the clean SIGSEGV exit).", "attempts": 6, "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": 2100, "guest_dirty": 0, "build_cmd": "cc -O2 -o /tmp/mt/victim victim.c -lutil; cc -O2 -o /tmp/mt/wrapper wrapper.c (fix kernel: cd /usr/src && patch -p1 < fix.diff && make -j4 nativekernel && make installkernel && reboot)", "run_cmd": "./run_baseline.sh (rpcbind+mountd+nfsd serving /export; mount -t nfs -o soft,udp localhost:/export /mnt2; run victim; pkill nfsd/mountd/rpcbind at READY; sleep 95; ps + dmesg)", "code_hash": "d5ab0bd95bddcfaffddc4577e76f27564769542b383208ce7b9f96a0b5340c43", "notes": "EINTR==4 collision with VM_PAGER_ERROR makes soft-NFS RPC failures the natural unprivileged trigger; other routes: forced unmount (VM_PAGER_BAD via vp->v_mount==NULL), media errors, beyond-EOF reads. The double-wakeup on a count of exactly 1 also fires a spurious wakeup before wrapping; net effect is a permanent -1 on the terminal object and +1 leak on the first object. Stock line numbers cited; +1 shift after the DF-2685 hunk in the combined patch.", "recommended_fix": "vm_fault.c:2241: wake fs->first_ba->object instead of the local 'object' (the terminal object's pip is released by cleanup_fault()) - see fix.diff.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 19:03:41 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Baseline reproduced on stock #0 (permanent D1E objtrm1 hang + repeating long-wait warnings). Patched kernel #1: identical trigger, pager error still genuine, process exits fully (killed by signal 11, no remnant), zero long-wait warnings across multiple error cycles. Hang eliminated.", "fix_evidence": "findings/poc/DF-2686/patched_run.txt; findings/poc/DF-2686/build.log" } |