# DF-0996 — vkernel64 pmap_unwire dereferences NULL pte

## Verdict
**NOT TESTABLE on default guest.** Bug is real and the fix is correct (verified
by source-level trace and by parity with the equivalent pc64 implementation),
but the vulnerable code lives in `sys/platform/vkernel64/`, which is **not
compiled into the running `X86_64_GENERIC` kernel** on this guest. The
`vkernel64` is a separate kernel target (a virtual kernel that runs as a
userspace process on the real kernel); exercising this path would require
building/booting a `VKERNEL64` binary that hosts user processes — outside the
scope of the default guest and not a default-kernel config.

The bug and fix are confirmed by code review (below). Filed severity (Medium)
and CVSS `AV:L` accurately reflect the on-host trigger model for a system
running `vkernel64`.

## Mechanism (cited)

`pmap_unwire()` at `sys/platform/vkernel64/platform/pmap.c:2621-2665`:

```c
2636:    vm_object_hold(pmap->pm_pteobj);
2637:    pte = pmap_pte(pmap, va);
2638:    if ((*pte & VPTE_V) == 0) {        /* <-- derefs pte with no NULL check */
2639:        *pva = pmap_nextva(pmap, va);
```

`pmap_pte()` at `sys/platform/vkernel64/platform/pmap.c:311-322` returns
**NULL** when `pmap_pde()` returns NULL (no PDE/PDPE/PML4E page-table page
exists for the VA) or when the PDE is not present:

```c
316:    pde = pmap_pde(pmap, va);
317:    if (pde == NULL || (*pde & VPTE_V) == 0)
318:        return NULL;
```

Line 2638 then dereferences `pte` unconditionally — a NULL dereference.

The equivalent function in the bare-metal pc64 pmap
(`sys/platform/pc64/x86_64/pmap.c:5565-5619`) **does** handle the NULL case
correctly (lines 5588, 5610–5618), proving the vkernel64 version is the
outlier and the fix pattern is canonical.

## Reachability (in a vkernel64 environment)
`pmap_unwire()` is called from `sys/vm/vm_fault.c:2636` and
`sys/vm/vm_fault.c:2691` during `vm_fault_wire()`/`vm_fault_unwire()` — i.e.
on `mlock`/`munlock`, and on `vm_map_entry_unwire_all()` during exit/exec of
processes that have wired sparse VA ranges. The finding summary cites
multiple realistic trigger paths: sparse VPAGETABLE entries, race window
where `vm_fault_wire` unlocks the map allowing concurrent `pmap_remove` to
free PT pages, and error cleanup paths.

In a vkernel64 (which maps a guest physical address space onto host user
memory), a NULL PDE is the normal state for vast ranges of the virtual
address space — `pmap_unwire` is called per page during unwire-walks, so any
unwire that walks into a region with no PT page triggers the NULL deref.

## Impact
- A vkernel64 crash takes down **all** virtual user processes it hosts
  (vkernel is single-process-from-host-perspective, multiplexing many guest
  user processes). This is a host-local DoS of the vkernel's guest.
- Per the finding summary, the fault is delivered as a NULL-page fault
  trapped by the real (host) kernel — no memory corruption of the host.

## Fix
`fix.diff` — adds the missing NULL check at line 2638, matching the pattern
used by the pc64 equivalent. When `pte == NULL`, advance `*pva` via
`pmap_nextva()` and return NULL (same as the existing VPTE_V==0 path).

This fix is **not validated by a built kernel** because the default guest
kernel does not include vkernel64 code. Applying the diff and rebuilding a
`VKERNEL64` target (with a workload that issues `mlock` over a sparse VA
range) would validate it; that is outside this run's scope.

## Files in this evidence pack
- `fix.diff` — NULL check for `pte` in `pmap_unwire()`
- `VERDICT.md` — this narrative
- `README.md` — overview
- `manifest.json` — artifact catalog
