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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5814 Β· 3 files
FileTypeDescriptionSize
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_toq removes it from the object (vm_page_remove) and, seeing hold_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/vclean of vp (which is exactly the situation in which vget(LK_NOWAIT/LK_TIMELOCK) fails β€” the reclaim path holds the vnode lock) can run to completion: pages reaped, last object reference dropped, vm_object returned to the objcache.
  • The subsequent object->flags load at :1265 then reads freed memory. (The successful-vget revalidation path's object->flags at :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_testable
baseline no→ patch + rebuild →patched clean

Fix 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).
↓ fix.diffper-fix-DF-5814

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.