DragonFlyBSD Kernel Audit
DF-2686 / verdict.json
← back to finding ↓ download raw
{
  "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"
}