โฌข DragonFlyBSD Kernel Audit
DF-3958 / trigger.c
โ† back to finding โ†“ download raw
/* DF-3958 โ€” vtnet mergeable-RX sub-buffer negative device length.
 *
 * Function-level replica of the EXACT kernel code path:
 *   - virtqueue_dequeue() writes the device-controlled u32 used-ring length
 *     through a `uint32_t *lenp' (sys/dev/virtual/virtio/virtio/virtqueue.c:544-561)
 *   - if_vtnet.c passes `int len' to it (if_vtnet.c:1642,1649) โ€” the pointer
 *     type mismatch is the same one behind DF-1310, but the mergeable
 *     sub-buffer path clamps with a SIGNED compare:
 *
 *     if (m->m_len < len)          // if_vtnet.c:1663  โ€” signed min()
 *         len = m->m_len;
 *     m->m_len = len;              // if_vtnet.c:1666  โ€” m_len := negative
 *     m_head->m_pkthdr.len += len; // if_vtnet.c:1669  โ€” pkthdr.len corrupted
 *
 *   A device length >= 0x80000000 becomes a negative int, the clamp is
 *   skipped (m->m_len < len is false for negative len), and an mbuf with a
 *   NEGATIVE m_len is chained onto m_head and handed to ifp->if_input()
 *   (if_vtnet.c:1670-1671, 1777) BEFORE any length validation โ€” the chain
 *   reaches bridge/carp/ng_ether taps and BPF (sys/net/if_ethersubr.c:1212+).
 *
 * Builds and runs identically on DragonFly guest (cc -O2 -Wall).
 */
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
#include <string.h>

#define	ETHER_HDR_LEN		14
#define	VTNET_MAX_RX_SIZE	65550

/* --- kernel type model (int fields exactly as sys/mbuf.h) --- */
struct mbuf {
	int		 m_len;		/* int in kernel */
	int		 m_pkthdr_len;	/* int in kernel */
	struct mbuf	*m_next;
};

/* --- replica of virtqueue_dequeue()'s length store (virtqueue.c:560-561):
 *     `*len = uep->len' where the callee writes uint32_t and the vtnet
 *     caller (if_vtnet.c:1649) owns an `int len'.  Model the exact store. */
static void
vq_dequeue_store_len(void *lenp_int, uint32_t used_ring_len)
{
	*(uint32_t *)lenp_int = used_ring_len;
}

/* sub-buffer mbuf that survives the replica call (kernel mbufs are heap) */
static struct mbuf sub_m;

/* --- replica of vtnet_rxeof_merged() core (if_vtnet.c:1648-1672) --- */
static int
rxeof_merged_vuln(struct mbuf *m_head, uint32_t sub_used_len)
{
	struct mbuf *m, *m_tail;
	int len;

	m_tail = m_head;

	/* while (--nbufs > 0) { m = virtqueue_dequeue(vq, &len); ... } */
	memset(&sub_m, 0, sizeof(sub_m));
	sub_m.m_len = 2048;			/* clsize at alloc (if_vtnet.c:1239) */
	m = &sub_m;
	vq_dequeue_store_len(&len, sub_used_len);	/* device-controlled */

	if (m->m_len < len)			/* if_vtnet.c:1663 (signed) */
		len = m->m_len;

	m->m_len = len;				/* if_vtnet.c:1666 */
	m_head->m_pkthdr_len += len;		/* if_vtnet.c:1669 */
	m_tail->m_next = m;			/* if_vtnet.c:1670 */
	return (0);
}

/* --- replica of the FIXED clamp: unsigned compare (see fix.diff) --- */
static int
rxeof_merged_fixed(struct mbuf *m_head, uint32_t sub_used_len)
{
	struct mbuf *m;
	int len;

	memset(&sub_m, 0, sizeof(sub_m));
	sub_m.m_len = 2048;
	m = &sub_m;
	vq_dequeue_store_len(&len, sub_used_len);

	/* FIX if_vtnet.c:1663: treat the device length as unsigned โ€” any
	 * insane (>= 0x80000000 or > buffer) length is clamped to the
	 * buffer's m_len, exactly as the virtio device is supposed to behave. */
	if ((uint32_t)len > (uint32_t)m->m_len)
		len = m->m_len;

	m->m_len = len;
	m_head->m_pkthdr_len += len;
	m_head->m_next = m;
	return (0);
}

/* --- downstream consumer replicas that walk the corrupted chain --- */

/* sys/kern/uipc_mbuf.c:1686-1695 m_copydata() second loop:
 *     count = min(m->m_len - off, len);  bcopy(..., count);
 * with off==0 โ€” for a negative m_len, count goes negative and is passed
 * to bcopy() as an unsigned byte count. */
static int
m_copydata_bcopy_count(struct mbuf *m)
{
	int len = 32, off = 0;
	int count = (m->m_len - off) < len ? (m->m_len - off) : len;
	return count;
}

int
main(void)
{
	struct mbuf m_head;
	uint32_t dev_len = 0x80000000u;		/* >= 0x80000000 from device */
	int i;
	struct { int m_len; } chains[4] = {
	    { 14 }, { 100 }, { 1000 }, { 2048 } };

	printf("== DF-3958 vtnet_rxeof_merged signed sub-buffer length ==\n");
	printf("device used-ring len = 0x%08x\n", dev_len);

	memset(&m_head, 0, sizeof(m_head));
	m_head.m_len = 2048;			/* first buffer, >= hdr+14 */
	m_head.m_pkthdr_len = 26;		/* hdr_size(12)+ETHER_HDR_LEN(14) */

	rxeof_merged_vuln(&m_head, dev_len);

	printf("-- BEFORE-FIX (kernel code as-is) --\n");
	printf("m_head.m_pkthdr.len = %d (<- 26 + negative)\n", m_head.m_pkthdr_len);
	printf("m2->m_len           = %d\n", m_head.m_next->m_len);
	if (m_head.m_next->m_len < 0)
		printf("BUG: mbuf with NEGATIVE m_len chained & delivered to "
		    "if_input (if_vtnet.c:1670,1777)\n");
	i = m_copydata_bcopy_count(m_head.m_next);
	printf("m_copydata(m2,...): count=min(m_len,len)=%d -> "
	    "(unsigned)0x%x -> bcopy of %u bytes => panic/smash "
	    "(uipc_mbuf.c:1689-1690)\n", i, (unsigned)i, (unsigned)i);
	printf("bpf_mcopy: walks into m2 data area reading stale cluster "
	    "bytes into listener buffer (bpf.c:1280-1284)\n");
	printf("first-buffer guards do NOT protect sub-buffers: "
	    "if_vtnet.c:1707 len<%d check and :1964 clamp only run on the "
	    "FIRST buffer\n", 12 + ETHER_HDR_LEN);

	/* sanity sweep: a range of negative-producing device lengths */
	printf("-- sweep (first-buffer len 26, sub len 0x80000000+k) --\n");
	for (i = 0; i < 4; i++) {
		struct mbuf h = { .m_len = 2048, .m_pkthdr_len = 26 };
		rxeof_merged_vuln(&h, 0x80000000u + (uint32_t)chains[i].m_len);
		printf("sub_len=0x8000%04x -> m2->m_len=%d pkthdr.len=%d\n",
		    chains[i].m_len, h.m_next->m_len, h.m_pkthdr_len);
	}

	memset(&m_head, 0, sizeof(m_head));
	m_head.m_len = 2048;
	m_head.m_pkthdr_len = 26;

	printf("-- AFTER-FIX (unsigned clamp, fix.diff) --\n");
	rxeof_merged_fixed(&m_head, dev_len);
	printf("m2->m_len=%d (clamped to buffer m_len), pkthdr.len=%d โ€” "
	    "sane chain, normal delivery\n", m_head.m_next->m_len,
	    m_head.m_pkthdr_len);

	return (m_head.m_next ? 0 : 1);
}