# DF-5467 VERDICT — vga_pci_unmap_bios() shadow-path teardown of a ROM-BAR mapping

**STATUS: reproduced (kernel panic on the unpatched guest) / impact:
kernel-memory-corruption primitive (PTE teardown + kernel_map VA free over a
128 KiB range owning a 64 KiB mapping, plus a leaked live resource). FIX
VALIDATED (fixed kernel: identical PoC sequence completes cleanly, guest
stays up).**

## Root cause (line-accurate)

1. `vga_pci_map_bios()` chooses the *mapping kind* by evaluating
   `vga_pci_is_boot_display(dev)` at entry — `sys/bus/pci/vga_pci.c:175`.
   TRUE ⇒ return a fresh `pmap_mapbios(0xC0000, 128K)` shadow mapping
   (:185-186). FALSE ⇒ ROM-BAR resource path (:190-274) returning
   `rman_get_virtual(res)` of size `rman_get_size(res)` (here 64 KiB).

2. The predicate is *mutable* and *latching*:
   - on the ROM path `vga_pci_map_bios()` itself sets
     `PCIB_BCR_VGA_ENABLE` on the parent PCI-PCI bridge (:197-201) — one of
     the exact inputs `vga_pci_is_boot_display()` consults (:120-124);
   - `vga_pci_default_unit` is sticky: the first evaluation that passes all
     checks latches it (:94-101, :139).

3. `vga_pci_unmap_bios()` **re-evaluates** the predicate (:288) instead of
   remembering how the BIOS was mapped. When it returns TRUE for a mapping
   that was created via the ROM path, unmap executes the *shadow* teardown

       pmap_unmapdev((vm_offset_t)bios, VGA_PCI_BIOS_SHADOW_SIZE)  /* 128 KiB */   (:290)

   on the ROM-BAR kva. `pmap_unmapdev()` clears PTEs via `pmap_qremove()`
   (`pmap.c:6230`, `atomic_readandclear_long` per page — see
   `pmap.c:2177-2192`) **and frees kernel_map VA via `kmem_free(kernel_map,
   base, size)`** (`pmap.c:6231`) for the full rounded 128 KiB — over a
   mapping that owns only 64 KiB. The 16 foreign pages of neighboring kernel
   mappings lose their PTEs, and their vm_map entries are destroyed by the
   over-range free (kernel_map VA-space corruption: the freed range is
   re-handable to future allocations while the true owner still holds it).
   The KASSERTs that would have caught the mismatch (:314-316) sit *after*
   the early `return` (:292) and never execute on this path.

4. Secondary effect: the shadow path returns without touching
   `sc->vga_bios` — the ROM resource stays cached (`vr_res != NULL`,
   `vr_refs == 1`), so the next `vga_pci_map_bios()`/`vga_pci_alloc_resource()`
   hands back the stale kva whose PTEs are gone → kernel page fault inside
   the kernel's own poll loop (:228).

## Reachability / threat model

Attacker = firmware/hardware state (or a malicious PCIe device presenting
GPU-class + bridge topology), same trust boundary as the DF-0990/DF-2324
family; no userland privilege is involved:

- Secondary GPU behind a PCI-PCI bridge whose `BCR.VGA_ENABLE` is clear
  (typical for non-boot GPUs on UEFI systems without CSM), with the GPU's
  own MEM/IO decode enabled, on a system where no display device passed the
  boot-display checks during probe (`vga_pci_default_unit == -1`: headless
  boxes, or firmware that left all VGA decoding unselected).
- drm attach then runs `radeon_read_bios()` / amdgpu's analogue
  (`sys/dev/drm/radeon/radeon_bios.c:86-104`): `vga_pci_map_bios()` sees
  predicate=0 (bridge bit clear at :121), takes the ROM path and *sets the
  bridge bit* (:200); the immediately following `vga_pci_unmap_bios()` sees
  predicate=1 (first passing evaluation latches :139) and runs the 128 KiB
  teardown on the 64 KiB ROM mapping.
- The same mismatch arises from any concurrent latch of
  `vga_pci_default_unit` between map and unmap (dual-GPU attach interleave).
- Note DF-5468: on devices whose ROM BAR readback *preserves* the enable
  bit, DF's lazy `pci_alloc_map()` fails the ROM allocation first, masking
  this chain; it is live when the readback masks bit0 or the ROM BAR is
  pre-reserved (hints / FreeBSD-style enumeration). QEMU's std VGA is in the
  first category — the PoC therefore seeds the resource-list entry exactly
  as FreeBSD's enumeration does, and the CONTROL phases prove the seeded
  path is balanced whenever the predicate does not flip.

## Exploitation ceiling (primitive characterization)

The demonstrated fault is the *simplest* consequence (the ROM mapping's own
PTEs). The primitive is stronger: `kmem_free(kernel_map, base, 128K)` frees
a 64 KiB span the kernel still believes allocated (VA double-assignment →
two owners for one kernel VA range: a write through the surviving owner and
a fresh allocation alias), and `pmap_qremove` strips PTEs from up to 64 KiB
of arbitrary neighboring kernel mappings chosen by allocator adjacency —
device MMIO windows, kernel heap, KLD text. A weaponized version (choose
neighbor, free VA, re-allocate over a live victim object) plausibly reaches
uid=0; this requires controlling kernel_map adjacency, which we did not
weaponize in this run (trigger is firmware-state-gated, not unpriv-user-
gated). Panic + leak + VA-space corruption are proven; privesc is
characterized but not demonstrated.

## Reproduction record (this pack)

- `serial-full.log` — full serial console of the baseline run.
- `panic.txt` — decisive excerpt:

      DF5467: BUG map     -> bios=0xfffff8011887c000 size=0x10000 (ROM-BAR path)
      DF5467: flip: is_boot_display=1 default_unit=0
      DF5467: BUG unmap returned — shadow-path pmap_unmapdev(ROM kva, 128K) ran
      DF5467: touching bios[0]=0xfffff8011887c000 — expect kernel page fault...
      Fatal trap 12: page fault while in kernel mode
      fault virtual address	= 0xfffff8011887c000
      fault code		= supervisor read data, page not present
      Stopped at      df5467_modevent+0x3ff:  movzbl  (%rbx),%esi

  CONTROL phases (predicate consistent) map/unmap/remap/unmap cleanly on the
  same kernel — the only variable is the predicate flip.

- Baseline kernel: stock snapshot `#0: Thu Jul 2 06:02:54 UTC 2026`
  (`env.txt`).

## Fix validation

- `fix.diff` — record in the softc how map_bios mapped the BIOS
  (`vga_bios_shadow`); unmap tears down *that* kind. git-apply clean
  against the repo tree.
- Applied to the guest `/usr/src`, kernel rebuilt
  (`make -j6 nativekernel KERNCONF=X86_64_GENERIC`, rc=0, -Werror;
  `fix_build.log`), installed, rebooted
  (`#1: Tue Oct 6 00:18:55 UTC 2026`).
- Identical PoC on the fixed kernel (`fix_run.log`): the BUG-phase unmap now
  performs a proper ROM-path release; the post-bug remap allocates a **fresh**
  mapping and reads the ROM signature `0x55/0xaa`; unmap clean; `kldload
  rc=0`; guest alive. Baseline panic ⇒ baseline_reproduced=1;
  patched bad-behavior ⇒ patched_reproduced=0.
- Note: the module's "shadow-path ... ran" kprintf fires unconditionally
  after `vga_pci_unmap_bios()` returns; on the fixed kernel the shadow path
  did NOT run (the remap discriminator proves it). Kept as-is to keep the
  two runs byte-identical except for the kernel.

## PoC changes vs the seeded sketch (honesty log)

- `run_root` does not forward stdin — initial `cat >` push produced an
  empty .c and a vacuous "successful" build; switched to `scp -F config`.
- KLD Makefile needed generated headers (`device_if.h bus_if.h pci_if.h`).
- DF kernel env accessor is `kgetenv`, not `getenv`.
- Seeded rle size math fixed (32-bit `~mask` + 1); stale rle now deleted
  before re-adding.
- ROM rle seeding added after the live discovery that DF's lazy
  `pci_alloc_map()` cannot allocate ROM BARs at all on this guest
  (DF-5468) — the seed emulates FreeBSD-style pre-reservation.
- Added CONTROL phases and the remap discriminator (a correct ROM-path
  release also unmaps the ROM, so "touch bios[0]" alone cannot distinguish
  fixed from unfixed).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
