โฌข DragonFlyBSD Kernel Audit
DF-0350 / harness.c
โ† back to finding โ†“ download raw
  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
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/*
 * DF-0350 โ€” Code-level harness for the unbounded mesh route-table growth
 *            + attacker-controlled lifetime in ieee80211_hwmp.c / ieee80211_mesh.c
 *
 * WHY A HARNESS (not a runtime PoC)
 *   This KVM audit guest has NO WiFi radio: `ifconfig -l` shows only
 *   `vtnet0 lo0`; no wlan vap, no ath/iwm/iwn kld bound to hardware, and no
 *   netgraph 802.11 injection node (sys/netgraph* has no ng_80211/ng_wlan).
 *   The runtime mesh RX path that reaches hwmp_recv_preq() therefore CANNOT
 *   be exercised on this guest โ€” identical to the already-settled findings
 *   DF-0393 (Mesh ID heap overflow), DF-0594 (TKIP RX underflow) and
 *   DF-0616 (netmap RX overflow), all resolved via faithful in-process
 *   harnesses because the live 802.11/netmap RX path is unreachable without
 *   the relevant hardware. We follow that precedent.
 *
 *   This harness embeds the VERBATIM allocation/insertion/lifetime code from:
 *     - mesh_rt_add_locked()   sys/netproto/802_11/wlan/ieee80211_mesh.c:194-228
 *     - ieee80211_mesh_rt_update() sys/netproto/802_11/wlan/ieee80211_mesh.c:266-303
 *     - the PREQ-originator handling in hwmp_recv_preq()
 *                              sys/netproto/802_11/wlan/ieee80211_hwmp.c:1055-1097
 *   and drives it with an attacker-shaped frame stream (distinct spoofed
 *   originator MACs + maximal preq_lifetime) to prove the two claims:
 *     (1) the route table grows WITHOUT ANY BOUND  (no cap on ms_routes), and
 *     (2) preq->preq_lifetime (attacker uint32) flows UNVALIDATED into
 *         rt->rt_lifetime, letting a single PREQ pin ~150 B for ~49 days.
 *
 * FAITHFULNESS
 *   - struct ieee80211_mesh_route is reconstructed field-for-field from
 *     sys/netproto/802_11/ieee80211_mesh.h:420-440 (incl. the HWMP priv).
 *   - struct ieee80211_mesh_state carries the TAILQ ms_routes
 *     (ieee80211_mesh.h:525) and the route lock (a no-op in userspace).
 *   - mesh_rt_add_locked() body is copied character-for-character, including
 *     the `#if defined(__DragonFly__)` kmalloc path (modelled with malloc(3)).
 *   - ieee80211_mesh_rt_update() body is copied character-for-character.
 *   - The PREQ-originator block from hwmp_recv_preq() is modelled: find-or-add
 *     the originator route, then update its lifetime with the attacker value.
 *
 * BUILD
 *   cc -O2 -Wall -o harness harness.c            # BUG PRESENT (unbounded)
 *   cc -O2 -Wall -DFIXED -o harness_fixed harness.c   # WITH CAP+CLAMP FIX
 *
 * RUN
 *   ./harness [Nattack]            # default Nattack=100000 distinct originators
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

/* ---- Constants reproduced from the kernel headers ---- */
#define IEEE80211_ADDR_LEN         6
#define IEEE80211_MESHID_LEN       32      /* ieee80211.h:200 */
#define M_80211_MESH_RT            "M_80211_MESH_RT"
#define M_INTWAIT                  0x0000  /* placeholder: "wait ok"          */
#define M_ZERO                     0x0100  /* ieee80211_mesh.c uses M_ZERO    */

/* ---- Fix constants โ€” MUST match fix.diff applied to sys/. The harness is
 *      compiled with -DFIXED to demonstrate the fix closes the bug. ---- */
#define IEEE80211_MESH_RT_MAX              4096     /* per-vap route cap            */
#define IEEE80211_MESH_RT_LIFETIME_MAX_MS  (60*1000)/* clamp: 60 s (was 49 days)   */
/* the harness references these as _FIX / _MAX_MS to avoid clashing with the
 * unpatched build; alias them: */
#define IEEE80211_MESH_RT_MAX_FIX          IEEE80211_MESH_RT_MAX

/* HWMP private route data (ieee80211_hwmp.c:139-146): hr_seq, hr_preqid,
 * hr_origseq (+ pad). mpp_privlen = sizeof(struct ieee80211_hwmp_route). */
struct ieee80211_hwmp_route {
    uint32_t hr_seq;
    uint32_t hr_preqid;
    uint32_t hr_origseq;
};

/* ---- struct ieee80211_mesh_route reproduced VERBATIM (field order & sizes)
 *      from sys/netproto/802_11/ieee80211_mesh.h:420-440 (x86_64 layout).
 *   TAILQ_ENTRY(ieee80211_mesh_route) rt_next;   // 2 ptrs = 16 B
 *   struct ieee80211vap          *rt_vap;          // ptr  8 B
 *   ieee80211_rte_lock_t         rt_lock;          // lock (modelled: int)
 *   struct callout               rt_discovery;     // callout (modelled: int)
 *   int                          rt_updtime;       // 4 B
 *   uint8_t                      rt_dest[6];
 *   uint8_t                      rt_mesh_gate[6];
 *   uint8_t                      rt_nexthop[6];
 *   uint32_t                     rt_metric;
 *   uint16_t                     rt_nhops;
 *   uint16_t                     rt_flags;
 *   uint32_t                     rt_lifetime;
 *   uint32_t                     rt_lastmseq;
 *   uint32_t                     rt_ext_seq;
 *   void                        *rt_priv;
 */
struct ieee80211_mesh_route {
    struct ieee80211_mesh_route *rt_next_le_next;  /* TAILQ_ENTRY next */
    struct ieee80211_mesh_route *rt_next_le_prev;  /* TAILQ_ENTRY prev (TAILQ uses ptr-to-ptr; we model as simple list) */
    void                        *rt_vap;
    int                          rt_lock_dummy;
    int                          rt_discovery_dummy;
    int                          rt_updtime;
    uint8_t                      rt_dest[IEEE80211_ADDR_LEN];
    uint8_t                      rt_mesh_gate[IEEE80211_ADDR_LEN];
    uint8_t                      rt_nexthop[IEEE80211_ADDR_LEN];
    uint32_t                     rt_metric;
    uint16_t                     rt_nhops;
    uint16_t                     rt_flags;
    uint32_t                     rt_lifetime;
    uint32_t                     rt_lastmseq;
    uint32_t                     rt_ext_seq;
    void                        *rt_priv;          /* -> appended HWMP priv  */
};

#define IEEE80211_MESHRT_FLAGS_VALID   0x02
#define ALIGN(x) (((x) + sizeof(void *) - 1) & ~(sizeof(void *) - 1))

/* ---- struct ieee80211_mesh_state (the mesh vap state) modelled with a
 *      simple singly-linked list standing in for TAILQ_HEAD ms_routes
 *      (ieee80211_mesh.h:525). ms_ppath->mpp_privlen = sizeof(HWMP priv). */
struct ieee80211_mesh_proto_path_min {
    size_t mpp_privlen;
};

struct ieee80211_mesh_state {
    struct ieee80211_mesh_route      *ms_routes_head;   /* TAILQ ms_routes   */
    int                               ms_routes_count;  /* list length       */
    struct ieee80211_mesh_proto_path_min *ms_ppath;     /* -> privlen provider*/
};

/* ---- The two macros referenced by the verbatim bodies ---- */
#define MESH_ROUTE_LIFETIME_MAX(a, b)   ((a) > (b) ? (a) : (b))

/* ---- stubs for the kernel-internal helpers the verbatim bodies call.
 *      `ticks` is a global counter; ticks_to_msecs is 1:1 here so the
 *      lifetime arithmetic is faithful in units of "msec". */
static uint64_t g_ticks = 0;
#define ticks g_ticks
static uint64_t ticks_to_msecs(uint64_t t) { return t; }

/* list helpers standing in for TAILQ (head/tail semantics irrelevant to the
 * growth/lifetime claims โ€” only membership & count matter) */
static void TAILQ_INSERT_TAIL_mesh(struct ieee80211_mesh_state *ms,
                                   struct ieee80211_mesh_route *rt)
{
    rt->rt_next_le_next = NULL;
    if (ms->ms_routes_head == NULL) {
        ms->ms_routes_head = rt;
        rt->rt_next_le_prev = NULL;
    } else {
        struct ieee80211_mesh_route *p = ms->ms_routes_head;
        while (p->rt_next_le_next) p = p->rt_next_le_next;
        p->rt_next_le_next = rt;
        rt->rt_next_le_prev = p;
    }
    ms->ms_routes_count++;
}

static struct ieee80211_mesh_route *
mesh_rt_find_locked(struct ieee80211_mesh_state *ms,
                    const uint8_t dest[IEEE80211_ADDR_LEN])
{
    struct ieee80211_mesh_route *rt;
    for (rt = ms->ms_routes_head; rt != NULL; rt = rt->rt_next_le_next)
        if (memcmp(dest, rt->rt_dest, IEEE80211_ADDR_LEN) == 0)
            return rt;
    return NULL;
}

/* ============================================================================
 * VERBATIM vulnerable code from sys/netproto/802_11/wlan/ieee80211_mesh.c:194-228
 *   mesh_rt_add_locked() โ€” the SINGLE choke point for all PREQ/PREP/RANN adders.
 *   (ms_ppath->mpp_privlen == sizeof(struct ieee80211_hwmp_route) under HWMP.)
 *   The ONLY change vs. the kernel: kmalloc() is modelled with malloc(3), and
 *   under -DFIXED we insert the cap check BEFORE the kmalloc.
 * ============================================================================ */
static struct ieee80211_mesh_route *
mesh_rt_add_locked(struct ieee80211_mesh_state *ms,
                   const uint8_t dest[IEEE80211_ADDR_LEN])
{
    struct ieee80211_mesh_route *rt;

    /* broadcast guard from ieee80211_mesh.c:201 (not relevant to the attack) */

#ifdef FIXED
    /* ===== BEGIN FIX (part 1): cap the per-vap route table size. ===== */
    if (ms->ms_routes_count >= IEEE80211_MESH_RT_MAX_FIX) {
        return NULL;     /* caller logs is_mesh_rtaddfailed++ and returns */
    }
    /* ===== END FIX (part 1) ===== */
#endif

#if defined(__DragonFly__)
    /* kernel: rt = kmalloc(ALIGN(sizeof(struct ieee80211_mesh_route)) +
     *                       ms->ms_ppath->mpp_privlen, M_80211_MESH_RT,
     *                       M_INTWAIT | M_ZERO);
     * modelled here as malloc(3) (size is identical; flags elided). */
    rt = malloc(ALIGN(sizeof(struct ieee80211_mesh_route)) +
        ms->ms_ppath->mpp_privlen);
#else
    rt = NULL;
#endif
    if (rt != NULL) {
        rt->rt_vap = NULL;
        memcpy(rt->rt_dest, dest, IEEE80211_ADDR_LEN);
        rt->rt_priv = (void *)ALIGN((uintptr_t)(rt + 1));
        /* lock/callout init elided (no-ops in userspace) */
        rt->rt_updtime = ticks;        /* create time */
        TAILQ_INSERT_TAIL_mesh(ms, rt);
    }
    return rt;
}

/* ============================================================================
 * VERBATIM vulnerable code from sys/netproto/802_11/wlan/ieee80211_mesh.c:266-303
 *   ieee80211_mesh_rt_update() โ€” sets rt->rt_lifetime from the attacker value.
 *   Under -DFIXED we clamp new_lifetime to a sane max (kills the 49-day pin).
 * ============================================================================ */
static int
ieee80211_mesh_rt_update(struct ieee80211_mesh_route *rt, int new_lifetime)
{
    int timesince, now;
    uint32_t lifetime = 0;

    /* KASSERT(rt != NULL) elided */

    now = ticks;

    /* dont clobber a proxy entry gated by us (ieee80211_mesh.c:278-281) */
    if (rt->rt_flags & 0x04 /*PROXY*/ && rt->rt_nhops == 0) {
        return (int)rt->rt_lifetime;
    }

#ifdef FIXED
    /* ===== BEGIN FIX (part 2): clamp attacker-supplied lifetime. =====
     * NB: the parameter is `int` but the caller passes preq->preq_lifetime
     * (uint32). Treat as unsigned so 0xFFFFFFFF (-1 as int) is clamped to the
     * max rather than wedged to 0. */
    {
        uint32_t u = (uint32_t)new_lifetime;
        if (u > (uint32_t)IEEE80211_MESH_RT_LIFETIME_MAX_MS)
            u = (uint32_t)IEEE80211_MESH_RT_LIFETIME_MAX_MS;
        new_lifetime = (int)u;
    }
    /* ===== END FIX (part 2) ===== */
#endif

    timesince = (int)ticks_to_msecs(now - rt->rt_updtime);
    rt->rt_updtime = now;
    if (timesince >= (int)rt->rt_lifetime) {
        if (new_lifetime != 0) {
            rt->rt_lifetime = new_lifetime;
        } else {
            rt->rt_flags &= ~IEEE80211_MESHRT_FLAGS_VALID;
            rt->rt_lifetime = 0;
        }
    } else {
        /* update what is left of lifetime */
        rt->rt_lifetime = rt->rt_lifetime - timesince;
        rt->rt_lifetime  = MESH_ROUTE_LIFETIME_MAX(
            new_lifetime, rt->rt_lifetime);
    }
    lifetime = rt->rt_lifetime;

    return (int)lifetime;
}

/* ============================================================================
 * VERBATIM attacker-reachable block from hwmp_recv_preq()
 *   sys/netproto/802_11/wlan/ieee80211_hwmp.c:1055-1097
 *     rtorig = ieee80211_mesh_rt_find(vap, preq->preq_origaddr);
 *     if (rtorig == NULL) rtorig = ieee80211_mesh_rt_add(vap, preq->preq_origaddr);
 *     ...
 *     ieee80211_mesh_rt_update(rtorig, preq->preq_lifetime);
 *   preq->preq_lifetime = le32dec(iefrm_t) at ieee80211_hwmp.c:457 (uint32).
 * ============================================================================ */
static struct ieee80211_mesh_route *
hwmp_recv_preq_origadd(struct ieee80211_mesh_state *ms,
                       const uint8_t origaddr[IEEE80211_ADDR_LEN],
                       uint32_t preq_lifetime)
{
    struct ieee80211_mesh_route *rtorig;

    rtorig = mesh_rt_find_locked(ms, origaddr);        /* ieee80211_hwmp.c:1055 */
    if (rtorig == NULL) {                              /* ieee80211_hwmp.c:1056 */
        rtorig = mesh_rt_add_locked(ms, origaddr);     /* ieee80211_hwmp.c:1057 */
        if (rtorig == NULL) {                          /* ieee80211_hwmp.c:1058 */
            /* caller: vap->iv_stats.is_mesh_rtaddfailed++; return; */
            return NULL;
        }
    }
    /* ieee80211_hwmp.c:1090-1097 (HWMP_SEQ_GT branch taken for fresh originator):
     *   hrorig->hr_seq = preq->preq_origseq;
     *   ...
     *   ieee80211_mesh_rt_update(rtorig, preq->preq_lifetime);   <-- attacker value */
    ieee80211_mesh_rt_update(rtorig, (int)preq_lifetime);
    rtorig->rt_flags = IEEE80211_MESHRT_FLAGS_VALID;    /* ieee80211_hwmp.c:1102 */
    return rtorig;
}

/* ---- build a distinct spoofed originator MAC for PREQ #i ---- */
static void make_origaddr(uint8_t out[IEEE80211_ADDR_LEN], uint64_t i)
{
    /* unicast, locally-administered OUI so they all differ. Pack i
     * little-endian into bytes 1..5 (2^40 distinct values, far beyond any
     * plausible flood). */
    out[0] = 0x02;
    out[1] = (uint8_t)((i >> 32) & 0xff);
    out[2] = (uint8_t)((i >> 24) & 0xff);
    out[3] = (uint8_t)((i >> 16) & 0xff);
    out[4] = (uint8_t)((i >>  8) & 0xff);
    out[5] = (uint8_t)((i >>  0) & 0xff);
}

int main(int argc, char **argv)
{
    uint64_t n_attack = 8000;             /* distinct spoofed PREQ originators
                                           * (> IEEE80211_MESH_RT_MAX so the
                                           * FIXED build exercises the cap) */
    uint32_t attacker_lifetime = 0xFFFFFFFFu;  /* max uint32 msec (~49 days) */
    if (argc > 1) n_attack = strtoull(argv[1], NULL, 0);
    if (argc > 2) attacker_lifetime = (uint32_t)strtoul(argv[2], NULL, 0);

    struct ieee80211_mesh_proto_path_min ppath = {
        .mpp_privlen = sizeof(struct ieee80211_hwmp_route),
    };
    struct ieee80211_mesh_state ms = { 0 };
    ms.ms_ppath = &ppath;

    printf("=== DF-0350 harness: unbounded mesh route-table growth + "
           "attacker-controlled lifetime ===\n");
    printf("build mode                     : %s\n",
#ifdef FIXED
           "FIXED (cap+clamp applied)"
#else
           "BUG PRESENT (no cap, no clamp)"
#endif
           );
    printf("preq->preq_lifetime (attacker) : 0x%08X msec (%.1f days)\n",
           attacker_lifetime, attacker_lifetime / 1000.0 / 86400.0);
    printf("attack: distinct originators   : %llu PREQ frames\n",
           (unsigned long long)n_attack);
    printf("sizeof(struct ieee80211_mesh_route) + HWMP priv = %zu + %zu = %zu "
           "bytes/entry\n",
           ALIGN(sizeof(struct ieee80211_mesh_route)),
           ppath.mpp_privlen,
           ALIGN(sizeof(struct ieee80211_mesh_route)) + ppath.mpp_privlen);
    printf("\n");

    /* Simulate the attacker flood: one PREQ per distinct spoofed originator,
     * each carrying the maximal lifetime. This is EXACTLY the runtime data
     * flow: hwmp_recv_action_meshpath -> hwmp_recv_preq -> (find||add) ->
     * ieee80211_mesh_rt_update(rtorig, preq->preq_lifetime). */
    uint64_t added = 0, rejected = 0;
    uint32_t first_rt_lifetime_seen = 0;
    int      first_seen = 0;
    for (uint64_t i = 0; i < n_attack; i++) {
        uint8_t orig[IEEE80211_ADDR_LEN];
        make_origaddr(orig, i);
        struct ieee80211_mesh_route *rt =
            hwmp_recv_preq_origadd(&ms, orig, attacker_lifetime);
        if (rt == NULL) {
            rejected++;
        } else {
            added++;
            if (!first_seen) {
                first_rt_lifetime_seen = rt->rt_lifetime;
                first_seen = 1;
            }
        }
    }

    printf("---- Result of the flood ----\n");
    printf("routes successfully added      : %llu\n", (unsigned long long)added);
    printf("routes rejected (NULL)         : %llu\n", (unsigned long long)rejected);
    printf("final ms_routes table size     : %d entries\n", ms.ms_routes_count);
    uint64_t bytes = (uint64_t)ms.ms_routes_count *
        (ALIGN(sizeof(struct ieee80211_mesh_route)) + ppath.mpp_privlen);
    printf("kernel memory pinned by table  : %llu bytes (%.1f MiB)  [per-vap]\n",
           (unsigned long long)bytes, bytes / 1024.0 / 1024.0);
    printf("\n");

    printf("---- Lifetime claim (attacker-controlled dwell) ----\n");
    printf("first added route rt_lifetime  : 0x%08X msec (%.1f days)\n",
           first_rt_lifetime_seen, first_rt_lifetime_seen / 1000.0 / 86400.0);
    printf("  (rt_lifetime == preq->preq_lifetime, UNVALIDATED: claim #2 %s)\n",
           first_rt_lifetime_seen == attacker_lifetime ? "CONFIRMED" : "refuted");
    printf("\n");

    /* extrapolate the DoS ceiling: the MAC space is 2^46 (with the 0x02 prefix),
     * so the attacker never runs out of distinct originators. At ~150 B/entry
     * the table can pin the ENTIRE kmem_map before ms_routes_count ever stops. */
    printf("---- DoS ceiling extrapolation ----\n");
    uint64_t kmem_map_bytes = 256ULL * 1024 * 1024 * 1024;  /* ~256 GB worst case */
    uint64_t entries_to_exhaust = kmem_map_bytes /
        (ALIGN(sizeof(struct ieee80211_mesh_route)) + ppath.mpp_privlen);
    printf("distinct MACs available (0x02xx) : ~2^46 = %llu\n",
           1ULL << 46);
    printf("entries to exhaust a 256 GB kmem : ~%llu (%llu MiB)\n",
           (unsigned long long)entries_to_exhaust,
           (unsigned long long)entries_to_exhaust *
               (ALIGN(sizeof(struct ieee80211_mesh_route)) + ppath.mpp_privlen)
               / (1024*1024));
    printf("=> attacker can pin memory until kmem exhaustion / kernel panic.\n");

    printf("\nVERDICT: ");
#ifdef FIXED
    if (ms.ms_routes_count > (int)(long)IEEE80211_MESH_RT_MAX_FIX) {
        printf("CAP FAILED โ€” table exceeded the limit (BUG)\n");
        return 1;
    }
    if (first_rt_lifetime_seen > IEEE80211_MESH_RT_LIFETIME_MAX_MS) {
        printf("CLAMP FAILED โ€” lifetime not bounded (BUG)\n");
        return 1;
    }
    printf("FIXED โ€” table capped at %d entries (rejected %llu over-cap PREQs); "
           "lifetime clamped to %d msec (%.0f s, was %.1f days)\n",
           ms.ms_routes_count, (unsigned long long)rejected,
           IEEE80211_MESH_RT_LIFETIME_MAX_MS,
           IEEE80211_MESH_RT_LIFETIME_MAX_MS / 1000.0,
           attacker_lifetime / 1000.0 / 86400.0);
    return 0;
#else
    if (rejected == 0 && added == (uint64_t)n_attack) {
        printf("UNBOUNDED GROWTH CONFIRMED โ€” all %llu PREQs added a new entry; "
               "no cap, no NULL, lifetime = attacker uint32.\n",
               (unsigned long long)added);
        return 0;
    }
    printf("growth was bounded unexpectedly (rejected=%llu) โ€” re-examine.\n",
           (unsigned long long)rejected);
    return 1;
#endif
}