# DF-2676 VERDICT — vm_page_grab() NULL dereference (no-RETRY error path)

**Status: reproduced** (deterministic kernel-module trigger; unprivileged
reachability currently latent — see below).  **Impact: kernel panic (DoS)**,
fixed `NULL + 0x76` read, no control over the faulting pointer, no write
primitive, no escalation path.

## The bug (source-level, certain)

`sys/vm/vm_page.c`:

- :3838 `m = vm_page_lookup_busy_try(object, pindex, TRUE, &error);`
  `also_m_busy = TRUE` means `error = TRUE` when the page exists and has
  `PBUSY_LOCKED` **or any soft-busy count** (`PBUSY_MASK != 0`).
- :3840-3844 on error without `VM_ALLOC_RETRY`: `m = NULL; break;`
- :3882 `if (m->valid == 0)` — unconditional dereference; the alloc-failure
  path correctly does `goto failed` (:3859) but the busy-error path forgot.

## Reproduction (guest, 2026-08-30)

KLD `df2676.c`: kernel thread grabs a busied page's object; grabber thread
calls `vm_page_grab(obj, 0, VM_ALLOC_NORMAL)` (no RETRY) → error path →
`vm_page_sleep_busy()` sleeps once → holder thread `vm_page_wakeup()`s the
page → grabber wakes, sets `m = NULL`, breaks, executes `movzbl 0x76,%eax`
(`m->valid`):

```
DF-2676: grabber: calling vm_page_grab(obj, 0, VM_ALLOC_NORMAL) - NO VM_ALLOC_RETRY - on busy page
DF-2676: holder: waking busy page 0xfffff80049701200
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x76          == offsetof(struct vm_page, valid)
Stopped at vm_page_grab.cold.30: movzbl 0x76,%eax
```

Full capture in `panic.txt`.  Attempts: 1 failed module iteration (token
accounting artifact of doing vm_object_hold in MOD_LOAD — fixed by moving
the work to a kernel thread), then clean reproduction.

## Why the syscall-level attempts could NOT reach it (important negative result)

Two userland racers were built (`shm_grab_race.c`, `shm_grab_race2.c`;
thousands of shmget-prealloc races against 6-16 shmat+fault processes,
kern.ipc.shm_use_phys=2).  No panic via vm_page_grab — and the source
analysis explains why, exhaustively:

1. `shmget_allocate_segment()` prealloc loop (sysv_shm.c:576) holds
   `vm_object_hold()` — **exclusive** — for the entire loop, and no
   instruction inside the loop can block (vm_page_grab→lookup/alloc never
   tsleep unless memory-starved; phys_pager_getpage just zeroes; activate/
   wakeup/yield do not drop LWKT tokens).
2. Every userland busier of a shm-object page needs the object token:
   the vm_fault slow path holds the object (shared) for the whole fault,
   so a fault either completes before the exclusive hold is granted or
   blocks until the loop is over — it can never overlap a `grab()`.
3. The one token-free busier — `vm_page_hash_get()` in the vm_fault quick
   path (soft-busy, no object token; vm_page.c:1601) — only serves pages
   that are fully valid, `PQ_ACTIVE`, `PG_MAPPEDMULTI` and already
   hash-entered.  Pages the prealloc loop produces are marked
   `PG_UNQUEUED` by `phys_pager_getpage()` (sys/vm/phys_pager.c:96) and are
   never mapped before the loop ends, so the quick path can never pick them
   up; and pages faulted by other processes before the exclusive hold
   begins can only exist for the microseconds between segment publication
   (sysv_shm.c:553) and `vm_object_hold()` (sysv_shm.c:576) — far too short
   for an shmat+fault to land, let alone stay in-flight.

Conclusion: with today's in-tree caller set the error path is unreachable
from unprivileged userland; it is a **latent API-contract NULL dereference**
(any future/3rd-party kernel caller without VM_ALLOC_RETRY trips it
instantly — demonstrated by the KLD).  Class paralleling the known
DF-0942 latent-deref finding, but with a real (if protected) in-tree caller
and a documented non-RETRY contract.

## Fix validation

`fix.diff` (one line: `break` → `goto failed`):

- baseline (stock INVARIANTS kernel): KLD trigger → Fatal trap 12 @ 0x76
  (see panic.txt).
- patched kernel (both DF-2676 and DF-2677 fixes applied, rebuilt with
  `make nativekernel KERNCONF=X86_64_GENERIC`): KLD trigger reloaded →
  `DF-2676: grabber: grab returned (nil) (no panic?!)`, guest stays up.
  See run_fixed.log.

## Severity rationale

Low: kernel NULL read → panic; requires kernel-thread/caller context to
reach today (root via kldload, or future kernel code).  Fixed-address read
at NULL — no exploitation beyond DoS even when reached.
