โฌข DragonFlyBSD Kernel Audit
DF-3459 / df3459_harness.c
โ† back to finding โ†“ download raw
/*
 * DF-3459 โ€” rssadapt_rate() returns/stores rix = -1: negative-index accesses
 * ==========================================================================
 *
 * Target: sys/netproto/802_11/wlan/ieee80211_rssadapt.c (wlan_rssadapt module)
 *
 * The rate-pick loop in rssadapt_rate() (ieee80211_rssadapt.c:253-255)
 *
 *     for (rix = rs->rs_nrates-1; rix >= 0; rix--)
 *             if ((*thrs)[rix] < (rssi << 8))
 *                     break;
 *
 * falls out of the loop with rix == -1 whenever NO rate row has a threshold
 * STRICTLY below rssi<<8.  Two field-reachable ways to get there:
 *
 *   (a) rssi == 0.  node_getrssi() (ieee80211_node.c:1146-1155) returns 0
 *       for every node whose ni_avgrssi is still IEEE80211_RSSI_DUMMY_MARKER
 *       (the never-received-a-frame init state) and clamps negatives to 0.
 *       With all thresholds zeroed (fresh M_ZERO allocation) the test
 *       `0 < (0<<8)` is false for every row  ->  rix = -1 deterministically.
 *   (b) threshold convergence: rssadapt_lower_rate() raises row thresholds
 *       toward rssi<<8 with an integer weighted average that can converge to
 *       exactly rssi<<8; the comparison is strict `<`, so once every row
 *       sits at >= the current rssi<<8 (e.g. station moving away, signal
 *       dropping), the loop again exhausts -> rix = -1.
 *
 * With rix == -1 the code then executes:
 *
 *   ieee80211_rssadapt.c:258
 *       ni->ni_txrate = ni->ni_rates.rs_rates[rix] & IEEE80211_RATE_VAL;
 *   -> rs_rates[-1]  (reads the rs_nrates byte: in-struct, wrong value)
 *   ieee80211_rssadapt.c:259
 *       ra->ra_rix = rix;   -> ra_rix = -1 stored
 *   returns -1 to every caller of ieee80211_ratectl_rate() โ€” e.g.
 *   if_iwm.c:3540/3574 `int rix = ieee80211_ratectl_rate(...); iwm_setrates(sc, in, rix);`
 *   and if_iwm.c:4366  KASSERT(rix >= 0 && rix < nrates, ("invalid rix"));
 *   -> guaranteed panic on INVARIANTS kernels, silent bogus config otherwise.
 *
 * Later, a failed TX completion runs rssadapt_tx_complete() ->
 * rssadapt_lower_rate() with ra->ra_rix == -1:
 *
 *   ieee80211_rssadapt.c:279   u_int rix;              <- UNSIGNED, as shipped
 *   ieee80211_rssadapt.c:283   rix = ra->ra_rix;       -> 0xffffffff
 *   ieee80211_rssadapt.c:284   last_thr = (*thrs)[rix];      READ
 *   ieee80211_rssadapt.c:285   (*thrs)[rix] = interpolate(...); WRITE
 *
 *   With rix = 0xffffffff, (*thrs)[rix] addresses
 *       thrs + 2 * 0xffffffff  =  chunk + 0x1fffffffe  (~ +8 GiB)
 *   i.e. a WILD 2-byte read followed by a WILD 2-byte write at a fixed
 *   +8GiB offset from the per-node chunk.  In the kernel that is a page
 *   fault on unmapped KVA -> guaranteed panic (and on machines whose
 *   direct map covers chunk+8GiB, a real 2-byte wild heap corruption
 *   with a semi-attacker-controlled value via interpolate(last_thr,
 *   rssi<<8)).
 *
 *   (rssadapt_raise_rate() at line 299 uses a SIGNED int rix; there
 *   (*thrs)[-1] is the 2 bytes before the row โ€” contained.)
 *
 * This is the NEGATIVE-side residual of DF-0730 (which fixed the HIGH-side
 * bound rs_nrates<=15 vs [8] columns).  DF-0730's fix.diff resizes the array
 * but leaves rix == -1 untouched.
 *
 * ------------------------------------------------------------------------
 * Harness: replicates the exact struct layout (with DF-0730's resize already
 * applied: 15 columns, so the only negative-index effect demonstrated here is
 * THIS finding's) and the exact loop/access logic, inside a canaried region.
 *
 *   default build : demonstrates rix=-1 -> rs_rates[-1], ra_rix=-1,
 *                   iwm_setrates KASSERT condition violation, and the
 *                   ra_rate_thresh[0][-1] write aliasing ra_pktrate.
 *   -DFIXED build : applies this finding's fix.diff logic (clamp rix to 0
 *                   when the loop exhausts; skip adapt when ra_rix < 0) and
 *                   shows none of the above occur.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h>
#include <assert.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

/* ---- sys/netproto/802_11/_ieee80211.h:374-377 ---- */
#define IEEE80211_RATE_SIZE     8    /* 802.11 standard rates */
#define IEEE80211_RATE_MAXSIZE  15   /* max rates we'll handle */
#define IEEE80211_RATE_VAL      0x7f

struct ieee80211_rateset {
	uint8_t	rs_nrates;
	uint8_t	rs_rates[IEEE80211_RATE_MAXSIZE];
};

/* ---- sys/netproto/802_11/ieee80211_rssadapt.h:45-67 ---- */
#define IEEE80211_RSSADAPT_BKTS      3
#define IEEE80211_RSSADAPT_BKT0      128
#define IEEE80211_RSSADAPT_BKTPOWER  3

struct ieee80211_rssadapt {
	const void	*vap;
	int		interval;
};

struct ieee80211_rssadapt_node {
	struct ieee80211_rssadapt *ra_rs;
	struct ieee80211_rateset ra_rates;
	int	ra_rix;
	int	ra_ticks;
	int	ra_last_raise;
	int	ra_raise_interval;
	uint32_t ra_nfail;
	uint32_t ra_nok;
	uint32_t ra_pktrate;
	uint16_t ra_rate_thresh[IEEE80211_RSSADAPT_BKTS]
				[IEEE80211_RATE_MAXSIZE]; /* DF-0730 resize */
};

/* ---- replica of the node container: ni_rates precedes ni_txrate ---- */
struct fake_node {
	struct ieee80211_rateset ni_rates;
	uint8_t	ni_txrate;
};

/* ---- exact rssadapt logic (ieee80211_rssadapt.c:220-338) ---- */
static void segv_handler(int sig) { (void)sig; _exit(42); }

static int
bucket(int pktlen)
{
	int i, top, thridx;

	for (i = 0, top = IEEE80211_RSSADAPT_BKT0;
	     i < IEEE80211_RSSADAPT_BKTS;
	     i++, top <<= IEEE80211_RSSADAPT_BKTPOWER) {
		thridx = i;
		if (pktlen <= top)
			break;
	}
	return thridx;
}

#define interpolate(parm, old, new) ((parm##_old * (old) + \
				     (parm##_denom - parm##_old) * (new)) / \
				     parm##_denom)

static const struct { int rc_thresh_old, rc_thresh_denom; } threshctl = {4, 8};

static int
rssadapt_rate(struct fake_node *ni, struct ieee80211_rssadapt_node *ra,
    int rssi, u_int pktlen)
{
	const struct ieee80211_rateset *rs = &ra->ra_rates;
	uint16_t (*thrs)[IEEE80211_RATE_MAXSIZE];
	int rix;

	thrs = &ra->ra_rate_thresh[bucket(pktlen)];

	for (rix = rs->rs_nrates-1; rix >= 0; rix--)
		if ((*thrs)[rix] < (rssi << 8))
			break;
#ifdef FIXED
	if (rix < 0)			/* DF-3459 fix: no rate fits */
		rix = 0;
#endif
	if (rix != ra->ra_rix) {
		/* line 258: index rix == -1 reads rs_rates[-1] */
		ni->ni_txrate = ni->ni_rates.rs_rates[rix] & IEEE80211_RATE_VAL;
		ra->ra_rix = rix;
	}
	return rix;
}

static void
rssadapt_lower_rate(struct ieee80211_rssadapt_node *ra, int pktlen, int rssi)
{
	uint16_t last_thr;
	uint16_t (*thrs)[IEEE80211_RATE_MAXSIZE];
	u_int rix;				/* <- u_int, as in the kernel */

	thrs = &ra->ra_rate_thresh[bucket(pktlen)];

	rix = ra->ra_rix;			/* -1 -> 0xffffffff */
#ifdef FIXED
	if ((int)rix < 0)		/* DF-3459 fix: nothing to adapt */
		return;
#endif
	last_thr = (*thrs)[rix];
	(*thrs)[rix] = interpolate(threshctl.rc_thresh, last_thr, (rssi << 8));
}

/* canaried region so we can prove where the [0][-1] write lands.
 * Struct sits at offset 32: 8-byte aligned (no aligned-SSE fault) with
 * canary bytes both before and after. */
enum { CAN = 32 };
static unsigned char zone[sizeof(struct ieee80211_rssadapt_node) + 2 * CAN];
#define CANARY0 0xAA
#define CANARY1 0x55

static int
canaries_intact(const unsigned char *z)
{
	size_t i;

	for (i = 0; i < CAN; i++)
		if (z[i] != CANARY0 ||
		    z[CAN + sizeof(struct ieee80211_rssadapt_node) + i] != CANARY1)
			return 0;
	return 1;
}

int
main(void)
{
	struct ieee80211_rssadapt_node *ra;
	struct fake_node ni;
	long off_thresh, off_pktrate;
	int rix, i;

	setvbuf(stdout, NULL, _IONBF, 0);
	memset(zone, 0, sizeof(zone));
	memset(zone, CANARY0, CAN);
	memset(zone + CAN + sizeof(struct ieee80211_rssadapt_node),
	    CANARY1, CAN);
	ra = (struct ieee80211_rssadapt_node *)(zone + CAN);

	off_thresh = offsetof(struct ieee80211_rssadapt_node, ra_rate_thresh);
	off_pktrate = offsetof(struct ieee80211_rssadapt_node, ra_pktrate);
	printf("layout: ra_pktrate@%ld  ra_rate_thresh@%ld  "
	       "(thresh[-1] aliases offset %ld = ra_pktrate%+ld)\n",
	       off_pktrate, off_thresh, off_thresh - 2,
	       (off_thresh - 2) - off_pktrate);

	/* fresh node: 8 negotiated rates, zeroed thresholds (M_ZERO alloc) */
	ra->ra_rates.rs_nrates = 8;
	for (i = 0; i < 8; i++)
		ra->ra_rates.rs_rates[i] = (i + 1) * 2;   /* 1..8 *2 = .5Mb units */
	memset(&ni, 0, sizeof(ni));
	ni.ni_rates = ra->ra_rates;
	ra->ra_rix = 7;			/* set by rssadapt_node_init */
	ra->ra_pktrate = 0x11223344;	/* poison to detect the alias write */

	/* (a) fresh node, no RX sample yet: node_getrssi() == 0 (marker) */
	rix = rssadapt_rate(&ni, ra, /*rssi*/0, /*pktlen*/64);
	printf("rssi=0 fresh node : rssadapt_rate -> rix=%d  ra_rix=%d  "
	       "ni_txrate=0x%02x (bogus: rs_nrates byte %d)\n",
	       rix, ra->ra_rix, ni.ni_txrate, ra->ra_rates.rs_nrates);

#ifndef FIXED
	if (rix >= 0) {
		printf("RESULT: unexpected rix >= 0 โ€” premise wrong\n");
		return 2;
	}
#endif
	printf("  -> rs_rates[-1] read returned the rs_nrates byte (%d): "
	       "in-struct OOB read confirmed\n",
	       ni.ni_txrate == (ra->ra_rates.rs_nrates & IEEE80211_RATE_VAL));

	/* iwm_setrates() KASSERT โ€” if_iwm.c:4366 */
	printf("  -> iwm_setrates KASSERT(rix >= 0 && rix < nrates): "
	       "%s  (INVARIANTS kernel would PANIC here)\n",
	       (rix >= 0 && rix < 8) ? "ok" : "VIOLATED");

	/* (b) failed TX completion at ra_rix == -1 -> lower_rate()[*thrs][-1] */
	{
		uint16_t (*thrs)[IEEE80211_RATE_MAXSIZE] =
		    &ra->ra_rate_thresh[bucket(64)];
		u_int wild_rix = (u_int)ra->ra_rix;	/* kernel's u_int rix */
		printf("lower_rate with ra_rix=-1: u_int rix = 0x%08x -> "
		       "access at chunk+%#llx (wild +%llu GiB)\n",
		       wild_rix,
		       (unsigned long long)((char *)thrs +
			   2ULL * wild_rix - (char *)ra),
		       (unsigned long long)((2ULL * wild_rix) >> 30));
	}
	{
		struct sigaction sa, osa;
		memset(&sa, 0, sizeof(sa));
		sa.sa_handler = segv_handler;
		sigaction(SIGSEGV, &sa, &osa);
		pid_t pid = fork();
		if (pid == 0) {
			/* child: perform the kernel's exact wild access */
			rssadapt_lower_rate(ra, 64, 0);
			_exit(0);			/* not reached if wild */
		}
		int st = 0;
		waitpid(pid, &st, 0);
		sigaction(SIGSEGV, &osa, NULL);
		if (WIFEXITED(st) && WEXITSTATUS(st) == 42)
			printf("  -> WILD ACCESS confirmed: kernel equivalent "
			       "is a ~+8GiB READ+WRITE from the rssadapt_node "
			       "chunk -> kernel page fault -> PANIC\n");
		else if (WIFSIGNALED(st))
			printf("  -> WILD ACCESS confirmed (signal %d): kernel "
			       "equivalent is a ~+8GiB access -> PANIC\n",
			       WTERMSIG(st));
		else
			printf("  -> no fault observed\n");
	}
	printf("after tx-complete failure: ra_pktrate = 0x%08x "
	       "(was 0x11223344)  canaries %s\n",
	       ra->ra_pktrate, canaries_intact(zone) ? "intact" : "CLOBBERED");

	if (rix < 0) {
		printf("RESULT: BUG REPRODUCED โ€” rix=-1: in-struct rs_rates[-1] "
		       "read, iwm KASSERT violation, and u_int-indexed "
		       "~+8GiB wild read+write in rssadapt_lower_rate()\n");
		return 0;
	}
	printf("RESULT: no negative-index effect observed (fix active)\n");
	return 0;
}