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.
Recommended fix
--- 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)
PoC verification
Evidence pack
findings/poc/DF-2686 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:2241callsvm_object_pip_wakeup(object)β waking the terminal (backing) object's pip,- then
unlock_things()βcleanup_fault()(vm_fault.c:277) callsvm_object_pip_wakeup(fs->ba->object)on the same object β a second decrement:paging_in_progresswraps below zero (u_int β 0xFFFFFFFF), - while the pip added on
fs->first_ba->objectatvm_fault.c:1859is 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 -9ineffective), 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 reporterrun_baseline.shβ full setup + baseline runbaseline_stock.txtβ dmesg + ps proof from the stock kernelpatched_run.txtβ same sequence on the fixed kernelfix.diffβ the one-line fixenv.txt,verdict.json,manifest.json
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.
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-940only kills fd/cwd holders), write-fault page 0 (creates the COW shadow = fs->first_ba), print READY, sleep 20.- Operator kills nfsd/mountd/rpcbind during the window.
- 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. - 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:2241vm_object_pip_wakeup(object)βobjectis the terminal/backing object (vm_fault.c:2124), so the backing pip goes 1β0, andvm_fault.c:2242unlock_things(fs)βcleanup_fault()atvm_fault.c:277executesvm_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+ repeatedrefcount_wait objtrm1: long wait+ pid stuckD1E objtrm1forever. - patched (same guest,
make nativekernelrebuild 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. Seepatched_run.txt.
Fix verification
fixedBaseline 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
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.
No comments yet.