DF-3459 / df3459_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 | /* * DF-3459 โ rssadapt_rate() returns/stores rix = -1: negative-index accesses * ========================================================================== * * Target: sys/netproto/802_11/wlan/ieee80211_rssadapt.c (wlan_rssadapt module) * * The rate-pick loop in rssadapt_rate() (ieee80211_rssadapt.c:253-255) * * for (rix = rs->rs_nrates-1; rix >= 0; rix--) * if ((*thrs)[rix] < (rssi << 8)) * break; * * falls out of the loop with rix == -1 whenever NO rate row has a threshold * STRICTLY below rssi<<8. Two field-reachable ways to get there: * * (a) rssi == 0. node_getrssi() (ieee80211_node.c:1146-1155) returns 0 * for every node whose ni_avgrssi is still IEEE80211_RSSI_DUMMY_MARKER * (the never-received-a-frame init state) and clamps negatives to 0. * With all thresholds zeroed (fresh M_ZERO allocation) the test * `0 < (0<<8)` is false for every row -> rix = -1 deterministically. * (b) threshold convergence: rssadapt_lower_rate() raises row thresholds * toward rssi<<8 with an integer weighted average that can converge to * exactly rssi<<8; the comparison is strict `<`, so once every row * sits at >= the current rssi<<8 (e.g. station moving away, signal * dropping), the loop again exhausts -> rix = -1. * * With rix == -1 the code then executes: * * ieee80211_rssadapt.c:258 * ni->ni_txrate = ni->ni_rates.rs_rates[rix] & IEEE80211_RATE_VAL; * -> rs_rates[-1] (reads the rs_nrates byte: in-struct, wrong value) * ieee80211_rssadapt.c:259 * ra->ra_rix = rix; -> ra_rix = -1 stored * returns -1 to every caller of ieee80211_ratectl_rate() โ e.g. * if_iwm.c:3540/3574 `int rix = ieee80211_ratectl_rate(...); iwm_setrates(sc, in, rix);` * and if_iwm.c:4366 KASSERT(rix >= 0 && rix < nrates, ("invalid rix")); * -> guaranteed panic on INVARIANTS kernels, silent bogus config otherwise. * * Later, a failed TX completion runs rssadapt_tx_complete() -> * rssadapt_lower_rate() with ra->ra_rix == -1: * * ieee80211_rssadapt.c:279 u_int rix; <- UNSIGNED, as shipped * ieee80211_rssadapt.c:283 rix = ra->ra_rix; -> 0xffffffff * ieee80211_rssadapt.c:284 last_thr = (*thrs)[rix]; READ * ieee80211_rssadapt.c:285 (*thrs)[rix] = interpolate(...); WRITE * * With rix = 0xffffffff, (*thrs)[rix] addresses * thrs + 2 * 0xffffffff = chunk + 0x1fffffffe (~ +8 GiB) * i.e. a WILD 2-byte read followed by a WILD 2-byte write at a fixed * +8GiB offset from the per-node chunk. In the kernel that is a page * fault on unmapped KVA -> guaranteed panic (and on machines whose * direct map covers chunk+8GiB, a real 2-byte wild heap corruption * with a semi-attacker-controlled value via interpolate(last_thr, * rssi<<8)). * * (rssadapt_raise_rate() at line 299 uses a SIGNED int rix; there * (*thrs)[-1] is the 2 bytes before the row โ contained.) * * This is the NEGATIVE-side residual of DF-0730 (which fixed the HIGH-side * bound rs_nrates<=15 vs [8] columns). DF-0730's fix.diff resizes the array * but leaves rix == -1 untouched. * * ------------------------------------------------------------------------ * Harness: replicates the exact struct layout (with DF-0730's resize already * applied: 15 columns, so the only negative-index effect demonstrated here is * THIS finding's) and the exact loop/access logic, inside a canaried region. * * default build : demonstrates rix=-1 -> rs_rates[-1], ra_rix=-1, * iwm_setrates KASSERT condition violation, and the * ra_rate_thresh[0][-1] write aliasing ra_pktrate. * -DFIXED build : applies this finding's fix.diff logic (clamp rix to 0 * when the loop exhausts; skip adapt when ra_rix < 0) and * shows none of the above occur. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> #include <assert.h> #include <signal.h> #include <unistd.h> #include <sys/wait.h> /* ---- sys/netproto/802_11/_ieee80211.h:374-377 ---- */ #define IEEE80211_RATE_SIZE 8 /* 802.11 standard rates */ #define IEEE80211_RATE_MAXSIZE 15 /* max rates we'll handle */ #define IEEE80211_RATE_VAL 0x7f struct ieee80211_rateset { uint8_t rs_nrates; uint8_t rs_rates[IEEE80211_RATE_MAXSIZE]; }; /* ---- sys/netproto/802_11/ieee80211_rssadapt.h:45-67 ---- */ #define IEEE80211_RSSADAPT_BKTS 3 #define IEEE80211_RSSADAPT_BKT0 128 #define IEEE80211_RSSADAPT_BKTPOWER 3 struct ieee80211_rssadapt { const void *vap; int interval; }; struct ieee80211_rssadapt_node { struct ieee80211_rssadapt *ra_rs; struct ieee80211_rateset ra_rates; int ra_rix; int ra_ticks; int ra_last_raise; int ra_raise_interval; uint32_t ra_nfail; uint32_t ra_nok; uint32_t ra_pktrate; uint16_t ra_rate_thresh[IEEE80211_RSSADAPT_BKTS] [IEEE80211_RATE_MAXSIZE]; /* DF-0730 resize */ }; /* ---- replica of the node container: ni_rates precedes ni_txrate ---- */ struct fake_node { struct ieee80211_rateset ni_rates; uint8_t ni_txrate; }; /* ---- exact rssadapt logic (ieee80211_rssadapt.c:220-338) ---- */ static void segv_handler(int sig) { (void)sig; _exit(42); } static int bucket(int pktlen) { int i, top, thridx; for (i = 0, top = IEEE80211_RSSADAPT_BKT0; i < IEEE80211_RSSADAPT_BKTS; i++, top <<= IEEE80211_RSSADAPT_BKTPOWER) { thridx = i; if (pktlen <= top) break; } return thridx; } #define interpolate(parm, old, new) ((parm##_old * (old) + \ (parm##_denom - parm##_old) * (new)) / \ parm##_denom) static const struct { int rc_thresh_old, rc_thresh_denom; } threshctl = {4, 8}; static int rssadapt_rate(struct fake_node *ni, struct ieee80211_rssadapt_node *ra, int rssi, u_int pktlen) { const struct ieee80211_rateset *rs = &ra->ra_rates; uint16_t (*thrs)[IEEE80211_RATE_MAXSIZE]; int rix; thrs = &ra->ra_rate_thresh[bucket(pktlen)]; for (rix = rs->rs_nrates-1; rix >= 0; rix--) if ((*thrs)[rix] < (rssi << 8)) break; #ifdef FIXED if (rix < 0) /* DF-3459 fix: no rate fits */ rix = 0; #endif if (rix != ra->ra_rix) { /* line 258: index rix == -1 reads rs_rates[-1] */ ni->ni_txrate = ni->ni_rates.rs_rates[rix] & IEEE80211_RATE_VAL; ra->ra_rix = rix; } return rix; } static void rssadapt_lower_rate(struct ieee80211_rssadapt_node *ra, int pktlen, int rssi) { uint16_t last_thr; uint16_t (*thrs)[IEEE80211_RATE_MAXSIZE]; u_int rix; /* <- u_int, as in the kernel */ thrs = &ra->ra_rate_thresh[bucket(pktlen)]; rix = ra->ra_rix; /* -1 -> 0xffffffff */ #ifdef FIXED if ((int)rix < 0) /* DF-3459 fix: nothing to adapt */ return; #endif last_thr = (*thrs)[rix]; (*thrs)[rix] = interpolate(threshctl.rc_thresh, last_thr, (rssi << 8)); } /* canaried region so we can prove where the [0][-1] write lands. * Struct sits at offset 32: 8-byte aligned (no aligned-SSE fault) with * canary bytes both before and after. */ enum { CAN = 32 }; static unsigned char zone[sizeof(struct ieee80211_rssadapt_node) + 2 * CAN]; #define CANARY0 0xAA #define CANARY1 0x55 static int canaries_intact(const unsigned char *z) { size_t i; for (i = 0; i < CAN; i++) if (z[i] != CANARY0 || z[CAN + sizeof(struct ieee80211_rssadapt_node) + i] != CANARY1) return 0; return 1; } int main(void) { struct ieee80211_rssadapt_node *ra; struct fake_node ni; long off_thresh, off_pktrate; int rix, i; setvbuf(stdout, NULL, _IONBF, 0); memset(zone, 0, sizeof(zone)); memset(zone, CANARY0, CAN); memset(zone + CAN + sizeof(struct ieee80211_rssadapt_node), CANARY1, CAN); ra = (struct ieee80211_rssadapt_node *)(zone + CAN); off_thresh = offsetof(struct ieee80211_rssadapt_node, ra_rate_thresh); off_pktrate = offsetof(struct ieee80211_rssadapt_node, ra_pktrate); printf("layout: ra_pktrate@%ld ra_rate_thresh@%ld " "(thresh[-1] aliases offset %ld = ra_pktrate%+ld)\n", off_pktrate, off_thresh, off_thresh - 2, (off_thresh - 2) - off_pktrate); /* fresh node: 8 negotiated rates, zeroed thresholds (M_ZERO alloc) */ ra->ra_rates.rs_nrates = 8; for (i = 0; i < 8; i++) ra->ra_rates.rs_rates[i] = (i + 1) * 2; /* 1..8 *2 = .5Mb units */ memset(&ni, 0, sizeof(ni)); ni.ni_rates = ra->ra_rates; ra->ra_rix = 7; /* set by rssadapt_node_init */ ra->ra_pktrate = 0x11223344; /* poison to detect the alias write */ /* (a) fresh node, no RX sample yet: node_getrssi() == 0 (marker) */ rix = rssadapt_rate(&ni, ra, /*rssi*/0, /*pktlen*/64); printf("rssi=0 fresh node : rssadapt_rate -> rix=%d ra_rix=%d " "ni_txrate=0x%02x (bogus: rs_nrates byte %d)\n", rix, ra->ra_rix, ni.ni_txrate, ra->ra_rates.rs_nrates); #ifndef FIXED if (rix >= 0) { printf("RESULT: unexpected rix >= 0 โ premise wrong\n"); return 2; } #endif printf(" -> rs_rates[-1] read returned the rs_nrates byte (%d): " "in-struct OOB read confirmed\n", ni.ni_txrate == (ra->ra_rates.rs_nrates & IEEE80211_RATE_VAL)); /* iwm_setrates() KASSERT โ if_iwm.c:4366 */ printf(" -> iwm_setrates KASSERT(rix >= 0 && rix < nrates): " "%s (INVARIANTS kernel would PANIC here)\n", (rix >= 0 && rix < 8) ? "ok" : "VIOLATED"); /* (b) failed TX completion at ra_rix == -1 -> lower_rate()[*thrs][-1] */ { uint16_t (*thrs)[IEEE80211_RATE_MAXSIZE] = &ra->ra_rate_thresh[bucket(64)]; u_int wild_rix = (u_int)ra->ra_rix; /* kernel's u_int rix */ printf("lower_rate with ra_rix=-1: u_int rix = 0x%08x -> " "access at chunk+%#llx (wild +%llu GiB)\n", wild_rix, (unsigned long long)((char *)thrs + 2ULL * wild_rix - (char *)ra), (unsigned long long)((2ULL * wild_rix) >> 30)); } { struct sigaction sa, osa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = segv_handler; sigaction(SIGSEGV, &sa, &osa); pid_t pid = fork(); if (pid == 0) { /* child: perform the kernel's exact wild access */ rssadapt_lower_rate(ra, 64, 0); _exit(0); /* not reached if wild */ } int st = 0; waitpid(pid, &st, 0); sigaction(SIGSEGV, &osa, NULL); if (WIFEXITED(st) && WEXITSTATUS(st) == 42) printf(" -> WILD ACCESS confirmed: kernel equivalent " "is a ~+8GiB READ+WRITE from the rssadapt_node " "chunk -> kernel page fault -> PANIC\n"); else if (WIFSIGNALED(st)) printf(" -> WILD ACCESS confirmed (signal %d): kernel " "equivalent is a ~+8GiB access -> PANIC\n", WTERMSIG(st)); else printf(" -> no fault observed\n"); } printf("after tx-complete failure: ra_pktrate = 0x%08x " "(was 0x11223344) canaries %s\n", ra->ra_pktrate, canaries_intact(zone) ? "intact" : "CLOBBERED"); if (rix < 0) { printf("RESULT: BUG REPRODUCED โ rix=-1: in-struct rs_rates[-1] " "read, iwm KASSERT violation, and u_int-indexed " "~+8GiB wild read+write in rssadapt_lower_rate()\n"); return 0; } printf("RESULT: no negative-index effect observed (fix active)\n"); return 0; } |