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

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