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

vm_fault_object() pager-error tail wakes the wrong object's PIP: double-decrement on the terminal object (u_int wrap) and permanent +1 leak on fs->first_ba->object β€” unkillable D-state hang in vm_object_terminate

Field Value
ID DF-2686
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-675 Duplicate Operations on a Resource, CWE-401
File sys/vm/vm_fault.c
Lines 2241 (wrong wakeup), 277 (cleanup's wakeup), 1859 (leak origin)
Area vm
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

When a fault resolves through a shadow (fs->ba != fs->first_ba) and the pager returns an error, vm_fault.c:2241 calls vm_object_pip_wakeup(object) on the terminal/backing object; unlock_things() β†’ cleanup_fault() (:277) then wakes the SAME object again, wrapping paging_in_progress below zero (u_int β†’ 0xFFFFFFFF), while the PIP added on fs->first_ba->object at :1859 is never released. All ten other wakeup sites in the function correctly target fs->first_ba->object β€” line 2241 is the lone outlier. The leaked/wrapped counts make vm_object_terminate()'s vm_object_pip_wait("objtrm1") sleep forever (_refcount_wait never gives up).

Threat model & preconditions

Any pager error on a backing object during a COW-shadowed fault: NFS read EIO / soft-mount RPC failure (verified live via loopback soft NFS + server kill; nfs_softterm reports EINTR == 4 == VM_PAGER_ERROR), forced unmount (VM_PAGER_BAD), media errors, beyond-EOF reads. Unprivileged trigger whenever the backing store is NFS. Demonstrated: vm_fault: pager read error, pid 907 + repeating warning: refcount_wait objtrm1: long wait + process wedged D1E objtrm1 permanently (kill -9 ineffective); the wrapped vnode-object count also wedges vnode recycling. No user→root route (permanent local DoS).

Proof of concept

VERIFIED (findings/poc/DF-2686/): loopback soft-NFS setup, victim mmaps MAP_PRIVATE 16MB (close(fd) keeps it off the umount kill list), write-faults page 0 (creates shadow), then reads a pager-backed page after nfsd/mountd/rpcbind are killed β†’ stock kernel: dmesg pager-error + repeating long-wait warnings + permanent D1E objtrm1 10+ minutes later. Fix validated on rebuilt kernel: same trigger β†’ SIGSEGV surfaces, victim exits fully, zero long-wait warnings.

--- a/sys/vm/vm_fault.c
+++ b/sys/vm/vm_fault.c
@@ -2238,7 +2239,7 @@
                vm_page_free(first_m);
                first_m = NULL;     /* safety */
            }
-           vm_object_pip_wakeup(object);
+           vm_object_pip_wakeup(fs->first_ba->object);
            unlock_things(fs);

(the terminal object's count is released by cleanup_fault(); validated in-guest: permanent hang β†’ clean exit)

Timeline

  • 2026-08-30 Discovered during pass-2 audit of vm_fault.c (GLM 5.3); reproduced with live NFS failure + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2686 Β· 12 files
FileTypeDescriptionSize
victim.c β€” 3.0 KB view raw
wrapper.c β€” 667 B view raw
run_baseline.sh β€” 1.6 KB view raw
baseline_stock.txt β€” 1001 B view raw
patched_run.txt β€” 1.3 KB view raw
run.log β€” 1001 B view raw
build.log β€” 2.4 KB view raw
env.txt β€” 443 B view raw
VERDICT.md β€” 4.0 KB ↓ raw
README.md β€” 3.3 KB ↓ raw
fix.diff β€” 335 B view raw
verdict.json β€” 5.4 KB view raw

DF-2686 β€” pip accounting corruption in vm_fault_object() pager-error tail

What

When a page fault resolves to a backing object (fs->ba != fs->first_ba, e.g. a COW shadow over a vnode/NFS object) and the pager returns an error (VM_PAGER_ERROR, VM_PAGER_BAD, or any non-OK/non-FAIL code), the error tail of vm_fault_object() releases the paging-in-progress counts wrongly:

  • sys/vm/vm_fault.c:2241 calls vm_object_pip_wakeup(object) β€” waking the terminal (backing) object's pip,
  • then unlock_things() β†’ cleanup_fault() (vm_fault.c:277) calls vm_object_pip_wakeup(fs->ba->object) on the same object β€” a second decrement: paging_in_progress wraps below zero (u_int β†’ 0xFFFFFFFF),
  • while the pip added on fs->first_ba->object at vm_fault.c:1859 is never released on this path β€” a permanent +1 leak.

Every other error/return path in the function correctly wakes fs->first_ba->object (see lines 1888, 1909, 1931, 2000, 2007, 2016, 2045, 2107, 2119, 2183) β€” line 2241 is the sole object-targeted wakeup.

Consequence

vm_object_terminate() waits for paging_in_progress == 0 (sys/vm/vm_object.c:762 / :795 β†’ _refcount_wait, which loops forever, re-printing warning: refcount_wait objtrm1: long wait every 60s). So:

  • the leaked +1 on the shadow object hangs the faulting process during exit() in permanent uninterruptible D-state (unkillable, kill -9 ineffective), and
  • the wrapped 0xFFFFFFFF count on the vnode object wedges its later termination (vnode recycling path).

Trigger

Any pager error on a backing object: NFS read EIO / soft-mount RPC failure (nfs_softterm() at sys/vfs/nfs/nfs_socket.c:1994 reports EINTR, and EINTR == 4 == VM_PAGER_ERROR in sys/vm/vm_pager.h:87), forced unmounts (vp->v_mount == NULL β†’ VM_PAGER_BAD, sys/vm/vnode_pager.c:476), media errors, beyond-EOF reads (vnode_pager.c:505-510). An unprivileged user mmap'ing a file on a soft NFS mount hits this whenever the server errors.

Reproduce (baseline, stock kernel)

./run_baseline.sh β€” sets up a loopback soft NFS mount in the guest, runs victim (shadow-creating write fault, then a pager-backed read), kills the NFS server mid-window. Observed (baseline_stock.txt):

nfs server localhost:/export: not responding
vnode_pager_getpage: I/O read error
vm_fault: pager read error, pid 907 (victim)          <- buggy tail executed
nfs send error 61 for server localhost:/export
warning: refcount_wait objtrm1: long wait             <- repeats every 60s
warning: refcount_wait objtrm1: long wait
warning: refcount_wait objtrm1: long wait
$ ps -axo pid,stat,wchan,comm
907 D1E    objtrm1  victim                            <- permanent hang, 10+ min

Fix

One line β€” wake the first object's pip (the terminal object's pip is released by cleanup_fault()), making line 2242 consistent with every other error path in the function. Validated by rebuild + rerun (patched_run.log).

Files

  • victim.c / wrapper.c β€” trigger + exit-fate reporter
  • run_baseline.sh β€” full setup + baseline run
  • baseline_stock.txt β€” dmesg + ps proof from the stock kernel
  • patched_run.txt β€” same sequence on the fixed kernel
  • fix.diff β€” the one-line fix
  • env.txt, verdict.json, manifest.json
VERDICT.md
↓ download raw

DF-2686 VERDICT β€” pip double-wakeup + leak in vm_fault_object() error tail

Status: REPRODUCED (stock) / FIXED (validated by kernel rebuild + rerun)

How it was reproduced

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), QEMU/KVM. Loopback soft NFS inside the guest (rpcbind+mountd+nfsd serving /export to localhost, mount -t nfs -o soft,udp), 16MB sparse file with real data at its last page.

  1. victim.c: open /mnt2/data.bin O_RDWR, mmap MAP_PRIVATE 16MB, close(fd) (closing the fd keeps the process off the forced-unmount kill list β€” sys/kern/vfs_syscalls.c:929-940 only kills fd/cwd holders), write-fault page 0 (creates the COW shadow = fs->first_ba), print READY, sleep 20.
  2. Operator kills nfsd/mountd/rpcbind during the window.
  3. Victim reads the pager-backed page at 16MB-4K: the fault descends shadow β†’ vnode object β†’ vm_pager_get_page() β†’ NFS RPC fails on the dead server β†’ nfs_softterm() sets error = EINTR (sys/vfs/nfs/nfs_socket.c:1994) and EINTR == 4 == VM_PAGER_ERROR (sys/vm/vm_pager.h:87) β†’ the buggy error tail runs.
  4. Victim exits β†’ shadow object teardown hangs.

Stock kernel observations (baseline_stock.txt, process pid 907):

nfs server localhost:/export: not responding
vnode_pager_getpage: I/O read error
vm_fault: pager read error, pid 907 (victim)      <- the buggy tail (vm_fault.c:2218)
nfs send error 61 for server localhost:/export
warning: refcount_wait objtrm1: long wait          <- every 60s, forever
warning: refcount_wait objtrm1: long wait
warning: refcount_wait objtrm1: long wait
$ ps -axo pid,stat,wchan,comm
907 D1E    objtrm1  victim                         <- still hung 10+ minutes later

The process is permanently wedged in uninterruptible D-state inside vm_object_terminate() β†’ vm_object_pip_wait("objtrm1") (sys/vm/vm_object.c:762); _refcount_wait (sys/kern/kern_refcount.c:59-77) never gives up. kill -9 cannot remove it. The vnode object's count separately wrapped to 0xFFFFFFFF (double decrement), wedging its later termination in the vnode-recycle path.

Why (line-level)

Fault state: pip was added on fs->first_ba->object (the shadow) at entry (sys/vm/vm_fault.c:1859) and on the backing vnode object at descent (vm_fault.c:2347). The error tail then does:

  • vm_fault.c:2241 vm_object_pip_wakeup(object) β€” object is the terminal/backing object (vm_fault.c:2124), so the backing pip goes 1β†’0, and
  • vm_fault.c:2242 unlock_things(fs) β†’ cleanup_fault() at vm_fault.c:277 executes vm_object_pip_wakeup(fs->ba->object) β€” the same object again β†’ 0β†’0xFFFFFFFF (u_int wrap),
  • the shadow's pip (+1 from line 1859) is never woken on this path.

Every other error/return path in vm_fault_object() wakes fs->first_ba->object (lines 1888, 1909, 1931, 2000, 2007, 2016, 2045, 2107, 2119, 2183 β€” line 2241 is the sole wakeup that targets object). The success protocol relies on cleanup_fault() releasing the terminal object's pip β€” the tail must release the FIRST object's.

Exploit chain

DoS only: one pager error on a backing object during a shadowed (COW) fault permanently wedges the faulting process (unkillable D-state) and corrupts the vnode object's pip (vnode-recycle wedge). No memory corruption or info leak. Unprivileged when the backing store is NFS: any user process mmap'ing a file on a soft NFS mount whose server hiccups (the classic EIO) hits it; no special setup for the attacker beyond normal mmap use.

Fix validation

fix.diff: line 2241 vm_object_pip_wakeup(object) β†’ vm_object_pip_wakeup(fs->first_ba->object).

  • baseline (stock): vm_fault: pager read error + repeated refcount_wait objtrm1: long wait + pid stuck D1E objtrm1 forever.
  • patched (same guest, make nativekernel rebuild with both DF-2685 and DF-2686 fixes, rebooted): same trigger sequence β€” fault still fails (the pager error is genuine), but the process exits cleanly; no objtrm1 wchan in ps; no long-wait warnings in dmesg. See patched_run.txt.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

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.

findings/poc/DF-2686/patched_run.txt; findings/poc/DF-2686/build.log
↓ fix.diffDragonFly 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

Confirmed kernel references

Detail

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 (decisive lines)

["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']

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).

Verified 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.

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.