vm_pageout_page(): object->flags use-after-free READ on the failed-vget error path β page hold parks the page on PQ_HOLD but pins neither the vm_object nor (on vget failure) the vnode
Summary
In the OBJT_VNODE laundering branch the daemon reads object=m->object while busied, then vm_page_hold+vm_page_wakeup and blocks in vget(). On vget FAILURE it takes no vnode reference yet immediately dereferences object->flags at :1265. A concurrent vgone/vclean β precisely the reclaim that makes vget fail β can complete vm_object_terminate (busy-waiting the now-unbusied page; the hold only parks the vm_page struct on PQ_HOLD) and free the vm_object; the subsequent load reads freed objcache memory. Impact bounded to a UAF READ of two bits (worst case one wrong vnodes_skipped increment) β no write, no disclosure; filed because the post-block-deref-of-unpinned-object class is refactor-fragile and the fix is trivial. Not guest-demonstrable (instruction-wide window vs full reclaim). Sample-OBJ_MIGHTBEDIRTY-while-busied fix git-apply-clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5814 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| fix.diff | β | 1.3 KB | view raw | |
| verdict.json | β | 2.9 KB | view raw |
DF-5814 β use-after-free READ of object->flags on the failed-vget path of vm_pageout_page()
Verdict: untested (analytical finding, Low/speculative). Not PoC-able
at acceptable cost: the window is a handful of instructions wide and
requires a vnode reclaim to complete inside it; impact is a read of
two bits from freed objcache memory (worst case: one wrong increment of
vnodes_skipped). Recorded for correctness of the lifetime proof, not
for exploitability.
Root cause (sys/vm/vm_pageout.c:1245-1268)
object = m->object; /* m busied: object alive */
vp = object->handle; /* OBJT_VNODE */
vm_page_hold(m); /* pin vm_page struct */
vm_page_wakeup(m); /* m UNBUSIED */
if (vget(vp, flags) != 0) { /* (a) may block, (b) on
failure takes NO vnode ref */
*vpfailedp = vp;
++pageout_lock_miss;
if (object->flags & OBJ_MIGHTBEDIRTY) /* <-- UAF READ */
++*vnodes_skippedp;
vm_page_unhold(m);
return 0;
}
While (m) is unbusied-but-held:
vm_page_hold()pins only the vm_page struct: object teardown (vm_object_terminate, sys/vm/vm_object.c:746+) busy-waits the page,vm_page_free_toqremoves it from the object (vm_page_remove) and, seeinghold_count != 0, parks it on PQ_HOLD instead of the allocator (sys/vm/vm_page.c:3199-3205). The object is not pinned by the page hold.- On vget failure the daemon holds no vnode reference, so a
concurrent
vgone/vcleanofvp(which is exactly the situation in whichvget(LK_NOWAIT/LK_TIMELOCK)fails β the reclaim path holds the vnode lock) can run to completion: pages reaped, last object reference dropped,vm_objectreturned to the objcache. - The subsequent
object->flagsload at :1265 then reads freed memory. (The successful-vget revalidation path'sobject->flagsat :1281 is safe: the taken vnode reference keeps the object allocated.)
The same stale-object pattern is present at :1281 but is protected by
the successful vget; :1265 is the unprotected instance. *vpfailedp = vp
storing the stale vnode is explicitly "allowed to be stale"
(vm_pageout.c:810) β only ever compared, never dereferenced β so it is
NOT part of this finding.
Why only a read / why Low
The value read influences a single counter increment
(++*vnodes_skippedp β speedup_syncer heuristics). No pointer from the
freed object is dereferenced, no write occurs through it. The realistic
worst case is a stale bit causing one spurious vnodes_skipped
increment. Reported because the class (post-block deref of an unpinned
object) is the kind that becomes a write in refactors, and the fix is
trivial.
Fix (fix.diff, applies clean)
Sample OBJ_MIGHTBEDIRTY into a local while the page is still
busied (object provably alive) and use the local in both post-vget
paths (:1265 and :1281).
Fix verification
not_testableFix is a two-line hoist of a flags sample before the unbusy/vget window; validated by construction (no kernel rebuild for a speculative Low read-only race).
findings/poc/DF-5814/fix.diff (git apply --check clean).
Confirmed kernel references
Detail
Evidence (decisive lines)
['sys/vm/vm_pageout.c:1245-1268 β hold/wakeup/vget window and the unprotected object->flags read at :1265', 'sys/vm/vm_object.c:746-866 β vm_object_terminate busy-waits pages, frees object after ref_count==0', 'sys/vm/vm_page.c:3199-3205 β free_toq parks held pages on PQ_HOLD (page struct pinned, object NOT)', 'findings/poc/DF-5814/README.md β full lifetime trace and why the successful-vget path at :1281 is safe', 'findings/poc/DF-5814/fix.diff β sample OBJ_MIGHTBEDIRTY before the unbusy window (applies clean)']
PoC changes
No runnable PoC authored: race window (instruction-level) x required interleaving (complete vnode reclaim) is not demonstrable on the single-tenant guest within budget; finding documented by source trace instead.
Verified recommended fix
Hoist the OBJ_MIGHTBEDIRTY test above vm_page_wakeup(m) while the page busy still pins the object (fix.diff).
Verdict
Analytical lifetime hole: on the failed-vget error path of vm_pageout_page()'s OBJT_VNODE laundering branch (sys/vm/vm_pageout.c:1262-1266), the daemon holds only a page hold (which parks the vm_page on PQ_HOLD at teardown, sys/vm/vm_page.c:3199-3205) and β because the vget FAILED β no vnode reference. A concurrent vgone/vclean of that vnode (precisely the lock-holder that makes vget's LK_NOWAIT/LK_TIMELOCK fail) can complete object termination and free the vm_object before the 'object->flags & OBJ_MIGHTBEDIRTY' load at :1265, making it a use-after-free READ of freed objcache memory. Impact is bounded to a possibly-wrong vnodes_skipped counter increment β no write primitive, no data returned to userspace β hence Low/speculative. Not run on the guest: the window is a few instructions and would require orchestrating a vnode reclaim inside it; per contract this Low-severity race is outside the mandatory verification set and a negative run would prove nothing.
No comments yet.