DragonFlyBSD Kernel Audit
DF-0730 / df0730_harness.c
← back to finding ↓ download raw
/*
 * DF-0730 — ra_rate_thresh[3][8] heap OOB read/write proof (code-level harness)
 * ---------------------------------------------------------------------------
 *
 * The DragonFlyBSD master `wlan_rssadapt` rate-adaptation module
 * (sys/netproto/802_11/wlan/ieee80211_rssadapt.c) has a fixed-size array
 * dimension that is smaller than the maximum index used to access it:
 *
 *   sys/netproto/802_11/ieee80211_rssadapt.h:65-66
 *       uint16_t  ra_rate_thresh[IEEE80211_RSSADAPT_BKTS=3]
 *                                 [IEEE80211_RATE_SIZE=8];
 *
 *   sys/netproto/802_11/_ieee80211.h:374-375
 *       #define IEEE80211_RATE_SIZE     8        // 802.11 standard
 *       #define IEEE80211_RATE_MAXSIZE  15       // max rates we'll handle
 *       struct ieee80211_rateset { uint8_t rs_nrates; uint8_t rs_rates[15]; };
 *
 * `rs_nrates` can legitimately reach up to 15 (ieee80211_setup_rates,
 * ieee80211_input.c:413-439, merges Supported(<=8) + XRATES(<=7), capped at
 * MAXSIZE=15).  rssadapt_node_init (:198) copies that rateset verbatim, so
 * ra->ra_rates.rs_nrates can be up to 15.
 *
 * Then:
 *   rssadapt_rate     :253   for (rix = rs->rs_nrates-1; rix >= 0; rix--)
 *                          :254       if ((*thrs)[rix] < (rssi << 8)) break;
 *   rssadapt_lower_rate:285  (*thrs)[rix]   = interpolate(...)   // WRITE
 *   rssadapt_raise_rate:310  (*thrs)[rix+1] = interpolate(...)   // WRITE
 *
 * where `thrs = &ra->ra_rate_thresh[bucket(pktlen)]` and bucket()=2 for
 * pktlen > 1024 (IEEE80211_RSSADAPT_BKT0 * 2^BKTPOWER = 128*8 = 1024).
 *
 * For bucket=2 and rix=14, (*thrs)[14] = ra->ra_rate_thresh[2][14], which is
 * 6 uint16_t (12 bytes) PAST the end of ra_rate_thresh[2][7] = past the end
 * of the struct -> heap OOB WRITE.
 *
 * This harness replicates the EXACT struct layout and the rssadapt_lower_rate
 * write logic, allocates the struct inside a canaried region, and demonstrates
 * the OOB write corrupts memory past the struct.  It is parameterized by
 * THRESH_COLS so the same harness proves the bug when built with the buggy
 * dimension (8) and proves the fix when built with IEEE80211_RATE_MAXSIZE (15).
 *
 * This is a code-level proof because the bug is LATENT on this guest:
 *   - no WiFi radio (ifconfig -l => vtnet0 lo0)
 *   - wlan_rssadapt is `optional wlan_rssadapt` (sys/conf/files:1655), NOT in
 *     the default X86_64_GENERIC kernel
 *   - NO in-tree driver calls ieee80211_ratectl_set(vap, RSSADAPT); the
 *     default ratectl is AMRR (ieee80211_ratectl.c:122)
 * So it cannot be triggered live here; the harness is the deterministic proof.
 */

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

/* --- Mirror sys/netproto/802_11/_ieee80211.h:374-379 --------------------- */
#define IEEE80211_RATE_SIZE        8    /* 802.11 standard (array dim)       */
#define IEEE80211_RATE_MAXSIZE    15    /* max rates we'll handle            */

struct ieee80211_rateset {
    uint8_t  rs_nrates;
    uint8_t  rs_rates[IEEE80211_RATE_MAXSIZE];
};

/* --- Mirror sys/netproto/802_11/ieee80211_rssadapt.h:41-67 ---------------- */
#define IEEE80211_RSSADAPT_BKTS    3
#define IEEE80211_RSSADAPT_BKT0  128
#define IEEE80211_RSSADAPT_BKTPOWER 3

/*
 * THRESH_COLS is the second dimension of ra_rate_thresh.
 * Default = IEEE80211_RATE_SIZE (8) = the BUGGY value in master.
 * Build with -DTHRESH_COLS=IEEE80211_RATE_MAXSIZE to model the fix.
 */
#ifndef THRESH_COLS
#define THRESH_COLS  IEEE80211_RATE_SIZE
#endif

struct ieee80211_rssadapt_node {
    void   *ra_rs;                          /* backpointer (pointer-sized) */
    struct ieee80211_rateset ra_rates;      /* negotiated 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;
    /* THE BUG: dimension is IEEE80211_RATE_SIZE(8) but indexed up to 14.   */
    uint16_t ra_rate_thresh[IEEE80211_RSSADAPT_BKTS][THRESH_COLS];
};

/* --- Mirror ieee80211_rssadapt.c:220-233 (bucket()) ---------------------- */
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;
}

/* --- Mirror ieee80211_rssadapt.c:274-292 (rssadapt_lower_rate) ----------- *
 * Only the WRITE matters for this proof; we use a fixed rix and a fake
 * interpolate result so the corruption is unambiguous.                       */
static uint16_t fake_new_thr;
static void
rssadapt_lower_rate_write(struct ieee80211_rssadapt_node *ra, int pktlen, int rix)
{
    uint16_t (*thrs)[THRESH_COLS] = &ra->ra_rate_thresh[bucket(pktlen)];
    /* (*thrs)[rix] = interpolate(...);  -- the OOB WRITE                    */
    (*thrs)[rix] = fake_new_thr;
}

int
main(void)
{
    /* Layout sanity prints (tie harness to kernel source). */
    printf("THRESH_COLS (ra_rate_thresh 2nd dim) = %d\n", (int)THRESH_COLS);
    printf("IEEE80211_RATE_SIZE    = %d\n", (int)IEEE80211_RATE_SIZE);
    printf("IEEE80211_RATE_MAXSIZE = %d\n", (int)IEEE80211_RATE_MAXSIZE);
    printf("IEEE80211_RSSADAPT_BKTS= %d\n", (int)IEEE80211_RSSADAPT_BKTS);
    printf("sizeof(struct ieee80211_rssadapt_node) = %zu\n",
           sizeof(struct ieee80211_rssadapt_node));
    printf("offsetof(ra_rate_thresh)               = %zu\n",
           offsetof(struct ieee80211_rssadapt_node, ra_rate_thresh));
    printf("sizeof(ra_rate_thresh[0])              = %zu\n",
           sizeof(((struct ieee80211_rssadapt_node *)0)->ra_rate_thresh[0]));
    printf("\n");

    /* The realistic trigger: a standard 11g AP advertises 12 rates. */
    const uint8_t nrates = 12;            /* rs_nrates, attacker/peer-driven */
    int max_rix = nrates - 1;             /* = 11 (could be 14 with 15 rates) */

    /* bucket=2 means pktlen > 1024 (the worst bucket: spills past struct). */
    int pktlen = 1500;                    /* typical MTU frame               */
    int b = bucket(pktlen);
    printf("Trigger: rs_nrates=%u, max rix=%d, pktlen=%d -> bucket=%d\n",
           nrates, max_rix, pktlen, b);

    /* Worst-case index across all valid nrates (<=15). */
    int worst_rix = IEEE80211_RATE_MAXSIZE - 1;   /* 14 */
    printf("Worst-case rix (rs_nrates=15) = %d\n", worst_rix);

    /* Place the struct inside a canaried allocation so an OOB write past its
     * end is detected deterministically. */
    enum { CANARY = 64 };
    unsigned char region[sizeof(struct ieee80211_rssadapt_node) + CANARY];
    memset(region, 0xAA, sizeof(region));
    struct ieee80211_rssadapt_node *ra =
        (struct ieee80211_rssadapt_node *)region;

    fake_new_thr = 0xDEAD;
    /* Simulate rssadapt_lower_rate writing at the worst-case index for the
     * worst bucket.  In the kernel this is (*thrs)[ra->ra_rix]; ra_rix comes
     * from rssadapt_rate which left it at the loop's last value. */
    rssadapt_lower_rate_write(ra, pktlen, worst_rix);

    size_t struct_end = sizeof(struct ieee80211_rssadapt_node);
    /* where did the write land, relative to region start? */
    size_t thresh_base = offsetof(struct ieee80211_rssadapt_node, ra_rate_thresh);
    size_t row_size    = sizeof(uint16_t) * THRESH_COLS;
    size_t write_off   = thresh_base + (size_t)b * row_size
                                    + (size_t)worst_rix * sizeof(uint16_t);

    printf("\nWrite landed at offset %zu within the allocation.\n", write_off);
    printf("struct end is at offset %zu.\n", struct_end);

    int oob = (write_off + sizeof(uint16_t)) > struct_end;
    int canary_clobbered = 0;
    for (size_t i = struct_end; i < struct_end + CANARY; i++) {
        if (region[i] != 0xAA) {
            canary_clobbered = 1;
            break;
        }
    }

    printf("\n=== VERDICT ===\n");
    if (oob) {
        printf("OUT-OF-BOUNDS: write at off %zu is %zu byte(s) PAST the struct "
               "(struct ends at %zu).\n",
               write_off, (write_off + sizeof(uint16_t)) - struct_end, struct_end);
        printf("Canary clobbered past struct end: %s\n",
               canary_clobbered ? "YES (heap OOB WRITE confirmed)" : "no");
        printf("BUG PRESENT: ra_rate_thresh[3][%d] indexed by rix=%d -> OOB.\n",
               (int)THRESH_COLS, worst_rix);
        return 1;   /* non-zero => bug demonstrated */
    } else {
        printf("IN-BOUNDS: write at off %zu is within the struct (ends %zu). "
               "No OOB.\n", write_off, struct_end);
        printf("Canary intact: %s\n", canary_clobbered ? "NO (unexpected)" : "YES");
        printf("BUG FIXED: ra_rate_thresh[3][%d] safely holds rix=%d.\n",
               (int)THRESH_COLS, worst_rix);
        return 0;   /* zero => no bug */
    }
}