โฌข DragonFlyBSD Kernel Audit
DF-0732 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0732 โ€” deterministic TOCTOU harness for ieee80211_acl acl_getioctl MACCMD_LIST
 *
 * Transcribes sys/netproto/802_11/wlan_acl/ieee80211_acl.c:299-343 verbatim,
 * with a CONTROLLED INTERLEAVING POINT between the unlocked `as_nacls` read
 * (line 313) and the `ACL_LOCK` acquisition (line 328). The race is
 * deterministic when the attacker controls both threads; this harness models
 * exactly that, so it is the primary proof of the bug (the runtime path needs
 * a wifi vap on a wifi radio, absent on this KVM guest โ€” see VERDICT.md).
 *
 * Build BUGGY transcription (matches current source):
 *     cc -O2 -pthread -o harness harness.c
 * Build FIXED transcription (lock-before-read + M_ZERO + bounded foreach):
 *     cc -O2 -pthread -DFIXED -o harness_fixed harness.c
 *
 * Modes:
 *     ./harness grow    N K    # list grows by K between read and lock
 *     ./harness shrink  N K    # list shrinks by K between read and lock
 *
 * On the BUGGY build:
 *   grow   -> "GROW RACE OOB WRITE CONFIRMED" (K MACs written past kmalloc end)
 *   shrink -> "SHRINK RACE UNINIT LEAK CONFIRMED" (K tail MACs are uninit canary)
 * On the FIXED build both races are eliminated (built with -DFIXED).
 */

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

#define IEEE80211_ADDR_LEN 6        /* ieee80211.h:35 */
#define CANARY       0xAA           /* poisoned-allocator fill (models uninit heap) */
#define MAX_REDZONE  4096

/* ---- faithful transcription of the data structures ---- */

struct acl {                        /* ieee80211_acl.c:74-78 */
    struct acl   *acl_next;         /* TAILQ link (singly-linked is enough) */
    uint8_t       acl_macaddr[IEEE80211_ADDR_LEN];
};

struct ieee80211req_maclist {       /* ieee80211_ioctl.h:329-331 */
    uint8_t       ml_macaddr[IEEE80211_ADDR_LEN];
} __attribute__((packed));

struct aclstate {                   /* ieee80211_acl.c:79-86 */
    pthread_mutex_t as_lock;        /* acl_lock_t  (ieee80211_dragonfly.h:568) */
    uint32_t         as_nacls;
    struct acl      *as_list_head;  /* TAILQ_HEAD simplified */
    struct acl     **as_list_ptail; /* tail-insert pointer */
};

static struct aclstate AS;

#define ACL_LOCK(as)   pthread_mutex_lock(&(as)->as_lock)
#define ACL_UNLOCK(as) pthread_mutex_unlock(&(as)->as_lock)

/* TAILQ_FOREACH equivalent for our singly-linked list */
#define TAILQ_FOREACH_equiv(var) \
    for (var = (AS.as_list_head); var != NULL; var = var->acl_next)

/* ---- controlled-interleaving state (buggy build only) ---- */
static volatile int g_size_read_done = 0;   /* victim set after unlocked read :313 */
static volatile int g_racer_done     = 0;   /* racer set after mutating list */
static int  g_mode;                          /* 0=grow, 1=shrink */
static int  g_K;                             /* entries to add/remove in the window */

/* attacker-controlled, recognizable MAC bytes (so OOB write is observable) */
static void
make_attacker_mac(uint8_t *m, int seq)
{
    m[0] = 0xDE; m[1] = 0xAD; m[2] = 0xBE; m[3] = 0xEF;
    m[4] = (uint8_t)((seq >> 8) & 0xff);
    m[5] = (uint8_t)(seq & 0xff);
}

/* ---- transcribed writers (acl_add / _acl_free) โ€” all under ACL_LOCK ---- */

static void
acl_add_locked(uint8_t mac[IEEE80211_ADDR_LEN])
{
    /* transcribes ieee80211_acl.c:179-220 (lock taken by caller here) */
    struct acl *n = (struct acl *)calloc(1, sizeof(struct acl));
    assert(n);
    memcpy(n->acl_macaddr, mac, IEEE80211_ADDR_LEN);
    n->acl_next = NULL;
    if (AS.as_list_ptail != NULL) *AS.as_list_ptail = n;
    else                          AS.as_list_head = n;
    AS.as_list_ptail = &n->acl_next;
    AS.as_nacls++;                                    /* :214 */
}

static void
acl_remove_first_locked(void)
{
    /* transcribes _acl_free (ieee80211_acl.c:150-159) on the head */
    struct acl *n = AS.as_list_head;
    if (n == NULL) return;
    AS.as_list_head = n->acl_next;
    if (AS.as_list_head == NULL) AS.as_list_ptail = &AS.as_list_head;
    free(n);
    AS.as_nacls--;                                    /* :158 */
}

#ifndef FIXED
/* ---- RACER thread: runs ADDMAC or DELMAC inside the TOCTOU window ---- */
static void *
racer(void *arg)
{
    (void)arg;
    /* spin-wait until victim has completed the UNLOCKED size read (:313) */
    while (!g_size_read_done)
        ;

    ACL_LOCK(&AS);
    if (g_mode == 0) {                                /* GROW: ADDMAC x K */
        for (int k = 0; k < g_K; k++) {
            uint8_t mac[IEEE80211_ADDR_LEN];
            make_attacker_mac(mac, 9000 + k);         /* recognizable OOB bytes */
            acl_add_locked(mac);
        }
    } else {                                          /* SHRINK: DELMAC x K */
        for (int k = 0; k < g_K; k++)
            acl_remove_first_locked();
    }
    ACL_UNLOCK(&AS);

    g_racer_done = 1;
    return NULL;
}
#endif

/* ---- allocator wrapper around the victim's kmalloc(:319) ----
 * Always allocates space + REDZONE and poisons ALL of it with CANARY. The
 * region [space .. space+REDZONE) is the OOB red zone the grow race writes
 * into; the region [0 .. space) is the legit buffer whose tail the shrink
 * race leaves uninitialized. In the FIXED build we additionally zero the
 * legit buffer [0..space) to model M_ZERO (the redzone stays canary so the
 * GROW OOB detection remains meaningful in both builds).
 */
static uint8_t *
harness_alloc(uint32_t space, uint32_t redzone)
{
    uint8_t *p = (uint8_t *)malloc(space + redzone);
    assert(p);
    memset(p, CANARY, space + redzone);   /* poison redzone + buffer */
#ifdef FIXED
    memset(p, 0, space);                  /* M_ZERO on the legit buffer only */
#endif
    return p;
}

/* ---- VICTIM: faithful transcription of acl_getioctl MACCMD_LIST (:312-340) ----
 * Returns the malloc'd buffer (base) and reports counts via outparams.
 */
static uint8_t *
victim_maccmd_list(uint32_t *out_space, uint32_t *out_i_final, uint32_t *out_oob)
{
    struct acl *acl;
    uint32_t i, space;
    struct ieee80211req_maclist *ap;
    uint32_t redzone = (uint32_t)g_K * IEEE80211_ADDR_LEN;
    if (redzone > MAX_REDZONE) redzone = MAX_REDZONE;

    /* ieee80211_acl.c:313 */
#ifdef FIXED
    /* ---- FIXED: lock BEFORE reading the size (:313) ---- */
    ACL_LOCK(&AS);
#endif
    space = AS.as_nacls * IEEE80211_ADDR_LEN;         /* :313 (UNLOCKED in buggy) */

#ifndef FIXED
    /* signal the racer to mutate the list right here, between :313 and :328 */
    g_size_read_done = 1;
    while (!g_racer_done)
        ;                        /* wait for racer's ADDMAC/DELMAC to land */
#endif

    /* ieee80211_acl.c:319-320  kmalloc(space, M_TEMP, M_INTWAIT)  -- no M_ZERO */
    ap = (struct ieee80211req_maclist *)harness_alloc(space, redzone);
    if (ap == NULL) { *out_space = space; *out_i_final = 0; *out_oob = 0; return NULL; }

    i = 0;                                            /* :327 */
#ifndef FIXED
    ACL_LOCK(&AS);                                    /* :328 */
#endif
    /* ieee80211_acl.c:329-332 */
#ifdef FIXED
    /* ---- FIXED: bound the write loop to space/ADDR_LEN entries ---- */
    {
        uint32_t bound = space / IEEE80211_ADDR_LEN;
        TAILQ_FOREACH_equiv(acl) {
            if (i >= bound) break;                    /* defense-in-depth */
            memcpy(ap[i].ml_macaddr, acl->acl_macaddr, IEEE80211_ADDR_LEN);
            i++;
        }
    }
#else
    TAILQ_FOREACH_equiv(acl) {
        memcpy(ap[i].ml_macaddr, acl->acl_macaddr, IEEE80211_ADDR_LEN);  /* :330 */
        i++;
    }
#endif
    ACL_UNLOCK(&AS);                                  /* :333 */

    *out_space = space;
    *out_i_final = i;
    *out_oob = (i * IEEE80211_ADDR_LEN > space) ? 1 : 0;
    return (uint8_t *)ap;
}

/* ---- setup: seed the list with N baseline entries ---- */
static void
seed_list(int N)
{
    ACL_LOCK(&AS);
    for (int k = 0; k < N; k++) {
        uint8_t mac[IEEE80211_ADDR_LEN];
        make_attacker_mac(mac, 1000 + k);
        acl_add_locked(mac);
    }
    ACL_UNLOCK(&AS);
}

static int
run_scenario(int mode, int N, int K)
{
    /* reset global state */
    pthread_mutex_destroy(&AS.as_lock);
    memset(&AS, 0, sizeof(AS));
    pthread_mutex_init(&AS.as_lock, NULL);
    AS.as_list_head = NULL;
    AS.as_list_ptail = &AS.as_list_head;
    AS.as_nacls = 0;
    g_size_read_done = 0;
    g_racer_done = 0;
    g_mode = mode;
    g_K = K;

    seed_list(N);

#ifndef FIXED
    pthread_t tr;
    pthread_create(&tr, NULL, racer, NULL);
#endif

    uint32_t space = 0, i_final = 0, oob = 0;
    uint8_t *ap = victim_maccmd_list(&space, &i_final, &oob);

#ifndef FIXED
    pthread_join(tr, NULL);
#endif

    if (ap == NULL) {
        printf("  [harness] kmalloc returned NULL (ENOMEM) โ€” scenario inconclusive\n");
        return 1;
    }

    uint32_t allocated_bytes = space;
    uint32_t written_bytes   = i_final * IEEE80211_ADDR_LEN;
    uint32_t redzone_bytes   = (uint32_t)K * IEEE80211_ADDR_LEN;
    if (redzone_bytes > MAX_REDZONE) redzone_bytes = MAX_REDZONE;

    printf("  [harness] N=%d K=%d  as_nacls@read=%u  space=%u bytes  list@foreach had %u entries\n",
           N, K, space / IEEE80211_ADDR_LEN, allocated_bytes, i_final);
    printf("  [harness] foreach wrote %u bytes into %u-byte buffer (oob_flag=%u)\n",
           written_bytes, allocated_bytes, oob);

    int verdict = 0;
    if (mode == 0) {
        /* GROW: expect ap[i] for i in [N .. N+K) to be OOB writes past `space`.
         * Check the red zone [allocated_bytes .. allocated_bytes+redzone_bytes). */
        int oob_hits = 0;
        for (uint32_t b = allocated_bytes; b < allocated_bytes + redzone_bytes; b++) {
            uint8_t v = ap[b];
            if (v != CANARY) {
                oob_hits++;
            }
        }
        if (oob_hits > 0) {
            printf("  [harness] GROW: %u OOB bytes overwritten past kmalloc end "
                   "(attacker MAC bytes landed in red zone)\n", oob_hits);
            printf("GROW RACE OOB WRITE CONFIRMED\n");
            verdict = 1;
        } else {
            printf("  [harness] GROW: red zone intact โ€” no OOB write\n");
            printf("GROW RACE: NOT TRIGGERED (no OOB write)\n");
        }
    } else {
        /* SHRINK: list has N-K entries at foreach; tail K slots [N-K..N) were
         * never written. With kmalloc(no M_ZERO) they hold heap residue (canary).
         * copyout would ship them to userland = info leak. */
        int uninit_hits = 0;
        uint32_t first_uninit_slot = (uint32_t)(N - K);
        for (uint32_t s = first_uninit_slot; s < (uint32_t)N; s++) {
            uint8_t *slot = ap + s * IEEE80211_ADDR_LEN;
            int all_canary = 1;
            for (int b = 0; b < IEEE80211_ADDR_LEN; b++)
                if (slot[b] != CANARY) { all_canary = 0; break; }
            if (all_canary) uninit_hits++;
        }
        if (uninit_hits > 0) {
            printf("  [harness] SHRINK: %u tail slots uninitialized (heap residue "
                   "would be shipped by copyout = info leak)\n", uninit_hits);
            printf("SHRINK RACE UNINIT LEAK CONFIRMED\n");
            verdict = 1;
        } else {
            printf("  [harness] SHRINK: all tail slots zeroed/valid โ€” no uninit leak\n");
            printf("SHRINK RACE: NOT TRIGGERED (no uninit tail)\n");
        }
    }

    free(ap);
    /* drain remaining list */
    ACL_LOCK(&AS);
    while (AS.as_list_head) acl_remove_first_locked();
    ACL_UNLOCK(&AS);
    return verdict;
}

int
main(int argc, char **argv)
{
    if (argc < 4) {
        fprintf(stderr,
            "usage: %s <grow|shrink> <N> <K>\n"
            "  N = baseline list size (as_nacls at unlocked read)\n"
            "  K = entries added(grow)/removed(shrink) in the TOCTOU window\n",
            argv[0]);
        return 2;
    }
    const char *mode_s = argv[1];
    int N = atoi(argv[2]);
    int K = atoi(argv[3]);
    int mode;
    if (strcmp(mode_s, "grow") == 0)      mode = 0;
    else if (strcmp(mode_s, "shrink") == 0) mode = 1;
    else { fprintf(stderr, "mode must be 'grow' or 'shrink'\n"); return 2; }

    if (N <= 0 || K <= 0) { fprintf(stderr, "N and K must be > 0\n"); return 2; }
    if (mode == 1 && K > N) { fprintf(stderr, "shrink: K must be <= N\n"); return 2; }

#ifdef FIXED
    printf("=== DF-0732 harness (FIXED transcription: lock-before-read + M_ZERO + bounded foreach) ===\n");
#else
    printf("=== DF-0732 harness (BUGGY transcription: matches ieee80211_acl.c current source) ===\n");
#endif
    printf("  mode=%s N=%d K=%d  IEEE80211_ADDR_LEN=%d\n\n",
           mode_s, N, K, IEEE80211_ADDR_LEN);

    int rc = run_scenario(mode, N, K);
    printf("\n[exit rc=%d]\n", rc);
    return rc;
}