⬢ DragonFlyBSD Kernel Audit
DF-4913 / v14leak.c
← back to finding ↓ download raw
/*
 * DF-4913 PoC — ath_hal_v14EepromAttach leaks HAL_EEPROM_v14 on error paths.
 *
 * Calls the REAL, in-kernel ath_hal_v14EepromAttach() (compiled into the
 * guest kernel) with a synthetic struct ath_hal whose ah_eepromRead callback
 * serves a crafted EEPROM image.  Exercises:
 *
 *   mode 1 (EEREAD):        read fails mid-image  -> return at ah_eeprom_v14.c:376
 *   mode 2 (EEBADSUM):      XOR checksum != 0xffff-> return at ah_eeprom_v14.c:414
 *   mode 3 (EEBADSUM ver):  checksum OK, bad ver  -> return at ah_eeprom_v14.c:433
 *   mode 0 (CONTROL):       valid v14 image       -> HAL_OK, eeprom installed,
 *                           explicit detach frees it (balanced).
 *
 * The three failure modes each orphan the ath_hal_malloc'd HAL_EEPROM_v14
 * (ah_eeprom_v14.c:363): no ath_hal_free, and AH_PRIVATE(ah)->ah_eeprom /
 * ah_eepromDetach are only installed at :438-440 after every error return,
 * so nothing can ever reclaim it.
 *
 * Measure the leak from userland:
 *   vmstat -m | grep ath_hal     (before kldload / after kldload / 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"
/* ath_hal_v14EepromAttach() is declared in ah_eeprom.h (via ah_eeprom_v14.h) */

#define NWORDS	(sizeof(struct ar5416eeprom) / 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 baseEepHeader.length word (word 2) is `length'.
 * The attach code checksums AH_MIN(length, sizeof(struct ar5416eeprom))/2
 * words; we fix up word 1 so that XOR over the checked range == 0xffff when
 * `fixup' is set.  After the checksum the code swaps words 0 and 2, so the
 * version it validates is the `length' value we put in word 2:
 *   good  : 0xE001 -> (0xE001 >> 12) & 0xF == 14  -> attach succeeds
 *   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))) / 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_v14EepromAttach((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-4913[%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_v14EepromAttach((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-4913[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-4913: trigger\n");
	fake_ahp = kmalloc(sizeof(*fake_ahp), M_TEMP, M_WAITOK | M_ZERO);
	do_mode("EEREAD@376",   0,       0, 1);
	do_mode("EEBADSUM@414", 0x1111,  0, 0);
	do_mode("BADVER@433",   NWORDS,  1, 0);
	do_control();
	kfree(fake_ahp, M_TEMP);
	kprintf("DF-4913: demo complete\n");
	return (0);
}

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

static moduledata_t v14leak_mod = {
	"v14leak",
	v14leak_modevent,
	NULL
};
DECLARE_MODULE(v14leak, v14leak_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
MODULE_VERSION(v14leak, 1);