# DF-2676 — vm_page_grab() NULL-pointer dereference (error path, no VM_ALLOC_RETRY)

## What

`vm_page_grab()` (sys/vm/vm_page.c:3827-3894): when `vm_page_lookup_busy_try()`
reports the page exists but is busy (`error = TRUE`) and the caller did **not**
pass `VM_ALLOC_RETRY`, the code does:

```c
if (error) {
        vm_page_sleep_busy(m, TRUE, "pgrbwt");
        if ((flags & VM_ALLOC_RETRY) == 0) {
                m = NULL;
                break;              /* <-- falls into the block below */
        }
        /* retry */
}
...
if (m->valid == 0) {               /* vm_page.c:3882 — m == NULL here */
```

`break` exits the loop into the `m->valid` read with `m == NULL` → kernel
page fault at `offsetof(struct vm_page, valid)` = 0x76 on x86_64.
The doc comment explicitly blesses non-RETRY usage (“if VM_ALLOC_RETRY is
not set then NULL is always returned if we had blocked”), so returning NULL
is the intended behavior — the missing `goto failed` is the bug.

## Contents

| file | what |
|---|---|
| `df2676.c` | deterministic KLD trigger (two kernel threads) |
| `shm_grab_race.c`, `shm_grab_race2.c` | syscall-level attempts via the only non-RETRY in-tree caller (sysv_shm prealloc) — see VERDICT.md for why they cannot reach the path today |
| `fix.diff` | one-line fix (`goto failed`) |
| `panic.txt` | captured Fatal trap 12, fault VA 0x76, `movzbl 0x76,%eax` in vm_page_grab |
| `verdict.json`, `manifest.json` | machine verdict |

## Build & run the KLD trigger (root on the QEMU guest)

```sh
mkdir -p /root/df2676_mod && cd /root/df2676_mod
cp df2676.c .
printf 'KMOD= df2676\nSRCS= df2676.c\nSYSDIR= /usr/src/sys\n.include "${SYSDIR}/conf/kmod.mk"\n' > Makefile
make
kldload ./df2676.ko
```

## Expected (vulnerable kernel)

~250 ms after `kldload`, the serial console shows the grabber/holder markers
and then:

```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x76
Stopped at vm_page_grab.cold.30: movzbl 0x76,%eax
```

Guest dies at the ddb prompt (DoS: kernel panic).

## Expected (patched kernel, fix.diff applied)

```
DF-2676: grabber: grab returned (nil) (no panic?!)
```

`vm_page_grab()` returns NULL cleanly; guest stays up.

## Threat model / reachability (read this)

The only in-tree caller that omits `VM_ALLOC_RETRY` is the SysV-shm
pre-allocation loop in `shmget_allocate_segment()` (sys/kern/sysv_shm.c:582,
flags `VM_ALLOC_SYSTEM | VM_ALLOC_NULL_OK | VM_ALLOC_ZERO`).  Detailed
reachability analysis (VERDICT.md) shows that caller cannot observe a busy
page today — it holds the object token exclusively for the whole loop, the
loop never blocks mid-iteration, and the phys pager marks its pages
`PG_UNQUEUED`, which excludes the only token-free busier (the
`vm_page_hash_get()` soft-busy quick-fault path).  The bug is therefore
**latent for unprivileged attackers on a stock kernel** but is a live
NULL-deref primitive for any kernel code (in-tree evolution or third-party
KLD) that calls `vm_page_grab()` without `VM_ALLOC_RETRY` on a contended
page — exactly what the KLD demonstrates.  Root-triggerable today via kldload.
