DF-0746 / 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 | /* * DF-0746 โ Deterministic transcription of the use-after-free READ in * l2cap_rtx(): req->lr_id is read AFTER zfree(l2cap_req_pool, req). * * USERSPACE HARNESS (not a kernel PoC). The runtime netbt L2CAP path * (BTPROTO_L2CAP socket + RTX timeout) is unreachable on this KVM guest: * - There is no Bluetooth radio. * - `BLUETOOTH` is `optional bluetooth` (sys/conf/files:1614) and is NOT * listed in sys/config/X86_64_GENERIC, so l2cap_misc.c is not compiled * into the default kernel (only into the unloadable /boot/kernel/netbt.ko). * - In the shipped module, `BLUETOOTH_DEBUG` is NOT defined * (sys/netbt/Makefile has no `-DBLUETOOTH_DEBUG`), so DPRINTF expands to * ((void)0) (sys/netbt/bluetooth.h:145) and req->lr_id is never actually * evaluated at runtime on a production box. * The bug is real at the source level (the DPRINTF dereferences req->lr_id * after zfree) and WOULD fire in any BLUETOOTH_DEBUG build. The harness * transcribes the exact l2cap_rtx body and a faithful vm_zone zalloc/zfree * model so the UAF READ is observable deterministically, the same precedent * as DF-0745 (the sibling double-free finding on the same function) and the * DF-0393/0594/0616/0732/0733 wifi/netgraph/bt-unreachable cluster. * * --------------------------------------------------------------------------- * THE BUG (transcribed exactly from sys/netbt/l2cap_misc.c): * * 163 void * 164 l2cap_request_free(struct l2cap_req *req) * 165 { * 166 struct hci_link *link = req->lr_link; * 167 * 168 callout_stop(&req->lr_rtx); * 169 if (callout_active(&req->lr_rtx)) // dead guard (DF-0745); always falls through * 170 return; * 171 * 172 TAILQ_REMOVE(&link->hl_reqs, req, lr_next); * 173 zfree(l2cap_req_pool, req); // <--- req is freed HERE * 174 } * * 183 void * 184 l2cap_rtx(void *arg) * 185 { * 186 struct l2cap_req *req = arg; * 187 struct l2cap_channel *chan; * 188 * 189 chan = req->lr_chan; // chan saved BEFORE free (safe) * 190 l2cap_request_free(req); // <--- calls zfree above * 191 * 192 DPRINTF("cid %d, ident %d\n", (chan ? chan->lc_lcid : 0), req->lr_id); * // ^^^^^^^^ USE-AFTER-FREE READ of req->lr_id * 193 * 194 if (chan && chan->lc_state != L2CAP_CLOSED) * 195 l2cap_close(chan, ETIMEDOUT); * 196 } * * --------------------------------------------------------------------------- * WHAT THE HARNESS PROVES: * * (1) zfree(l2cap_req_pool, req) writes ONLY offset 0 (freelist link, * vm_zone.c:233 `((void **)item)[0] = zpcpu->zitems`) and, under * INVARIANTS, offset 8 (ZENTRY_FREE magic, vm_zone.c:237). It does NOT * touch offset 16 (lr_code) or offset 17 (lr_id). So the immediate * stale read of req->lr_id returns the ORIGINAL id โ a benign value * the caller itself set in l2cap_request_alloc (l2cap_misc.c:129). * * (2) HOWEVER the access is unambiguously against FREED memory: if any * same-CPU zalloc from l2cap_req_pool happens between the zfree and the * DPRINTF (in this single-threaded callout path that cannot happen, * but it is the latent risk the bug creates), the read returns * whatever bytes the new owner wrote at offset 17. The harness models * that reuse step explicitly to demonstrate the read is of freed * memory that may be (re)shaped by an attacker-influenced allocation. * * (3) The fix is to capture lr_id BEFORE the free and use the local in the * DPRINTF. harness_fixed.c transcribes the fix and proves the read no * longer touches req after free. * * --------------------------------------------------------------------------- * MODEL FIDELITY: * - struct l2cap_req layout from sys/netbt/l2cap.h:423-430 (lr_link, lr_chan, * lr_code, lr_id, lr_rtx, lr_next). On amd64 the offsets are * 0=lr_link, 8=lr_chan, 16=lr_code, 17=lr_id. * - vm_zone zalloc/zfree transcribed from sys/vm/vm_zone.c:87-120 / :211-244 * (LIFO per-zone freelist; zfree writes item[0]=link, item[1]=ZENTRY_FREE * under INVARIANTS; zalloc pops item[0]). * - DPRINTF is modeled as an unconditional kprintf (the BLUETOOTH_DEBUG form, * sys/netbt/bluetooth.h:132-135) so the read is actually performed; on a * production kernel DPRINTF is ((void)0) and the read is never emitted, * which is why the runtime impact on the default kernel is "none". * * Build: cc -O2 -Wall -Wextra -o harness harness.c * Run: ./harness * Expected (BUG PRESENT): * "UAF READ CONFIRMED: req->lr_id dereferenced after zfree" * " stale-or-reused value at offset 17: 0xNN" * exit 0 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> /* ---------------------------------------------------------------------- * Faithful vm_zone model (sys/vm/vm_zone.c:87-120 / :211-244) * LIFO freelist, item[0]=next, item[1]=ZENTRY_FREE under INVARIANTS. * -------------------------------------------------------------------- */ #define ZENTRY_FREE 0x12342378u /* vm_zone.c:76 */ #define INVARIANTS /* model a GENERIC build */ struct zone { void *zitems; /* LIFO head */ size_t zsize; const char *zname; }; static inline void * zalloc(struct zone *z) { void *item = z->zitems; if (item) { z->zitems = *(void **)item; /* vm_zone.c:104 pop */ return item; } return malloc(z->zsize); /* back the zone with malloc */ } static inline void zfree(struct zone *z, void *item) { *(void **)item = z->zitems; /* vm_zone.c:233 item[0]=link */ #ifdef INVARIANTS if (((void **)item)[1] == (void *)(uintptr_t)ZENTRY_FREE) fprintf(stderr, " [INVARIANTS] zerror(ZONE_ERROR_ALREADYFREE)\n"); ((void **)item)[1] = (void *)(uintptr_t)ZENTRY_FREE; /* vm_zone.c:237 */ #endif z->zitems = item; /* vm_zone.c:239 push */ } /* ---------------------------------------------------------------------- * struct l2cap_req (sys/netbt/l2cap.h:423-430) * On amd64: 0=lr_link(8) 8=lr_chan(8) 16=lr_code(1) 17=lr_id(1) * 18..23 pad, then lr_rtx (struct callout), then lr_next. * For the harness we only need the head layout (offsets 0..17); the rest is * modeled as opaque bytes so the struct is the same size class as in the * kernel. * -------------------------------------------------------------------- */ struct l2cap_req { void *lr_link; /* offset 0 */ void *lr_chan; /* offset 8 */ uint8_t lr_code; /* offset 16 */ uint8_t lr_id; /* offset 17 <-- the field read after free */ uint8_t pad[6]; /* 18..23 */ uint8_t lr_rtx_dummy[64]; /* stand-in for struct callout */ /* TAILQ_ENTRY follows in the kernel; size-only here */ uint8_t lr_tail[16]; }; static struct zone l2cap_req_pool = { .zitems = NULL, .zsize = sizeof(struct l2cap_req), .zname = "l2cap_req" }; /* ---------------------------------------------------------------------- * DPRINTF in BLUETOOTH_DEBUG form (sys/netbt/bluetooth.h:132-135). * In production this is ((void)0) and req->lr_id is never read; we model the * debug form so the UAF is observable. * -------------------------------------------------------------------- */ #define DPRINTF(fmt, ...) \ do { if (bt_debug) printf("%s: " fmt, "l2cap_rtx", ##__VA_ARGS__); } while (0) static int bt_debug = 1; /* Global used to detect that req->lr_id was actually dereferenced after free */ static int g_uaf_read_observed = 0; static uint8_t g_uaf_read_value = 0; /* ---------------------------------------------------------------------- * l2cap_request_free (sys/netbt/l2cap_misc.c:163-174) โ transcribed verbatim * -------------------------------------------------------------------- */ struct hci_link { int dummy; } fake_link; static void l2cap_request_free(struct l2cap_req *req) { /* callout_stop + dead callout_active guard elided (see DF-0745): always * falls through to TAILQ_REMOVE + zfree. */ (void)fake_link; zfree(&l2cap_req_pool, req); /* l2cap_misc.c:173 */ } /* ---------------------------------------------------------------------- * l2cap_rtx (sys/netbt/l2cap_misc.c:183-197) โ transcribed verbatim * * NB: in the kernel, req is freed at line 190 then read at line 192. * We instrument the read so it is unambiguously observable. * -------------------------------------------------------------------- */ static void l2cap_rtx(void *arg) { struct l2cap_req *req = arg; struct l2cap_channel { uint16_t lc_lcid; int lc_state; } *chan; /* min model */ chan = (void *)req->lr_chan; /* l2cap_misc.c:189 (safe) */ l2cap_request_free(req); /* l2cap_misc.c:190 (FREES) */ /* l2cap_misc.c:192 โ DPRINTF reads req->lr_id AFTER free. * We mark the address as freed first, then perform the exact deref. */ { volatile uint8_t *p = &req->lr_id; g_uaf_read_observed = 1; g_uaf_read_value = *p; /* THE UAF READ */ DPRINTF("cid %d, ident %d\n", (chan ? (int)chan->lc_lcid : 0), (int)g_uaf_read_value); } if (chan && ((int *)(void *)chan)[1] != 0) (void)0; /* l2cap_close stub */ } /* ---------------------------------------------------------------------- * Test 1: stale-but-original read (no intervening reuse). * zfree leaves offset 17 untouched, so the value read is the original id. * -------------------------------------------------------------------- */ static void test_stale_read(void) { printf("== Test 1: stale read of req->lr_id immediately after zfree ==\n"); struct l2cap_req *req = zalloc(&l2cap_req_pool); memset(req, 0, sizeof(*req)); req->lr_link = &fake_link; req->lr_code = 0x0a; /* L2CAP_CONNECT_REQ etc. */ req->lr_id = 0x42; /* the "original" id */ g_uaf_read_observed = 0; g_uaf_read_value = 0; l2cap_rtx(req); printf(" g_uaf_read_observed = %d\n", g_uaf_read_observed); printf(" g_uaf_read_value = 0x%02x (original was 0x42)\n", (unsigned)g_uaf_read_value); if (g_uaf_read_observed && g_uaf_read_value == 0x42) { printf("UAF READ CONFIRMED: req->lr_id dereferenced after zfree\n"); printf(" stale value at offset 17 == original id (no slab reuse yet)\n"); } else { printf("UNEXPECTED: uaf_observed=%d value=0x%02x\n", g_uaf_read_observed, (unsigned)g_uaf_read_value); exit(2); } printf("\n"); } /* ---------------------------------------------------------------------- * Test 2: same-CPU slab reuse between zfree and DPRINTF. * Model an intervening zalloc (which hands back the just-freed slot), * let the new owner scribble an attacker-chosen byte at offset 17, * then perform the DPRINTF read. This proves the read is of memory that * an attacker-influenced allocation can reshape โ the latent risk. * (In the actual l2cap_rtx path no allocation sits between zfree and DPRINTF, * but the bug is that the deref is of freed memory that ANY later allocator * action can repurpose.) * -------------------------------------------------------------------- */ static void test_reuse_read(void) { printf("== Test 2: read after same-CPU slab reuse (latent risk) ==\n"); /* prime the pool so zfree pushes one entry, then a new zalloc pops it */ struct l2cap_req *victim = zalloc(&l2cap_req_pool); memset(victim, 0, sizeof(*victim)); victim->lr_link = &fake_link; victim->lr_id = 0x11; /* free it back into the LIFO */ zfree(&l2cap_req_pool, victim); /* a different consumer grabs the same slot and writes attacker bytes */ struct l2cap_req *reused = zalloc(&l2cap_req_pool); if (reused != victim) { printf("UNEXPECTED: pool did not hand back the same slot\n"); exit(2); } memset(reused, 0xCC, sizeof(*reused)); /* attacker-shaped */ reused->lr_id = 0xDD; /* now model the DPRINTF read that l2cap_rtx performs on the ORIGINAL * pointer after its zfree โ the original pointer aliases the reused slot */ volatile uint8_t *p = &victim->lr_id; uint8_t got = *p; printf(" read of freed req->lr_id returned 0x%02x (reused slot wrote 0xDD)\n", (unsigned)got); if (got == 0xDD) { printf("UAF READ CONFIRMED: freed req->lr_id returns attacker-shaped byte\n"); } else { printf("UNEXPECTED: got 0x%02x\n", (unsigned)got); exit(2); } printf("\n"); } int main(void) { printf("DF-0746 harness: UAF read of req->lr_id in l2cap_rtx after zfree\n"); printf("transcribed from sys/netbt/l2cap_misc.c:163-197\n"); printf("vm_zone model from sys/vm/vm_zone.c:87-244\n\n"); test_stale_read(); test_reuse_read(); printf("== Summary ==\n"); printf("BUG: l2cap_rtx reads req->lr_id AFTER zfree(l2cap_req_pool, req)\n"); printf(" at sys/netbt/l2cap_misc.c:192 (free at :173 via :190).\n"); printf("RUNTIME IMPACT on default X86_64_GENERIC kernel: NONE โ\n"); printf(" netbt is `optional bluetooth` (not in GENERIC), and even in\n"); printf(" the netbt.ko module DPRINTF is ((void)0) without -DBLUETOOTH_DEBUG,\n"); printf(" so the deref is never emitted in production. Latent hardening\n"); printf(" bug for BLUETOOTH_DEBUG builds.\n"); return 0; } |