โฌข DragonFlyBSD Kernel Audit
DF-5467 / df5467.c
โ† back to finding โ†“ download raw
/*
 * DF-5467 PoC โ€” vga_pci_unmap_bios() shadow-path teardown of a ROM-BAR mapping
 *
 * Bug (sys/bus/pci/vga_pci.c):
 *   - vga_pci_map_bios() picks the mapping kind by calling
 *     vga_pci_is_boot_display(dev) at entry (vga_pci.c:175).
 *   - On the ROM path it *itself* mutates the inputs of that predicate:
 *     it sets PCIB_BCR_VGA_ENABLE on the parent bridge (vga_pci.c:197-201).
 *     vga_pci_is_boot_display() also latches vga_pci_default_unit (>=0 is
 *     sticky) on its first successful evaluation (vga_pci.c:94-101,139).
 *   - vga_pci_unmap_bios() RE-EVALUATES the same predicate (vga_pci.c:288)
 *     instead of remembering how the BIOS was mapped.  If the answer flipped
 *     from 0 (at map time) to 1 (at unmap time), it executes the shadow-copy
 *     teardown:
 *         pmap_unmapdev((vm_offset_t)bios, VGA_PCI_BIOS_SHADOW_SIZE)   (:290)
 *     on a pointer that is actually a ROM-BAR mapping of size
 *     rman_get_size(res) โ€” typically 64K, never 128K โ€” so pmap_unmapdev()
 *     clears PTEs and frees kernel_map VA over a 128KiB range
 *     (sys/platform/pc64/x86_64/pmap.c:6223-6232: pmap_qremove + kmem_free).
 *     The KASSERTs that would have validated the mapping (vga_pci.c:314-316)
 *     sit *after* the early return and never run on this path.
 *     It also leaks the ROM resource (vr_res/refs left set), so the next
 *     vga_pci_map_bios() hands out the same kva whose PTEs are gone.
 *
 * Real-hardware trigger (no root needed โ€” it is drm attach-time state):
 *   secondary GPU behind a PCI-PCI bridge with BCR.VGA_ENABLE clear (typical
 *   for non-boot GPUs on UEFI/firmware that never enabled VGA routing),
 *   vga_pci_default_unit still -1 (no boot display selected during probe),
 *   GPU MEM/IO decoding on  ->  drm attach -> radeon_read_bios()/
 *   amdgpu map+unmap (sys/dev/drm/radeon/radeon_bios.c:86-104) flips the
 *   predicate in between -> kernel PTE teardown + kernel_map VA corruption.
 *
 * QEMU-guest emulation of the trigger:
 *   this guest's std VGA (0x1234:0x1111) hangs on the root bus (no PCI-PCI
 *   bridge, so vga_pci.c:197-201 is a no-op here) and IS the boot display
 *   (default_vgapci_unit=0).  We emulate the *outcomes* of the firmware
 *   state with the exact kernel-visible variables:
 *     - map-time "not boot display": default_unit := 77 (>=0, != unit) โ€”
 *       identical to vga_pci.c:100 returning 0;
 *     - the flip between map and unmap: default_unit := -1 followed by
 *       calling the kernel's own vga_pci_is_boot_display(dev), which latches
 *       default_unit := 0 and returns 1 โ€” literally the same latch the
 *       kernel performs (vga_pci.c:139) once map_bios has set the bridge
 *       VGA-enable bit.
 *
 * Expected result: panic (kernel page fault) when touching the ROM mapping
 * right after the mismatched unmap โ€” proof that the shadow-path teardown
 * destroyed the ROM-BAR mapping's page tables.
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/rman.h>
#include <bus/pci/pcireg.h>
#include <bus/pci/pcivar.h>

extern int vga_pci_default_unit;	/* sys/bus/pci/vga_pci.c:72 */

static device_t	vga_dev;

/*
 * DF's lazy pci_alloc_map() misclassifies a ROM BAR whose enable bit
 * survives the 0xffffffff sizing readback (PCI_BAR_IO(testval)==1), and DF
 * never pre-reserves the ROM BAR during enumeration (pci.c:2720-2751 stop at
 * BAR5), so vga_pci_map_bios()'s ROM path always fails here.  Seed the
 * resource-list entry exactly as FreeBSD's enumeration does โ€” this also
 * emulates real bit0-masking hardware where the lazy path succeeds.  We seed
 * at the firmware-programmed address so no BAR rebasing is involved.
 */
static int
seed_rom_rle(void)
{
	struct resource_list *rl;
	uint32_t saved, testval, mask;
	uint64_t base, romsz;

	saved = pci_read_config(vga_dev, PCIR_BIOS, 4);
	pci_write_config(vga_dev, PCIR_BIOS, 0xffffffff, 4);
	testval = pci_read_config(vga_dev, PCIR_BIOS, 4);
	pci_write_config(vga_dev, PCIR_BIOS, saved, 4);

	kprintf("DF5467: ROM BAR sizing readback testval=0x%08x "
	    "(enable-bit readback => pci_alloc_map sees PCI_BAR_IO=%d)\n",
	    testval, testval & 1);

	mask = testval & ~0x7ffU;
	if (mask == 0 || mask == 0xffffffffU)
		return (0);
	romsz = (u_long)(~mask) + 1;	/* 0xffff0000 -> 0x10000 */
	base = saved & ~0x7ffUL;

	rl = BUS_GET_RESOURCE_LIST(device_get_parent(vga_dev), vga_dev);
	if (rl == NULL)
		return (0);
	/* replace any stale entry (must have res == NULL) */
	resource_list_delete(rl, SYS_RES_MEMORY, PCIR_BIOS);
	resource_list_add(rl, SYS_RES_MEMORY, PCIR_BIOS,
	    (u_long)base, (u_long)(base + romsz - 1), (u_long)romsz, -1);
	kprintf("DF5467: seeded ROM rle rid=0x%x base=0x%jx size=0x%jx\n",
	    PCIR_BIOS, (uintmax_t)base, (uintmax_t)romsz);
	return (1);
}

static void
df5467_run(void)
{
	unsigned long size;
	void *bios, *bios2;
	int saved_unit, is_boot, val;

	saved_unit = vga_pci_default_unit;
	kprintf("DF5467: start vgapci0=%p default_unit=%d hz=%d\n",
	    vga_dev, vga_pci_default_unit, hz);

	if (!seed_rom_rle()) {
		kprintf("DF5467: no usable ROM BAR; cannot demo\n");
		goto out;
	}

	/*
	 * CONTROL: predicate returns 0 for BOTH map and unmap
	 * (this is the correct pairing โ€” must be clean).
	 */
	vga_pci_default_unit = 77;
	bios = vga_pci_map_bios(vga_dev, (size_t *)&size);
	kprintf("DF5467: CONTROL map  -> bios=%p size=0x%lx\n", bios, size);
	if (bios != NULL) {
		vga_pci_unmap_bios(vga_dev, bios);
		kprintf("DF5467: CONTROL unmap (ROM path) done\n");

		bios = vga_pci_map_bios(vga_dev, (size_t *)&size);
		kprintf("DF5467: CONTROL remap-> bios=%p size=0x%lx\n", bios, size);
		if (bios != NULL) {
			vga_pci_unmap_bios(vga_dev, bios);
			kprintf("DF5467: CONTROL remap+unmap done (balanced)\n");
		}
	}

	/*
	 * BUG: map sees is_boot_display()==0, unmap sees 1.
	 */
	vga_pci_default_unit = 77;
	bios = vga_pci_map_bios(vga_dev, (size_t *)&size);
	kprintf("DF5467: BUG map     -> bios=%p size=0x%lx (ROM-BAR path)\n",
	    bios, size);
	if (bios == NULL) {
		kprintf("DF5467: map failed; cannot demo\n");
		goto out;
	}

	/*
	 * The flip.  On bridged hardware vga_pci_map_bios() itself causes it
	 * (vga_pci.c:197-201 sets PCIB_BCR_VGA_ENABLE; the next
	 * vga_pci_is_boot_display() then passes and latches default_unit,
	 * vga_pci.c:139).  Here we perform the identical kernel-side latch.
	 */
	vga_pci_default_unit = -1;
	is_boot = vga_pci_is_boot_display(vga_dev);
	kprintf("DF5467: flip: is_boot_display=%d default_unit=%d\n",
	    is_boot, vga_pci_default_unit);
	if (!is_boot) {
		kprintf("DF5467: flip failed; aborting\n");
		goto out;
	}

	vga_pci_unmap_bios(vga_dev, bios);
	kprintf("DF5467: BUG unmap returned โ€” shadow-path pmap_unmapdev(ROM kva, 128K) ran\n");

	/*
	 * Primitive proof: the ROM mapping's own PTEs were cleared by
	 * pmap_qremove() over the 128K shadow size.  (On an UNFIXED kernel
	 * this faults; on a FIXED kernel the unmap took the ROM path, which
	 * also unmaps โ€” so this probe is only run when kenv df5467.touch=1.)
	 */
	if (kgetenv("df5467.touch") != NULL) {
		kprintf("DF5467: touching bios[0]=%p โ€” expect kernel page fault...\n", bios);
		val = *(volatile uint8_t *)bios;
		kprintf("DF5467: read bios[0]=0x%x WITHOUT fault (unexpected)\n", val);
	}

	/*
	 * Leak discriminator (fix validation): on an unfixed kernel the ROM
	 * resource is leaked (vr_res still cached) and this map hands back
	 * the stale kva -> kernel page fault inside vga_pci_map_bios' poll
	 * loop.  On a fixed kernel the release above was a proper ROM-path
	 * release and this remap must succeed cleanly.
	 */
	vga_pci_default_unit = 77;
	bios2 = vga_pci_map_bios(vga_dev, (size_t *)&size);
	kprintf("DF5467: post-bug remap -> %p size=0x%lx\n", bios2, (unsigned long)size);
	if (bios2 != NULL) {
		kprintf("DF5467: remap reads 0x55/0xaa = %02x/%02x (clean)\n",
		    ((volatile uint8_t *)bios2)[0], ((volatile uint8_t *)bios2)[1]);
		vga_pci_unmap_bios(vga_dev, bios2);
		kprintf("DF5467: post-bug remap unmap done โ€” FIXED behavior\n");
	}

out:
	vga_pci_default_unit = saved_unit;
	kprintf("DF5467: done (default_unit restored to %d)\n", saved_unit);
}

static int
df5467_modevent(struct module *m, int what, void *arg)
{
	switch (what) {
	case MOD_LOAD:
		vga_dev = devclass_get_device(devclass_find("vgapci"), 0);
		if (vga_dev == NULL) {
			kprintf("DF5467: vgapci0 not found\n");
			return (ENOENT);
		}
		df5467_run();
		return (0);
	default:
		return (0);
	}
}

static moduledata_t df5467_mod = {
	"df5467",
	df5467_modevent,
	NULL
};
DECLARE_MODULE(df5467, df5467_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_VERSION(df5467, 1);