โฌข DragonFlyBSD Kernel Audit
DF-0981 / run_aggr_oob.c
โ† back to finding โ†“ download raw
/*
 * DF-0981 โ€” proof-of-concept harness for the heap OOB write in
 *            run_bulk_rx_callback() aggregated-frame path.
 *
 * The vulnerable path lives in sys/bus/u4b/wlan/if_run.c and is only
 * reachable when a run(4) (Ralink RT2770/RT2870/RT3070/RT3370/...)
 * USB WiFi adapter is present and a bulk RX URB completes.  The audit
 * guest (DragonFly 6.5-DEVELOPMENT #0 in KVM) has NO USB controller and
 * NO run(4) device, so the live path CANNOT be triggered here.
 *
 * This harness is a faithful userspace re-implementation of the EXACT
 * frame-aggregation / dmalen / m_copydata logic of run_bulk_rx_callback()
 * (if_run.c:2985-3031) plus the blind bcopy() of m_copydata()
 * (uipc_mbuf.c:1671-1696).  It demonstrates that, given a bulk URB whose
 * device-controlled per-frame DMA length exceeds MCLBYTES (2048), the
 * "before" code path overflows the destination cluster, while the patched
 * path (m_getjcl(MJUMPAGESIZE) + explicit bound) does not.
 *
 * Build:  cc -O2 -Wall -o run_aggr_oob run_aggr_oob.c
 * Run:    ./run_aggr_oob            # prints BEFORE/OVERFLOW and AFTER/OK
 *
 * This is a code-level proof.  See VERDICT.md for the line-by-line trace.
 */

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

/* ---- constants mirrored verbatim from the kernel ---- */
#define MCLBYTES        2048            /* sys/sys/param.h:497          */
#define MJUMPAGESIZE    4096            /* sys/sys/param.h:473          */
#define RUN_MAX_RXSZ    4096            /* if_runvar.h:26  MIN(4096,4K) */
#define RT2870_RXD_SIZE 4               /* if_runreg.h: struct rt2870_rxd is one uint32_t, __packed */
#define DMA_LEN_HDR     4               /* leading 32-bit per-frame DMA-len field */

/* guard pattern so we can detect an overrun */
#define GUARD_BYTE      0xCD
#define GUARD_SIZE      256

/* ---- m_copydata() replica: sys/kern/uipc_mbuf.c:1671-1696 ----
 * Blind bcopy() of `len` bytes into _cp with NO destination-bound check.
 * Here `m` is the URB buffer and we copy from `off`. */
static uint32_t le32toh_sim(uint32_t v) { return v; }   /* host is little-endian x86_64 */

static void m_copydata(const uint8_t *m_data, int off, int len, void *_cp)
{
    /* faithful: no bounds check on the destination */
    memcpy(_cp, m_data + off, len);
}

/*
 * Replicate the per-cluster allocation.  cluster_size is MCLBYTES (before)
 * or MJUMPAGESIZE (after).  We allocate cluster_size + a tail guard region
 * so the harness can observe whether the copy wrote past the legal cluster.
 */
static uint8_t *alloc_cluster(int cluster_size, uint8_t **guard_out)
{
    uint8_t *buf = calloc(1, cluster_size + GUARD_SIZE);
    if (!buf) { perror("calloc"); exit(1); }
    memset(buf + cluster_size, GUARD_BYTE, GUARD_SIZE);
    *guard_out = buf + cluster_size;
    return buf;
}

static int guard_touched(const uint8_t *guard)
{
    for (int i = 0; i < GUARD_SIZE; i++)
        if (guard[i] != GUARD_BYTE)
            return 1;
    return 0;
}

/*
 * Faithful re-implementation of the aggregation loop in
 * run_bulk_rx_callback() (if_run.c:2985-3031) for a SINGLE aggregated
 * frame.  Returns the number of overflow bytes written past the cluster
 * (0 if none), or -1 if the path was not taken.
 *
 * cluster_size: MCLBYTES (vulnerable) or MJUMPAGESIZE (fixed)
 * also_bound:   if nonzero, also enforce dmalen + RT2870_RXD_SIZE <= cluster_size
 */
static long simulate_aggr(const uint8_t *urb, int xferlen,
                          int cluster_size, int also_bound)
{
    uint32_t dmalen;
    long overflow = -1;     /* path not taken */

    /* mirror the loop body for the FIRST frame */
    dmalen = le32toh_sim(*(const uint32_t *)urb) & 0xffff;

    /* if_run.c:2991-2995 */
    if ((dmalen >= (uint32_t)-8) || (dmalen == 0) || ((dmalen & 3) != 0))
        return -1;
    /* if_run.c:2996-3001 */
    if ((dmalen + 8) > (uint32_t)xferlen)
        return -1;

    int remain = xferlen - (dmalen + 8);   /* if_run.c:3003 LHS */
    if (remain <= 8) {
        /* single-frame path โ€” does not overflow (uses sc->rx_m which is jumbo) */
        return -2;   /* signal: aggregated branch NOT taken */
    }

    /* aggregated-frame branch โ€” THIS is the bug. */
    if (also_bound && dmalen + RT2870_RXD_SIZE > (uint32_t)cluster_size) {
        /* defense-in-depth: fixed path rejects oversize frame */
        return -3;   /* signal: rejected by fix */
    }

    uint8_t *guard;
    uint8_t *dest = alloc_cluster(cluster_size, &guard);

    /* if_run.c:3023-3024 โ€” the OOB write */
    long copy_len = (long)dmalen + RT2870_RXD_SIZE;
    m_copydata(urb, DMA_LEN_HDR, copy_len, dest);

    overflow = guard_touched(guard) ? (copy_len - cluster_size) : 0;
    if (overflow < 0) overflow = 0;   /* guard not reached but copy fit */

    free(dest);
    return overflow;
}

int main(void)
{
    /* Craft the malicious bulk URB exactly as the finding describes:
     *   bytes  0..3   = dmalen (3000, multiple of 4)
     *   bytes  4..3003 = forged rxwi + frame body (3000 bytes)
     *   bytes  3004..3007 = forged rt2870_rxd (flags=0)
     *   bytes  3008..4095 = second dummy frame header so xferlen-(dmalen+8) > 8
     * Total xferlen = RUN_MAX_RXSZ = 4096.
     */
    static uint8_t urb[RUN_MAX_RXSZ];
    memset(urb, 0xA1, sizeof(urb));

    uint32_t dmalen = 3000;                       /* multiple of 4, > MCLBYTES */
    memcpy(urb + 0, &dmalen, sizeof(dmalen));     /* little-endian on x86       */
    /* rt2870_rxd at offset dmalen+4 with flags=0 (already zeroed) */
    /* trailing frame: leave non-zero so remain>8 forces the aggregated branch */

    int xferlen = RUN_MAX_RXSZ;

    printf("DF-0981 run_bulk_rx_callback aggregated-frame OOB harness\n");
    printf("xferlen        = %d (RUN_MAX_RXSZ)\n", xferlen);
    printf("dmalen         = %u (device-controlled, > MCLBYTES)\n", dmalen);
    printf("copy length    = %lu (dmalen + sizeof(rt2870_rxd)=%d)\n",
           (unsigned long)dmalen + RT2870_RXD_SIZE, RT2870_RXD_SIZE);
    printf("MCLBYTES       = %d   (m_getcl cluster โ€” VULNERABLE alloc)\n", MCLBYTES);
    printf("MJUMPAGESIZE   = %d   (m_getjcl cluster โ€” FIXED alloc)\n", MJUMPAGESIZE);
    printf("\n");

    /* ---- BEFORE: vulnerable m_getcl path ---- */
    long ov = simulate_aggr(urb, xferlen, MCLBYTES, /*also_bound=*/0);
    printf("[BEFORE] m_getcl() (cluster=%d), no extra bound:\n", MCLBYTES);
    if (ov == -2) {
        printf("  aggregated branch NOT taken (single-frame path) โ€” recheck harness\n");
    } else if (ov < 0) {
        printf("  path rejected unexpectedly (rc=%ld)\n", ov);
    } else if (ov > 0) {
        printf("  *** HEAP OOB WRITE CONFIRMED: %ld bytes past the %d-byte cluster ***\n",
               ov, MCLBYTES);
        printf("  -> matches if_run.c:3023-3024 m_copydata() overflow\n");
    } else {
        printf("  no overflow (unexpected)\n");
    }

    /* ---- AFTER: fixed m_getjcl(MJUMPAGESIZE) path ---- */
    ov = simulate_aggr(urb, xferlen, MJUMPAGESIZE, /*also_bound=*/0);
    printf("\n[AFTER] m_getjcl(MJUMPAGESIZE) (cluster=%d):\n", MJUMPAGESIZE);
    if (ov == -2) {
        printf("  aggregated branch NOT taken (single-frame path)\n");
    } else if (ov < 0) {
        printf("  path rejected (rc=%ld)\n", ov);
    } else if (ov > 0) {
        printf("  overflow STILL present: %ld bytes (fix insufficient)\n", ov);
    } else {
        printf("  OK: copy of %lu bytes fits in %d-byte cluster โ€” no overflow\n",
               (unsigned long)dmalen + RT2870_RXD_SIZE, MJUMPAGESIZE);
    }

    /* ---- AFTER2: defense-in-depth bound (reject oversize) ---- */
    ov = simulate_aggr(urb, xferlen, MCLBYTES, /*also_bound=*/1);
    printf("\n[AFTER2] defense-in-depth bound (reject dmalen+rxd > cluster):\n");
    if (ov == -3) {
        printf("  OK: oversize frame REJECTED before the copy โ€” no overflow\n");
    } else {
        printf("  bound did not reject (rc=%ld) โ€” unexpected\n", ov);
    }

    printf("\nverdict: the vulnerable m_getcl() path overflows; the patched ");
    printf("m_getjcl()+bound path does not.\n");
    return 0;
}