⬢ DragonFlyBSD Kernel Audit
DF-5249 / cmdq_race.c
← back to finding ↓ download raw
/*
 * DF-5249 - Simulated reproduction of the if_run(4) run_cmdq machinery races
 * ==========================================================================
 *
 * Kernel code modeled (sys/bus/u4b/wlan/if_run.c):
 *
 *  - sc->cmdq[16] ring, slot picked locklessly by
 *        RUN_CMDQ_GET(c)  (atomic_fetchadd_32(c,1) & 15)      if_run.c:92
 *  - Producers store fields with plain stores, no lock, e.g.
 *        run_key_set:     cmdq[i].func = run_key_set_cb;      if_run.c:2370
 *                         cmdq[i].arg0 = NULL;                if_run.c:2371
 *                         cmdq[i].arg1 = vap;                 if_run.c:2372
 *                         cmdq[i].k = k;                      if_run.c:2373
 *        run_update_beacon: cmdq[i].func = run_update_beacon_cb;
 *                           cmdq[i].arg0 = vap;               if_run.c:4896-4897
 *        run_key_delete / run_newassoc / run_updateprot /
 *        run_updateslot / run_tx(fifo) / tx-timeout cb        (same pattern)
 *    followed by ieee80211_runtask() -> taskqueue_enqueue.
 *  - Single consumer run_cmdq_cb() runs under RUN_LOCK:       if_run.c:1041-1068
 *        for (i = cmdq_exec; cmdq[i].func && pending;
 *             i = cmdq_exec, pending--) {
 *            if (cmdq_run == GO) {
 *                if (cmdq[i].arg0) cmdq[i].func(cmdq[i].arg0);
 *                else              cmdq[i].func(&cmdq[i]);  <- expects a
 *            }                                                  fully written
 *            cmdq[i].arg0 = NULL; cmdq[i].func = NULL;          slot
 *            cmdq_exec++ &= 15;
 *        }
 *
 * Two failure modes simulated:
 *
 *  (1) TORN SLOT: producer preempted between storing .func and .arg0 (or a
 *      wrap-overwrite happening between the consumer's func/arg0 loads);
 *      consumer sees func != NULL with arg0 == NULL -> kernel equivalent:
 *      run_update_beacon_cb(NULL) dereferences vap = NULL immediately.
 *      Or func_old called with arg0_new: run_key_set_cb(vap) treats the vap
 *      as struct run_cmdq* and loads cmdq->k from vap memory -> wild
 *      pointer deref (type confusion).
 *
 *  (2) SLOT WRAP: more than 16 submissions outstanding overwrite
 *      not-yet-executed entries (RUN_CMDQ_GET wraps the 16-slot ring while
 *      the consumer is behind) -> a deferred command is silently replaced
 *      by another one; e.g. a lost run_key_delete leaves a stale hardware
 *      key installed.
 *
 * The consumer below is driven exactly like run_cmdq_cb, with `pending`
 * being the taskqueue coalesced enqueue count.
 *
 * build: cc -O2 -pthread -o cmdq_race cmdq_race.c
 * run:   ./cmdq_race [seconds]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

#define RUN_CMDQ_MAX 16
#define RUN_CMDQ_MASQ (RUN_CMDQ_MAX - 1)

#define TAG_BEACON 0x11
#define TAG_KEYSET 0x22

struct run_cmdq {
    void *arg0;
    void *arg1;
    void (*func)(void *);
    void *k;
};

static struct run_cmdq cmdq[RUN_CMDQ_MAX];
static volatile unsigned cmdq_store;
static volatile int cmdq_exec;
static volatile int pending;
static volatile int stop;

static long torn_null_hits;
static long type_confusion_hits;
static long wrap_overwrites;
static long executed;

static void cb_beacon(void *arg) { (void)arg; }
static void cb_keyset(void *arg) { (void)arg; }

static void *producer(void *arg)
{
    long id = (long)arg;
    while (!stop) {
        unsigned slot = __sync_fetch_and_add(&cmdq_store, 1) & RUN_CMDQ_MASQ;
        if (cmdq[slot].func != NULL)
            __sync_fetch_and_add(&wrap_overwrites, 1);
        cmdq[slot].func = (id & 1) ? cb_keyset : cb_beacon;
        __sync_synchronize();
        cmdq[slot].arg0 = (void *)((id & 1) ? (long)TAG_KEYSET : (long)TAG_BEACON);
        cmdq[slot].k = cmdq[slot].arg0;
        __sync_fetch_and_add(&pending, 1);      /* ieee80211_runtask() */
    }
    return NULL;
}

static void *consumer(void *arg)
{
    (void)arg;
    for (;;) {
        int p, i;
        while ((p = pending) == 0) {
            if (stop) return NULL;
            usleep(100);
        }
        p = __sync_fetch_and_add(&pending, -pending);  /* coalesced count */

        i = cmdq_exec;
        while (cmdq[i].func && p) {
            void (*f)(void *) = cmdq[i].func;
            void *a = cmdq[i].arg0;
            if (a == NULL)
                __sync_fetch_and_add(&torn_null_hits, 1);
            else if ((f == cb_keyset) != (a == (void *)TAG_KEYSET))
                __sync_fetch_and_add(&type_confusion_hits, 1);
            f(a);
            cmdq[i].arg0 = NULL;
            cmdq[i].func = NULL;
            cmdq_exec = (cmdq_exec + 1) & RUN_CMDQ_MASQ;
            __sync_fetch_and_add(&executed, 1);
            p--;
            i = cmdq_exec;
        }
    }
}

int main(int argc, char **argv)
{
    int secs = (argc > 1) ? atoi(argv[1]) : 5;
    pthread_t p1, p2, c;

    pthread_create(&p1, NULL, producer, (void *)0);
    pthread_create(&p2, NULL, producer, (void *)1);
    pthread_create(&c, NULL, consumer, NULL);

    sleep(secs);
    stop = 1;
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    pthread_join(c, NULL);

    printf("DF-5249 cmdq machinery race simulation over %ds\n", secs);
    printf("  executed callbacks   : %ld\n", executed);
    printf("  torn NULL-arg calls  : %ld  (kernel: run_update_beacon_cb(NULL) = NULL deref)\n",
        torn_null_hits);
    printf("  identity mismatches  : %ld  (kernel: func() run with another cmd's args)\n",
        type_confusion_hits);
    printf("  slot wrap overwrites : %ld  (kernel: deferred cmd lost/replaced)\n",
        wrap_overwrites);
    return 0;
}