vga_pci_unmap_bios() re-evaluates vga_pci_is_boot_display() and tears down a ROM-BAR mapping with the 128K shadow size: kernel page-table teardown + kernel_map VA free beyond the mapping, plus a leaked live ROM resource
Summary
map_bios picks the mapping kind via the predicate then MUTATES its inputs (sets PCIB_BCR_VGA_ENABLE) and the predicate latches default_unit; unmap RE-evaluates it - when the answer flips, the shadow teardown pmap_unmapdev's 128K over the 64K ROM mapping: neighboring kernel mappings lose PTEs and kernel_map VA is double-assigned; the mismatch KASSERTs sit after the early return and the ROM resource leaks. KLD-proven on the guest: flip -> Fatal trap 12 page-not-present at the ROM kva; stable-predicate controls clean. Live on secondary GPUs with bridge VGA-enable clear + headless default_unit (drm radeon_read_bios map/unmap pair flips it); DF-5468 masks it on bit0-preserving readback. Record-the-kind fix validated on rebuilt kernel.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5467 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5467.c | β | 8.5 KB | view raw | |
| Makefile | β | 82 B | β download | |
| build.sh | β | 220 B | view raw | |
| run.sh | β | 127 B | view raw | |
| build.log | β | 822 B | view raw | |
| run.log | β | 715 B | view raw | |
| run.2.log | β | 239 B | view raw | |
| fix_run.log | β | 864 B | view raw | |
| fix_build.log | β | 5.0 KB | view raw | |
| serial-full.log | β | 2.6 KB | view raw | |
| panic.txt | β | 1.4 KB | view raw | |
| env.txt | β | 491 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| VERDICT.md | β | 7.9 KB | β raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 6.6 KB | view raw |
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)
-
vga_pci_map_bios()chooses the mapping kind by evaluatingvga_pci_is_boot_display(dev)at entry βsys/bus/pci/vga_pci.c:175. TRUE β return a freshpmap_mapbios(0xC0000, 128K)shadow mapping (:185-186). FALSE β ROM-BAR resource path (:190-274) returningrman_get_virtual(res)of sizerman_get_size(res)(here 64 KiB). -
The predicate is mutable and latching: - on the ROM path
vga_pci_map_bios()itself setsPCIB_BCR_VGA_ENABLEon the parent PCI-PCI bridge (:197-201) β one of the exact inputsvga_pci_is_boot_display()consults (:120-124); -vga_pci_default_unitis sticky: the first evaluation that passes all checks latches it (:94-101, :139). -
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 teardownpmap_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.
- Secondary effect: the shadow path returns without touching
sc->vga_biosβ the ROM resource stays cached (vr_res != NULL,vr_refs == 1), so the nextvga_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_ENABLEis 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 followingvga_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_unitbetween 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 signature0x55/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_rootdoes not forward stdin β initialcat >push produced an empty .c and a vacuous "successful" build; switched toscp -F config.- KLD Makefile needed generated headers (
device_if.h bus_if.h pci_if.h). - DF kernel env accessor is
kgetenv, notgetenv. - 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.
Fix verification
fixedfix.diff applied to the guest /usr/src copy, kernel rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC (rc=0, -Werror), installed, rebooted (#1: Tue Oct 6 00:18:55 UTC 2026). Identical PoC and state sequence: BUG-phase unmap now performs a proper ROM-path release; post-bug remap allocates a fresh mapping and reads the ROM signature 0x55/0xaa; unmap clean; kldload rc=0; guest alive. Baseline (stock #0 kernel): Fatal trap 12 kernel page fault, guest down.
fix_run.log (patched run, decisive lines 'post-bug remap -> 0xfffff801185a4000', 'remap reads 0x55/0xaa = 55/aa (clean)', 'alive'); panic.txt (baseline); fix_build.log (fixed-tree build completion rc=0); fix.diff (git apply --check clean against the repo tree)
Confirmed kernel references
Detail
Exploit chain
firmware/hardware state (bridge VGA-enable clear + default_unit unset + GPU decoding on, or malicious PCIe device presenting this topology) -> drm attach (radeon_read_bios, radeon_bios.c:86-104) -> vga_pci_map_bios ROM path sets the bridge bit -> vga_pci_unmap_bios re-evaluates predicate, now true -> pmap_unmapdev(rom_kva, 128K) over a 64K mapping -> (a) PTEs of the ROM mapping and up to 16 adjacent kernel pages cleared (demonstrated: kernel page fault/panic), (b) kmem_free of 64K of kernel_map VA the kernel still considers allocated (VA double-assignment primitive), (c) ROM resource leaked with live vr_res -> subsequent map_bios hands out stale kva -> fault in kernel poll loop. Weaponization to uid=0 would require grooming kernel_map adjacency so the freed 64K range re-allocates over a victim object; not executed (trigger is firmware-state-gated rather than unpriv-user-gated).
Evidence (decisive lines)
panic.txt (Fatal trap 12, fault virtual address = ROM kva 0xfffff8011887c000, 'supervisor read data, page not present', stopped at df5467_modevent+0x3ff); serial-full.log lines 30-46 (full CONTROL/BUG sequence; CONTROL phases prove balanced map/unmap when the predicate does not flip); fix_run.log (patched kernel: post-bug remap -> 0xfffff801185a4000, 'remap reads 0x55/0xaa = 55/aa (clean)', kldload rc=0, guest alive); fix.diff; fix_build.log; VERDICT.md
PoC changes
run_root does not forward stdin (first push produced an empty .c and a vacuous green build - switched to scp); KLD Makefile needed device_if.h/bus_if.h/pci_if.h in SRCS; DF kernel env accessor is kgetenv not getenv; seeded ROM rle size math corrected to 32-bit ~mask+1 and stale rle deleted before re-add; ROM resource-list seeding added after discovering live that DF's lazy pci_alloc_map() cannot allocate ROM BARs at all on this guest (filed as DF-5468) - the seed emulates FreeBSD-style pre-reservation/bit0-masking hardware; CONTROL phases and a post-bug remap discriminator added because a correct ROM-path release also unmaps the ROM (touch-after-unmap alone cannot distinguish fixed from unfixed).
Verified recommended fix
Record the mapping kind in struct vga_pci_softc (vga_bios_shadow) when vga_pci_map_bios() creates it, and have vga_pci_unmap_bios() tear down that recorded kind instead of re-evaluating vga_pci_is_boot_display(); full diff in findings/poc/DF-5467/fix.diff
Verdict
vga_pci_unmap_bios() re-evaluates vga_pci_is_boot_display() (vga_pci.c:288) to decide which mapping kind to tear down, but vga_pci_map_bios() itself mutates that predicate's inputs (it sets PCIB_BCR_VGA_ENABLE on the parent bridge, vga_pci.c:197-201, and the predicate latches the sticky vga_pci_default_unit, vga_pci.c:139). When the answer flips between map and unmap β the stock case being a secondary GPU behind a bridge with VGA-enable clear and no boot display selected, driven by drm's radeon_read_bios() map/unmap pair β unmap executes the shadow-copy teardown pmap_unmapdev(bios, 128K) (vga_pci.c:290) on a ROM-BAR mapping of size rman_get_size() (64K here). pmap_unmapdev clears PTEs and frees kernel_map VA for the full 128K (pmap.c:6223-6232): kernel page-table teardown of up to 64K of neighboring kernel mappings plus a kernel_map VA double-free of unowned space; the KASSERTs at vga_pci.c:314-316 are bypassed by the early return, and the ROM resource leaks so the next map_bios returns stale unmapped kva. Demonstrated on the QEMU guest: CONTROL phases (consistent predicate) clean; BUG phase ends in Fatal trap 12, supervisor read data page not present, at the ROM kva. fix.diff (record the mapping kind in the softc) rebuilt and installed on the guest: identical PoC completes cleanly (fresh remap reads 0x55/0xaa, guest stays up).
No comments yet.