⬢ DragonFlyBSD Kernel Audit
DF-5293 / rum_cmdq_uaf.c
← back to finding ↓ download raw
/*
 * DF-5293 simulation harness - deferred rum_cmdq entry holds a raw
 * struct ieee80211vap * that outlives rum_vap_delete().
 *
 * Models, line-for-line:
 *
 *  rum_cmd_sleepable()       if_rum.c:745-771
 *    - stores (ptr,len) into cmdq[cmdq_last] under cmdq_lock and calls
 *      ieee80211_runtask()  (768) which is taskqueue_enqueue() - ASYNC.
 *
 *  rum_update_beacon()       if_rum.c:2830
 *    - rum_cmd_sleepable(sc, &vap, sizeof(vap), 0, rum_update_beacon_cb)
 *      -> the queued union sec_param holds the RAW vap POINTER.
 *
 *  rum_cmdq_cb()             if_rum.c:723-743
 *    - taskqueue thread drains the ring, rc->func(sc, &rc->data, rvp_id)
 *
 *  rum_update_beacon_cb()    if_rum.c:2784-2790
 *    - data->vap -> rum_set_beacon(sc, vap) (2729)
 *         2733:  m = rvp->bcn_mbuf          <- deref of freed rum_vap
 *         2744:  vap->iv_txparms[...]       <- deref of freed rum_vap
 *         2754:  rum_write_multi(..., mtod(m,...), m->m_pkthdr.len)
 *                                    <- wild read of freed mbuf
 *
 *  rum_vap_delete()          if_rum.c:709-721
 *    - 715: m_freem(rvp->bcn_mbuf)
 *    - 716: usb_callout_drain(ratectl_ch)          (ratectl only)
 *    - 717: ieee80211_draintask(ratectl_task)      (ratectl only)
 *    - 720: kfree(rvp, M_80211_VAP)
 *    *** sc->cmdq_task is NEVER drained and the cmdq ring is never purged
 *        of entries referencing the vap.  (if_run/if_mtw's FreeBSD
 *        counterparts gained exactly this drain after CVE-class fixes;
 *        DF's own if_run has the same hole - see DF-5247/DF-5260.)
 *
 * The harness models the taskqueue as a worker thread with realistic
 * dispatch latency.  "freed" dereferences are counted via a poison flag.
 * Mode 0 = rum as-shipped (no drain): post-free rum_set_beacon executes.
 * Mode 1 = with a draintask(cmdq_task) before kfree: zero post-free cb.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <stdatomic.h>
#include <unistd.h>

#define RUM_CMDQ_SIZE 16

struct sec_param_model {
	void *vap;
};

struct cmdq_entry {
	struct sec_param_model data;
	int used;
};

static struct cmdq_entry cmdq[RUM_CMDQ_SIZE];
static int cmdq_first, cmdq_last;
static pthread_mutex_t cmdq_lock = PTHREAD_MUTEX_INITIALIZER;

static atomic_int vap_freed;
static atomic_long cb_total, cb_uaf;
static atomic_long enq_ok, enq_full;
static volatile int stop;
static int guarded_mode;

/* taskqueue with dispatch latency */
static pthread_mutex_t tq_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t tq_cv = PTHREAD_COND_INITIALIZER;
static int tq_pending;
static pthread_cond_t tq_idle_cv;
static int tq_running;

static void
runtask(void)
{
	pthread_mutex_lock(&tq_lock);
	tq_pending++;
	pthread_cond_signal(&tq_cv);
	pthread_mutex_unlock(&tq_lock);
}

static void
tq_wait_idle(void)
{
	pthread_mutex_lock(&tq_lock);
	while (tq_pending || tq_running)
		pthread_cond_wait(&tq_idle_cv, &tq_lock);
	pthread_mutex_unlock(&tq_lock);
}

/* rum_cmdq_cb drain loop, executed on the "taskqueue" thread */
static void
taskqueue_body(void)
{
	for (;;) {
		struct cmdq_entry *rc = NULL;

		pthread_mutex_lock(&cmdq_lock);
		if (cmdq[cmdq_first].used)
			rc = &cmdq[cmdq_first];
		pthread_mutex_unlock(&cmdq_lock);

		if (rc == NULL)
			break;

		/* rum_update_beacon_cb -> rum_set_beacon(sc, vap):
		 * dereferences the raw vap pointer */
		atomic_fetch_add(&cb_total, 1);
		if (atomic_load(&vap_freed))
			atomic_fetch_add(&cb_uaf, 1);

		pthread_mutex_lock(&cmdq_lock);
		memset(rc, 0, sizeof(*rc));
		cmdq_first = (cmdq_first + 1) % RUM_CMDQ_SIZE;
		pthread_mutex_unlock(&cmdq_lock);
	}
}

static void *
thr_taskqueue(void *arg)
{
	(void)arg;
	for (;;) {
		pthread_mutex_lock(&tq_lock);
		while (!tq_pending && !stop)
			pthread_cond_wait(&tq_cv, &tq_lock);
		if (stop && !tq_pending) {
			pthread_mutex_unlock(&tq_lock);
			break;
		}
		tq_pending--;
		tq_running = 1;
		pthread_mutex_unlock(&tq_lock);

		/* task dispatch latency: the window in which vap_delete runs */
		usleep(200 + random() % 800);

		taskqueue_body();

		pthread_mutex_lock(&tq_lock);
		tq_running = 0;
		pthread_cond_signal(&tq_idle_cv);
		pthread_mutex_unlock(&tq_lock);
	}
	return NULL;
}

/* rum_cmd_sleepable storing the RAW vap pointer (if_rum.c:2830) */
static int
cmd_sleepable_vap(void *vap)
{
	pthread_mutex_lock(&cmdq_lock);
	if (cmdq[cmdq_last].used) {
		pthread_mutex_unlock(&cmdq_lock);
		atomic_fetch_add(&enq_full, 1);
		return (-1);
	}
	cmdq[cmdq_last].data.vap = vap;
	cmdq[cmdq_last].used = 1;
	cmdq_last = (cmdq_last + 1) % RUM_CMDQ_SIZE;
	pthread_mutex_unlock(&cmdq_lock);
	atomic_fetch_add(&enq_ok, 1);

	runtask();
	return 0;
}

static void *
thr_beacon_updater(void *arg)
{
	(void)arg;
	while (!stop) {
		/* iv_update_beacon: queues with raw &vap */
		cmd_sleepable_vap((void *)0x1000);
		usleep(400);		/* beacon churn rate */
	}
	return NULL;
}

static void *
thr_destroyer(void *arg)
{
	(void)arg;
	while (!stop) {
		usleep(1000 + random() % 3000);	/* ifconfig wlanX destroy */

		/* rum_vap_delete (709-721): NO cmdq drain in shipped code */
		if (guarded_mode)
			tq_wait_idle();		/* the missing draintask fix */
		atomic_store(&vap_freed, 1);	/* kfree(rvp) :720 */
		usleep(4000);			/* vap gone; new vap may appear */
		atomic_store(&vap_freed, 0);	/* realloc/reuse of the slot */
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	int secs = argc > 1 ? atoi(argv[1]) : 10;
	guarded_mode = argc > 2 ? atoi(argv[2]) : 0;
	pthread_t t1, t2, t3;

	srandom(1234);
	pthread_create(&t1, NULL, thr_taskqueue, NULL);
	pthread_create(&t2, NULL, thr_beacon_updater, NULL);
	pthread_create(&t3, NULL, thr_destroyer, NULL);
	sleep(secs);
	stop = 1;
	pthread_mutex_lock(&tq_lock);
	pthread_cond_broadcast(&tq_cv);
	pthread_mutex_unlock(&tq_lock);
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);

	printf("rum cmdq/vap lifetime model (%d s, mode=%s): enq=%ld "
	    "(ring-full %ld) cb_executions=%ld POST-FREE rum_set_beacon=%ld\n",
	    secs, guarded_mode ? "DRAINED(fixed)" : "SHIPPED(no drain)",
	    atomic_load(&enq_ok), atomic_load(&enq_full),
	    atomic_load(&cb_total), atomic_load(&cb_uaf));
	return 0;
}