⬢ DragonFlyBSD Kernel Audit
DF-4956 / v4kleak.c
← back to finding ↓ download raw
/*
 * DF-4956 PoC — ath_hal_v4kEepromAttach leaks HAL_EEPROM_v4k on error paths.
 *
 * Third and last member of the eeprom-attach leak family:
 *   DF-4873 (ah_eeprom_9287.c, ar9287) / DF-4913 (ah_eeprom_v14.c, ar5416)
 *   / DF-1995 (ar5210 v1).  This file: ah_eeprom_v4k.c (ar9285 "Kite").
 *
 * Calls the REAL, in-kernel ath_hal_v4kEepromAttach() (compiled into the
 * guest kernel) with a synthetic struct ath_hal whose ah_eepromRead callback
 * serves a crafted 4k-variant EEPROM image (owl_eep_start_loc == 64).
 *
 *   mode 1 (EEREAD):        read fails mid-image  -> return at ah_eeprom_v4k.c:324
 *   mode 2 (EEBADSUM):      XOR checksum != 0xffff-> return at ah_eeprom_v4k.c:366
 *   mode 3 (EEBADSUM ver):  checksum OK, bad ver  -> return at ah_eeprom_v4k.c:385
 *   mode 0 (CONTROL):       valid v4k image       -> HAL_OK, eeprom installed,
 *                           explicit detach frees it (balanced).
 *
 * The three failure modes each orphan the ath_hal_malloc'd HAL_EEPROM_v4k
 * (ah_eeprom_v4k.c:311): no ath_hal_free, and AH_PRIVATE(ah)->ah_eeprom /
 * ah_eepromDetach are only installed at :390-396 after every error return,
 * so nothing can ever reclaim it.  The caller (ar9285_attach.c:246-248
 * "goto bad" -> ah_detach -> ar5416Detach:507 -> ath_hal_eepromDetach macro,
 * ah_internal.h:474-477) is a no-op for it.
 *
 * Measure the leak from userland:
 *   vmstat -m | grep ath_hal     (before kldload / after trigger / after kldunload)
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/errno.h>
#include <sys/sysctl.h>

#include "opt_ah.h"
#include "ah.h"
#include "ah_internal.h"
#include "ah_eeprom_v14.h"
#include "ah_eeprom_v4k.h"
/* ath_hal_v4kEepromAttach() is declared in ah_eeprom.h (pulled in above).
 * NB: ah_eeprom_v4k.h #undef's owl_eep_start_loc and sets it to 64. */

#define NWORDS	(sizeof(struct ar5416eeprom_4k) / sizeof(uint16_t))
#define NFAIL	200		/* attach attempts per failure mode */

static uint16_t fake_eep[NWORDS];
static int fail_word = -1;

static HAL_BOOL
fake_eepromRead(struct ath_hal *ah, u_int off, uint16_t *data)
{
	u_int idx;

	if (off == AR5416_EEPROM_MAGIC_OFFSET) {
		*data = AR5416_EEPROM_MAGIC;
		return AH_TRUE;
	}
	if (off < owl_eep_start_loc) {
		*data = 0xffff;
		return AH_TRUE;
	}
	idx = off - owl_eep_start_loc;
	if (fail_word >= 0 && idx == (u_int)fail_word)
		return AH_FALSE;	/* simulated EEPROM read failure */
	*data = (idx < NWORDS) ? fake_eep[idx] : 0xffff;
	return AH_TRUE;
}

/*
 * Build an image whose word 2 is `length'.  The attach code checksums
 * AH_MIN(length, sizeof(struct ar5416eeprom_4k))/2 words; we fix up word 1
 * (the checksum field) so that XOR over the checked range == 0xffff when
 * `fixup' is set.  After the checksum the code swaps words 0 and 2
 * (ah_eeprom_v4k.c:373-375), so the version it validates is the value we
 * put in word 2:
 *   good  : 0xE001 -> (0xE001 >> 12) & 0xF == 14 == AR5416_EEP_VER -> OK
 *   badver: NWORDS -> high nibble 0          -> version check fails
 */
static void
craft_image(uint16_t length, int fixup)
{
	u_int len_check = (AH_MIN(length, sizeof(struct ar5416eeprom_4k))) / 2;
	uint16_t s = 0;
	u_int w;

	memset(fake_eep, 0, sizeof(fake_eep));
	fake_eep[2] = length;
	if (!fixup)
		fake_eep[5] = 0x1234;	/* guaranteed != 0xffff checksum */
	else {
		for (w = 0; w < len_check && w < NWORDS; w++)
			s ^= fake_eep[w];
		fake_eep[1] = (uint16_t)(0xffff ^ s);	/* word 1 in range */
	}
	fail_word = -1;
}

struct ath_hal_private *fake_ahp;

static int
do_mode(const char *name, uint16_t length, int fixup, int midread_fail)
{
	HAL_STATUS st;
	int i, ok = 0, leaked = 0;

	craft_image(length, fixup);
	if (midread_fail)
		fail_word = 5;

	for (i = 0; i < NFAIL; i++) {
		memset(fake_ahp, 0, sizeof(*fake_ahp));
		AH_PRIVATE((struct ath_hal *)fake_ahp)->ah_eepromRead =
		    fake_eepromRead;
		st = ath_hal_v4kEepromAttach((struct ath_hal *)fake_ahp);
		if (st == HAL_OK)
			ok++;
		/* not installed => permanently orphaned */
		if (AH_PRIVATE((struct ath_hal *)fake_ahp)->ah_eeprom == NULL)
			leaked++;
	}
	kprintf("DF-4956[%s]: %d attaches, %d HAL_OK, %d with ah_eeprom==NULL "
	    "(orphaned mallocs)\n", name, NFAIL, ok, leaked);
	return 0;
}

static int
do_control(void)
{
	HAL_STATUS st;
	int i, ok = 0;

	craft_image(0xE001, 1);
	for (i = 0; i < NFAIL; i++) {
		memset(fake_ahp, 0, sizeof(*fake_ahp));
		AH_PRIVATE((struct ath_hal *)fake_ahp)->ah_eepromRead =
		    fake_eepromRead;
		st = ath_hal_v4kEepromAttach((struct ath_hal *)fake_ahp);
		if (st == HAL_OK) {
			ok++;
			/* properly installed: detach balances the malloc */
			AH_PRIVATE((struct ath_hal *)fake_ahp)->
			    ah_eepromDetach((struct ath_hal *)fake_ahp);
		}
	}
	kprintf("DF-4956[control]: %d attaches, %d HAL_OK, each detached "
	    "(balanced; expect no ath_hal growth)\n", NFAIL, ok);
	return 0;
}

static int
sysctl_run(SYSCTL_HANDLER_ARGS)
{
	int v = 0, error;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	kprintf("DF-4956: trigger\n");
	fake_ahp = kmalloc(sizeof(*fake_ahp), M_TEMP, M_WAITOK | M_ZERO);
	do_mode("EEREAD@324",   0,       0, 1);
	do_mode("EEBADSUM@366", 0x1111,  0, 0);
	do_mode("BADVER@385",   NWORDS,  1, 0);
	do_control();
	kfree(fake_ahp, M_TEMP);
	kprintf("DF-4956: demo complete\n");
	return (0);
}

static int
v4kleak_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		kprintf("DF-4956 PoC loaded (inert): sizeof(HAL_EEPROM_v4k)=%zu "
		    "sizeof(ar5416eeprom_4k)=%zu NWORDS=%zu owl_eep_start_loc=%d "
		    "— sysctl hw.v4kleak=1 to run\n",
		    sizeof(HAL_EEPROM_v4k), sizeof(struct ar5416eeprom_4k),
		    (size_t)NWORDS, owl_eep_start_loc);
		SYSCTL_ADD_OID(NULL, SYSCTL_STATIC_CHILDREN(_hw), OID_AUTO,
		    "v4kleak", CTLTYPE_INT|CTLFLAG_RW, NULL, 0, sysctl_run,
		    "I", "DF-4956 trigger");
		return 0;
	case MOD_UNLOAD:
		kprintf("DF-4956 PoC unloaded (leaked blocks stay leaked)\n");
		return 0;
	}
	return 0;
}

static moduledata_t v4kleak_mod = {
	"v4kleak",
	v4kleak_modevent,
	NULL
};
DECLARE_MODULE(v4kleak, v4kleak_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_VERSION(v4kleak, 1);