DF-5246 / beacon_race.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 | /* * DF-5246 - Simulated reproduction of the if_run(4) beacon_mbuf race * ================================================================== * * Kernel code modeled (sys/bus/u4b/wlan/if_run.c, DragonFly 6.5-DEVEL): * * run_newstate() [thread A: net80211 nstate task] if_run.c:2067-2106 * IEEE80211_UNLOCK(ic); (drops ic lock) * RUN_LOCK(sc); (takes sc lock only) * ... * case IEEE80211_S_RUN: * m_freem(rvp->beacon_mbuf); <-- FREES, holds only sc lock * rvp->beacon_mbuf = NULL; * * run_update_beacon() [thread B: tx/rx path, e.g. ieee80211_set_tim * -> vap->iv_update_beacon, ieee80211_power.c:316, * executed with the ic lock held, sc lock NOT held] * if_run.c:4861-4901 * if (rvp->beacon_mbuf == NULL) { ... alloc ... } <-- check 4887 * ieee80211_beacon_update(ni, rvp->beacon_mbuf, mcast); <-- USE 4892 * -> writes wh->i_seq (2 bytes @ offset 22), bo_caps, TIM bytes, * and on the CSA path rewrites the whole beacon (beacon_construct) * through the mbuf data pointer (ieee80211_output.c:3339+) * * The two paths share NO common lock (ic lock vs sc lock), so the * check-then-use in run_update_beacon races with the free in * run_newstate / run_vap_delete (if_run.c:1013) -> write-after-free on a * mbuf cluster (kernel heap). * * This harness reproduces that interleaving in userland with two threads * and the same two independent mutexes. A "hit" is counted when thread B * passes the NULL-check on a beacon mbuf that thread A frees before B's * write phase. (The write is withheld in the harness so the process * survives; in the kernel it is a real write into freed memory.) * * build: cc -O2 -pthread -o beacon_race beacon_race.c * run: ./beacon_race [seconds] */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <unistd.h> /* ---- the two kernel lock domains ---- */ static pthread_mutex_t ic_lock = PTHREAD_MUTEX_INITIALIZER; /* IEEE80211_LOCK */ static pthread_mutex_t sc_lock = PTHREAD_MUTEX_INITIALIZER; /* RUN_LOCK */ /* ---- rvp->beacon_mbuf stand-in ---- */ struct mbuf_sim { volatile int live; /* 1 while not m_freem'd */ unsigned char data[512]; }; static struct mbuf_sim *beacon_mbuf; static long uaf_hits; /* thread B used a freed mbuf */ static long allocs; /* beacon_alloc calls */ static long frees; /* m_freem calls */ static long updates; /* beacon_update calls that saw live mbuf */ static volatile int stop; static struct mbuf_sim *beacon_alloc(void) { struct mbuf_sim *m = calloc(1, sizeof(*m)); m->live = 1; __sync_fetch_and_add(&allocs, 1); return m; } /* m_freem(): the kernel frees the cluster here. We mark dead then free. */ static void m_freem(struct mbuf_sim *m) { m->live = 0; __sync_fetch_and_add(&frees, 1); free(m); } /* thread B: run_update_beacon() - executes holding ONLY ic_lock */ static void run_update_beacon(void) { struct mbuf_sim *m; pthread_mutex_lock(&ic_lock); /* IEEE80211_LOCK(ic) held by caller */ m = beacon_mbuf; /* if_run.c:4887 check */ if (m == NULL) { m = beacon_alloc(); /* if_run.c:4888 */ beacon_mbuf = m; } /* * ieee80211_beacon_update(ni, m, mcast): writes seq/capinfo/TIM * through the mbuf. We mirror the write set of the kernel function * but first test liveness: if the mbuf was freed between the * NULL-check above and here, the kernel would have written into * freed heap. Count it and skip the actual write. */ if (!m->live) { __sync_fetch_and_add(&uaf_hits, 1); } else { m->data[22] = 0x11; m->data[23] = 0x22; /* wh->i_seq */ memset(m->data + 60, 0x5a, 8); /* TIM bytes */ __sync_fetch_and_add(&updates, 1); } pthread_mutex_unlock(&ic_lock); } /* thread B body: hammer the TIM-update path (ieee80211_set_tim) */ static void *beacon_updater(void *arg) { (void)arg; while (!stop) run_update_beacon(); return NULL; } /* thread A: run_newstate() RUN->INIT - executes holding ONLY sc_lock */ static void run_newstate_to_init(void) { pthread_mutex_lock(&sc_lock); /* if_run.c:2105-2106 */ if (beacon_mbuf != NULL) { m_freem(beacon_mbuf); beacon_mbuf = NULL; } pthread_mutex_unlock(&sc_lock); } /* thread A body: state transitions RUN->INIT->RUN forever */ static void *state_machine(void *arg) { (void)arg; while (!stop) { run_newstate_to_init(); usleep(50); /* re-enter RUN, beacon re-allocated by updater */ } return NULL; } int main(int argc, char **argv) { int secs = (argc > 1) ? atoi(argv[1]) : 5; pthread_t ta, tb, tb2; beacon_mbuf = beacon_alloc(); pthread_create(&ta, NULL, state_machine, NULL); pthread_create(&tb, NULL, beacon_updater, NULL); pthread_create(&tb2, NULL, beacon_updater, NULL); sleep(secs); stop = 1; pthread_join(ta, NULL); pthread_join(tb, NULL); pthread_join(tb2, NULL); printf("DF-5246 beacon_mbuf race simulation over %ds\n", secs); printf(" beacon_allocs : %ld\n", allocs); printf(" m_freems : %ld\n", frees); printf(" clean updates : %ld\n", updates); printf(" UAF hits : %ld (write-after-free in kernel equivalent)\n", uaf_hits); if (uaf_hits > 0) { printf("RESULT: RACE OBSERVED - run_update_beacon used a freed mbuf\n"); return 0; } printf("RESULT: no hit this run (widen load / rerun)\n"); return 1; } |