DF-0732 / harness.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 350 351 352 353 354 355 356 | /* * DF-0732 โ deterministic TOCTOU harness for ieee80211_acl acl_getioctl MACCMD_LIST * * Transcribes sys/netproto/802_11/wlan_acl/ieee80211_acl.c:299-343 verbatim, * with a CONTROLLED INTERLEAVING POINT between the unlocked `as_nacls` read * (line 313) and the `ACL_LOCK` acquisition (line 328). The race is * deterministic when the attacker controls both threads; this harness models * exactly that, so it is the primary proof of the bug (the runtime path needs * a wifi vap on a wifi radio, absent on this KVM guest โ see VERDICT.md). * * Build BUGGY transcription (matches current source): * cc -O2 -pthread -o harness harness.c * Build FIXED transcription (lock-before-read + M_ZERO + bounded foreach): * cc -O2 -pthread -DFIXED -o harness_fixed harness.c * * Modes: * ./harness grow N K # list grows by K between read and lock * ./harness shrink N K # list shrinks by K between read and lock * * On the BUGGY build: * grow -> "GROW RACE OOB WRITE CONFIRMED" (K MACs written past kmalloc end) * shrink -> "SHRINK RACE UNINIT LEAK CONFIRMED" (K tail MACs are uninit canary) * On the FIXED build both races are eliminated (built with -DFIXED). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <pthread.h> #include <assert.h> #define IEEE80211_ADDR_LEN 6 /* ieee80211.h:35 */ #define CANARY 0xAA /* poisoned-allocator fill (models uninit heap) */ #define MAX_REDZONE 4096 /* ---- faithful transcription of the data structures ---- */ struct acl { /* ieee80211_acl.c:74-78 */ struct acl *acl_next; /* TAILQ link (singly-linked is enough) */ uint8_t acl_macaddr[IEEE80211_ADDR_LEN]; }; struct ieee80211req_maclist { /* ieee80211_ioctl.h:329-331 */ uint8_t ml_macaddr[IEEE80211_ADDR_LEN]; } __attribute__((packed)); struct aclstate { /* ieee80211_acl.c:79-86 */ pthread_mutex_t as_lock; /* acl_lock_t (ieee80211_dragonfly.h:568) */ uint32_t as_nacls; struct acl *as_list_head; /* TAILQ_HEAD simplified */ struct acl **as_list_ptail; /* tail-insert pointer */ }; static struct aclstate AS; #define ACL_LOCK(as) pthread_mutex_lock(&(as)->as_lock) #define ACL_UNLOCK(as) pthread_mutex_unlock(&(as)->as_lock) /* TAILQ_FOREACH equivalent for our singly-linked list */ #define TAILQ_FOREACH_equiv(var) \ for (var = (AS.as_list_head); var != NULL; var = var->acl_next) /* ---- controlled-interleaving state (buggy build only) ---- */ static volatile int g_size_read_done = 0; /* victim set after unlocked read :313 */ static volatile int g_racer_done = 0; /* racer set after mutating list */ static int g_mode; /* 0=grow, 1=shrink */ static int g_K; /* entries to add/remove in the window */ /* attacker-controlled, recognizable MAC bytes (so OOB write is observable) */ static void make_attacker_mac(uint8_t *m, int seq) { m[0] = 0xDE; m[1] = 0xAD; m[2] = 0xBE; m[3] = 0xEF; m[4] = (uint8_t)((seq >> 8) & 0xff); m[5] = (uint8_t)(seq & 0xff); } /* ---- transcribed writers (acl_add / _acl_free) โ all under ACL_LOCK ---- */ static void acl_add_locked(uint8_t mac[IEEE80211_ADDR_LEN]) { /* transcribes ieee80211_acl.c:179-220 (lock taken by caller here) */ struct acl *n = (struct acl *)calloc(1, sizeof(struct acl)); assert(n); memcpy(n->acl_macaddr, mac, IEEE80211_ADDR_LEN); n->acl_next = NULL; if (AS.as_list_ptail != NULL) *AS.as_list_ptail = n; else AS.as_list_head = n; AS.as_list_ptail = &n->acl_next; AS.as_nacls++; /* :214 */ } static void acl_remove_first_locked(void) { /* transcribes _acl_free (ieee80211_acl.c:150-159) on the head */ struct acl *n = AS.as_list_head; if (n == NULL) return; AS.as_list_head = n->acl_next; if (AS.as_list_head == NULL) AS.as_list_ptail = &AS.as_list_head; free(n); AS.as_nacls--; /* :158 */ } #ifndef FIXED /* ---- RACER thread: runs ADDMAC or DELMAC inside the TOCTOU window ---- */ static void * racer(void *arg) { (void)arg; /* spin-wait until victim has completed the UNLOCKED size read (:313) */ while (!g_size_read_done) ; ACL_LOCK(&AS); if (g_mode == 0) { /* GROW: ADDMAC x K */ for (int k = 0; k < g_K; k++) { uint8_t mac[IEEE80211_ADDR_LEN]; make_attacker_mac(mac, 9000 + k); /* recognizable OOB bytes */ acl_add_locked(mac); } } else { /* SHRINK: DELMAC x K */ for (int k = 0; k < g_K; k++) acl_remove_first_locked(); } ACL_UNLOCK(&AS); g_racer_done = 1; return NULL; } #endif /* ---- allocator wrapper around the victim's kmalloc(:319) ---- * Always allocates space + REDZONE and poisons ALL of it with CANARY. The * region [space .. space+REDZONE) is the OOB red zone the grow race writes * into; the region [0 .. space) is the legit buffer whose tail the shrink * race leaves uninitialized. In the FIXED build we additionally zero the * legit buffer [0..space) to model M_ZERO (the redzone stays canary so the * GROW OOB detection remains meaningful in both builds). */ static uint8_t * harness_alloc(uint32_t space, uint32_t redzone) { uint8_t *p = (uint8_t *)malloc(space + redzone); assert(p); memset(p, CANARY, space + redzone); /* poison redzone + buffer */ #ifdef FIXED memset(p, 0, space); /* M_ZERO on the legit buffer only */ #endif return p; } /* ---- VICTIM: faithful transcription of acl_getioctl MACCMD_LIST (:312-340) ---- * Returns the malloc'd buffer (base) and reports counts via outparams. */ static uint8_t * victim_maccmd_list(uint32_t *out_space, uint32_t *out_i_final, uint32_t *out_oob) { struct acl *acl; uint32_t i, space; struct ieee80211req_maclist *ap; uint32_t redzone = (uint32_t)g_K * IEEE80211_ADDR_LEN; if (redzone > MAX_REDZONE) redzone = MAX_REDZONE; /* ieee80211_acl.c:313 */ #ifdef FIXED /* ---- FIXED: lock BEFORE reading the size (:313) ---- */ ACL_LOCK(&AS); #endif space = AS.as_nacls * IEEE80211_ADDR_LEN; /* :313 (UNLOCKED in buggy) */ #ifndef FIXED /* signal the racer to mutate the list right here, between :313 and :328 */ g_size_read_done = 1; while (!g_racer_done) ; /* wait for racer's ADDMAC/DELMAC to land */ #endif /* ieee80211_acl.c:319-320 kmalloc(space, M_TEMP, M_INTWAIT) -- no M_ZERO */ ap = (struct ieee80211req_maclist *)harness_alloc(space, redzone); if (ap == NULL) { *out_space = space; *out_i_final = 0; *out_oob = 0; return NULL; } i = 0; /* :327 */ #ifndef FIXED ACL_LOCK(&AS); /* :328 */ #endif /* ieee80211_acl.c:329-332 */ #ifdef FIXED /* ---- FIXED: bound the write loop to space/ADDR_LEN entries ---- */ { uint32_t bound = space / IEEE80211_ADDR_LEN; TAILQ_FOREACH_equiv(acl) { if (i >= bound) break; /* defense-in-depth */ memcpy(ap[i].ml_macaddr, acl->acl_macaddr, IEEE80211_ADDR_LEN); i++; } } #else TAILQ_FOREACH_equiv(acl) { memcpy(ap[i].ml_macaddr, acl->acl_macaddr, IEEE80211_ADDR_LEN); /* :330 */ i++; } #endif ACL_UNLOCK(&AS); /* :333 */ *out_space = space; *out_i_final = i; *out_oob = (i * IEEE80211_ADDR_LEN > space) ? 1 : 0; return (uint8_t *)ap; } /* ---- setup: seed the list with N baseline entries ---- */ static void seed_list(int N) { ACL_LOCK(&AS); for (int k = 0; k < N; k++) { uint8_t mac[IEEE80211_ADDR_LEN]; make_attacker_mac(mac, 1000 + k); acl_add_locked(mac); } ACL_UNLOCK(&AS); } static int run_scenario(int mode, int N, int K) { /* reset global state */ pthread_mutex_destroy(&AS.as_lock); memset(&AS, 0, sizeof(AS)); pthread_mutex_init(&AS.as_lock, NULL); AS.as_list_head = NULL; AS.as_list_ptail = &AS.as_list_head; AS.as_nacls = 0; g_size_read_done = 0; g_racer_done = 0; g_mode = mode; g_K = K; seed_list(N); #ifndef FIXED pthread_t tr; pthread_create(&tr, NULL, racer, NULL); #endif uint32_t space = 0, i_final = 0, oob = 0; uint8_t *ap = victim_maccmd_list(&space, &i_final, &oob); #ifndef FIXED pthread_join(tr, NULL); #endif if (ap == NULL) { printf(" [harness] kmalloc returned NULL (ENOMEM) โ scenario inconclusive\n"); return 1; } uint32_t allocated_bytes = space; uint32_t written_bytes = i_final * IEEE80211_ADDR_LEN; uint32_t redzone_bytes = (uint32_t)K * IEEE80211_ADDR_LEN; if (redzone_bytes > MAX_REDZONE) redzone_bytes = MAX_REDZONE; printf(" [harness] N=%d K=%d as_nacls@read=%u space=%u bytes list@foreach had %u entries\n", N, K, space / IEEE80211_ADDR_LEN, allocated_bytes, i_final); printf(" [harness] foreach wrote %u bytes into %u-byte buffer (oob_flag=%u)\n", written_bytes, allocated_bytes, oob); int verdict = 0; if (mode == 0) { /* GROW: expect ap[i] for i in [N .. N+K) to be OOB writes past `space`. * Check the red zone [allocated_bytes .. allocated_bytes+redzone_bytes). */ int oob_hits = 0; for (uint32_t b = allocated_bytes; b < allocated_bytes + redzone_bytes; b++) { uint8_t v = ap[b]; if (v != CANARY) { oob_hits++; } } if (oob_hits > 0) { printf(" [harness] GROW: %u OOB bytes overwritten past kmalloc end " "(attacker MAC bytes landed in red zone)\n", oob_hits); printf("GROW RACE OOB WRITE CONFIRMED\n"); verdict = 1; } else { printf(" [harness] GROW: red zone intact โ no OOB write\n"); printf("GROW RACE: NOT TRIGGERED (no OOB write)\n"); } } else { /* SHRINK: list has N-K entries at foreach; tail K slots [N-K..N) were * never written. With kmalloc(no M_ZERO) they hold heap residue (canary). * copyout would ship them to userland = info leak. */ int uninit_hits = 0; uint32_t first_uninit_slot = (uint32_t)(N - K); for (uint32_t s = first_uninit_slot; s < (uint32_t)N; s++) { uint8_t *slot = ap + s * IEEE80211_ADDR_LEN; int all_canary = 1; for (int b = 0; b < IEEE80211_ADDR_LEN; b++) if (slot[b] != CANARY) { all_canary = 0; break; } if (all_canary) uninit_hits++; } if (uninit_hits > 0) { printf(" [harness] SHRINK: %u tail slots uninitialized (heap residue " "would be shipped by copyout = info leak)\n", uninit_hits); printf("SHRINK RACE UNINIT LEAK CONFIRMED\n"); verdict = 1; } else { printf(" [harness] SHRINK: all tail slots zeroed/valid โ no uninit leak\n"); printf("SHRINK RACE: NOT TRIGGERED (no uninit tail)\n"); } } free(ap); /* drain remaining list */ ACL_LOCK(&AS); while (AS.as_list_head) acl_remove_first_locked(); ACL_UNLOCK(&AS); return verdict; } int main(int argc, char **argv) { if (argc < 4) { fprintf(stderr, "usage: %s <grow|shrink> <N> <K>\n" " N = baseline list size (as_nacls at unlocked read)\n" " K = entries added(grow)/removed(shrink) in the TOCTOU window\n", argv[0]); return 2; } const char *mode_s = argv[1]; int N = atoi(argv[2]); int K = atoi(argv[3]); int mode; if (strcmp(mode_s, "grow") == 0) mode = 0; else if (strcmp(mode_s, "shrink") == 0) mode = 1; else { fprintf(stderr, "mode must be 'grow' or 'shrink'\n"); return 2; } if (N <= 0 || K <= 0) { fprintf(stderr, "N and K must be > 0\n"); return 2; } if (mode == 1 && K > N) { fprintf(stderr, "shrink: K must be <= N\n"); return 2; } #ifdef FIXED printf("=== DF-0732 harness (FIXED transcription: lock-before-read + M_ZERO + bounded foreach) ===\n"); #else printf("=== DF-0732 harness (BUGGY transcription: matches ieee80211_acl.c current source) ===\n"); #endif printf(" mode=%s N=%d K=%d IEEE80211_ADDR_LEN=%d\n\n", mode_s, N, K, IEEE80211_ADDR_LEN); int rc = run_scenario(mode, N, K); printf("\n[exit rc=%d]\n", rc); return rc; } |