⬢ DragonFlyBSD Kernel Audit
DF-3712 / em_bounds_harness.c
← back to finding ↓ download raw
/*
 * em_bounds_harness.c - userspace bound-math replica of if_em.c RX/TX math.
 *
 * Replicates, line-for-line against sys/dev/netif/em/if_em.c (7.6.2 driver):
 *   [A] em_newbuf (:3359-3405) posting math vs em_init_rx_unit (:3593-3635)
 *       RCTL SZ_2048 + LPE configuration  ->  2-byte DMA write past cluster
 *       when MTU in (1500, 2028] (ETHER_ALIGN front-trim + long-frame RX).
 *   [B] em_rxeof (:3661-3821) descriptor-length trust:
 *         desc_len = 0xFFFF -> m_len/m_pkthdr.len claim 63,5xx bytes in a
 *         2048-byte cluster -> heap OOB read of ~63.5KB.
 *         prev_len_adj underflow (:3697-3699, :3754-3758) -> negative m_len.
 *   [C] em_tx_collect (:3275-3335) unvalidated TDH walk math.
 *
 * Every allocation is followed by a 64-byte poisoned redzone; any claimed
 * write/read span past the mapped span hits it.  This is *not* a driver
 * exploit - it proves the length math that the driver performs on the real
 * kernel heap, gated behind hardware the audit guest does not have.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* --- constants mirrored from the kernel/driver --- */
#define MCLBYTES	2048		/* sys/sys/param.h */
#define ETHER_ALIGN	2		/* net/ethernet.h */
#define ETHER_CRC_LEN	4
#define ETHERMTU	1500
#define ETHER_HDR_LEN	14
#define REDZONE		64

static unsigned char cluster[MCLBYTES + REDZONE];
static int fails;

static void check(const char *what, int bad)
{
	printf("  [%s] %s\n", bad ? "OVERFLOW" : "ok     ", what);
	if (bad)
		fails++;
}

/*
 * [A] em_newbuf:3376-3383 (post) vs em_init_rx_unit:3607-3632 (NIC fill).
 * max_frame_size = mtu + ETHER_HDR_LEN + ETHER_CRC_LEN  (em_ioctl:1276)
 * ETHER_ALIGN trim iff max_frame_size <= MCLBYTES - ETHER_ALIGN (<=2046)
 * LPE iff if_mtu > ETHERMTU (em_init_rx_unit:3629)
 * RCTL_SZ_2048 always (rx_buffer_len==MCLBYTES, set at em_attach:684)
 * NIC fills first descriptor of a multi-descriptor (jumbo) frame with
 * min(frame_len, 2048) bytes at the DMA address = cluster start (+trim).
 */
static void case_A(int mtu, int frame_len)
{
	int max_frame_size = mtu + ETHER_HDR_LEN + ETHER_CRC_LEN;
	int trim = (max_frame_size <= MCLBYTES - ETHER_ALIGN) ? ETHER_ALIGN : 0;
	int lpe = mtu > ETHERMTU;
	int mapped = MCLBYTES - trim;		/* bus_dmamap span */
	int fill = frame_len < 2048 ? frame_len : 2048;	/* RCTL_SZ_2048 */
	int dma_end = trim + fill;		/* bytes 0..dma_end-1 of cluster */
	int oob_write = dma_end > MCLBYTES;

	printf("[A] MTU=%d (max_frame_size=%d, trim=%d, LPE=%d), frame=%d B, "
	       "mapped=%d B, first-desc fill=%d B -> cluster write span "
	       "[%d, %d)\n", mtu, max_frame_size, trim, lpe, frame_len,
	       mapped, fill, trim, dma_end);

	if (trim && lpe) {
		/* the dangerous configuration: trim AND long frames admitted */
		memset(cluster, 0xAA, sizeof(cluster));
		/* NIC DMA-writes `fill` attacker-frame bytes at cluster+trim */
		memset(cluster + trim, 0x41, fill);
		/* redzone bytes written? (cluster is 2048, zone after it) */
		check("2-byte attacker-controlled DMA write past 2048-byte "
		      "mbuf cluster (first descriptor)", oob_write);
		/* stack side: em_rxeof:3738 sets m_len=len (desc1 non-EOP) */
		{
			int len = fill;		/* em_rxeof:3705 */
			int m_len_span = trim + len;
			check("stack-side m_len read span past cluster "
			      "(em_rxeof:3738)", m_len_span > MCLBYTES);
		}
	} else {
		printf("  [skip]   trim/LPE do not coexist (safe config)\n");
	}
}

/*
 * [B] em_rxeof length trust: descriptor says 0xFFFF (16-bit field,
 * DMA-written by NIC). Non-EOP fragment: len = desc_len (em_rxeof:3705),
 * mp->m_len = len (:3738); EOP: len = desc_len - ETHER_CRC_LEN (:3701).
 */
static void case_B(int desc_len, int eop)
{
	int len = eop ? (desc_len < ETHER_CRC_LEN ? 0 :
			 desc_len - ETHER_CRC_LEN) : desc_len;
	int trim = 0;			/* jumbo MTU: no ETHER_ALIGN (em_newbuf) */
	int usable = MCLBYTES - trim;

	printf("[B] desc_len=%u (%s): driver len=%d usable cluster span=%d\n",
	       (unsigned)desc_len, eop ? "EOP" : "non-EOP fragment", len, usable);
	if (eop && desc_len < ETHER_CRC_LEN) {
		printf("  (em_rxeof:3697-3699 -> len=0, prev_len_adj=%d - "
		       "DF-1188 territory)\n", ETHER_CRC_LEN - desc_len);
		return;
	}
	if (len > usable) {
		/* m_len = len; stack/BPF walk len bytes from cluster start */
		int oob_read = len - usable;
		printf("  [OVERFLOW] mp->m_len=%d claims %d bytes past the "
		       "2048-byte cluster: kernel heap OOB read of up to %d "
		       "bytes per descriptor (ether_input/BPF/forwarding "
		       "consumers)\n", len, oob_read, oob_read);
		fails++;
	}
	/* error-descriptor probe: em_rxeof:3715 reads last byte at
	 * mp->m_data + desc_len - 1 */
	{
		int probe = desc_len - 1;
		if (probe < 0 || probe >= usable)
			check("error-path last_byte probe OOB (em_rxeof:3715)",
			      probe < 0 || probe >= usable);
	}
}

/*
 * [B2] prev_len_adj underflow: previous non-EOP fragment had m_len 0
 * (desc_len=0), EOP fragment desc_len=2 -> prev_len_adj=2 ->
 * lmp->m_len -= 2 -> NEGATIVE m_len in the chain (em_rxeof:3754-3758).
 */
static void case_B2(void)
{
	int prev_len = 0;			/* previous mp->m_len */
	int desc_len = 2;			/* EOP fragment < 4 bytes */
	int prev_len_adj = ETHER_CRC_LEN - desc_len;	/* :3699 */
	int new_prev_len = prev_len - prev_len_adj;	/* :3755 */

	printf("[B2] prev frag m_len=%d, EOP desc_len=%d -> prev_len_adj=%d "
	       "-> lmp->m_len = %d\n", prev_len, desc_len, prev_len_adj,
	       new_prev_len);
	check("negative m_len injected into RX mbuf chain", new_prev_len < 0);
}

/*
 * [C] em_tx_collect:3284-3322 walk math: first advances mod num_tx_desc
 * until == tdh (raw TDH register read). KKASSERT(num_avail < num_tx_desc)
 * at :3302 is INVARIANTS-only (sys/sys/systm.h:117-119 no-op otherwise).
 */
static void case_C(unsigned tdh, int ndesc)
{
	int first = 0, num_avail = 36 /* whatever */, iters = 0, wraps = 0;
	int assert_would_fire = 0, iter_limit = 1 << 20;

	printf("[C] num_tx_desc=%d, next_tx_to_clean(first)=%d, raw TDH=%u\n",
	       ndesc, first, tdh);
	if (tdh >= (unsigned)ndesc) {
		while (first != (int)tdh && iters < iter_limit) {
			num_avail++;
			if (num_avail == ndesc)
				assert_would_fire = iters + 1;
			if (++first == ndesc) {
				first = 0;
				wraps++;
			}
			iters++;
		}
		printf("  TDH out of ring range: walked %d descriptors "
		       "(%d full ring wraps) without reaching TDH -> "
		       "unbounded ISR-context loop\n", iters, wraps);
		check("KKASSERT(num_avail<num_tx_desc) fires on INVARIANTS "
		      "builds (:3302); production: permanent serializer-held "
		      "spin", assert_would_fire != 0);
	} else {
		printf("  in-range TDH: normal cleanup, no issue\n");
	}
}

int main(void)
{
	printf("== if_em.c bound-math harness (guest has no em hardware; "
	       "source-line replica) ==\n\n");

	printf("--- [A] em_newbuf ETHER_ALIGN trim vs RCTL_SZ_2048+LPE "
	       "(if_em.c:3378-3379, :3607-3632) ---\n");
	case_A(1500, 3000);	/* standard MTU: safe */
	case_A(2000, 3000);	/* MTU in (1500,2028]: 2-byte heap overflow */
	case_A(9000, 9026);	/* big jumbo: no trim, safe from this bug */

	printf("\n--- [B] em_rxeof descriptor-length trust (:3692-3761) ---\n");
	case_B(1500, 1);	/* honest EOP frame: safe */
	case_B(0xFFFF, 0);	/* malicious/faulty NIC fragment */
	case_B(0xFFFF, 1);	/* malicious/faulty NIC EOP */

	printf("\n--- [B2] prev_len_adj underflow (:3697-3699, :3754-3758) ---\n");
	case_B2();

	printf("\n--- [C] em_tx_collect TDH walk (:3284-3322) ---\n");
	case_C(100, 512);	/* honest TDH */
	case_C(4096, 512);	/* out-of-range TDH */

	printf("\n%d overflow class(es) demonstrated in bound math\n", fails);
	return fails ? 1 : 0;
}