โฌข DragonFlyBSD Kernel Audit
DF-5546 / msileak.c
โ† back to finding โ†“ download raw
/*
 * DF-5546 harness โ€” xhci_pci_attach() allocates MSI vectors via
 * pci_alloc_1intr() (xhci_pci.c:234) unconditionally, but when the
 * driver runs in forced-polling mode (hw.usb.xhci.use_polling=1 boot
 * tunable) the IRQ resource is never created (xhci_pci.c:253-268), and
 * xhci_pci_detach() only calls pci_release_msi() under
 * `if (sc->sc_irq_res)' (xhci_pci.c:366-371) โ€” the MSI vectors, the
 * rid map and the config-space MSI-enable bit LEAK on every
 * attach/detach cycle.
 *
 * Decisive observable: after a clean device_detach(xhci0) (children
 * detach fine โ€” no veto involved), calling pci_release_msi(xhci0)
 * returns 0 (ENODEV == "nothing to release" is what a correct driver
 * leave behind). The MSI cap control register still has
 * PCIM_MSICTRL_MSI_ENABLE set post-detach. The harness releases the
 * leaked vectors itself so the guest stays usable.
 *
 * Guest must be booted with a real xHCI controller AND the tunable:
 *   /boot/loader.conf.local: hw.usb.xhci.use_polling=1
 *   QEMU_EXTRA="-device qemu-xhci" dfbsd-qemu/vm.sh up
 *
 * kldload /root/df5546/msileak.ko
 * sysctl hw.df5546=1
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/sysctl.h>
#include <sys/malloc.h>
#include <sys/condvar.h>
#include <sys/kthread.h>
#include <sys/proc.h>
#include <sys/unistd.h>
#include <sys/errno.h>

#include <bus/pci/pcireg.h>
#include <bus/pci/pcivar.h>

#include <bus/u4b/usb.h>
#include <bus/u4b/usbdi.h>
#include <bus/u4b/usb_core.h>
#include <bus/u4b/usb_busdma.h>
#include <bus/u4b/usb_process.h>
#include <bus/u4b/usb_controller.h>
#include <bus/u4b/usb_bus.h>
#include <bus/u4b/controller/xhci.h>

static device_t xhci_dev;
static struct thread *df_td;
static volatile int kick;
static volatile int dead;

static int saved_irq_rid;
static void *saved_irq_res;
static void *saved_intr_hdl;
static int msi_cap_off = -1;

static void
df5546_find_msi_cap(device_t dev)
{
	uint8_t ptr;

	msi_cap_off = -1;
	if (!(pci_read_config(dev, PCIR_STATUS, 2) & PCIM_STATUS_CAPPRESENT))
		return;
	ptr = pci_read_config(dev, PCIR_CAP_PTR, 1) & ~0x3;
	while (ptr != 0 && ptr != 0xff) {
		if (pci_read_config(dev, ptr, 1) == PCIY_MSI) {
			msi_cap_off = ptr;
			return;
		}
		ptr = pci_read_config(dev, ptr + 1, 1) & ~0x3;
	}
}

static uint16_t
df5546_msi_ctrl(device_t dev)
{
	if (msi_cap_off < 0)
		return (0xffff);
	return (pci_read_config(dev, msi_cap_off + PCIR_MSI_CTRL, 2));
}

static void
df5546_cmd1(void)
{
	struct xhci_softc *sc;
	uint16_t pre, post;
	int err, rel;

	if (!xhci_dev) {
		kprintf("DF5546: no xhci device\n");
		return;
	}
	sc = device_get_softc(xhci_dev);
	saved_irq_rid = sc->sc_irq_rid;
	saved_irq_res = sc->sc_irq_res;
	saved_intr_hdl = sc->sc_intr_hdl;

	df5546_find_msi_cap(xhci_dev);
	pre = df5546_msi_ctrl(xhci_dev);
	kprintf("DF5546: xhci0 pre-detach: sc_irq_rid=%d sc_irq_res=%p "
	    "sc_intr_hdl=%p (polling mode => MSI allocated by "
	    "pci_alloc_1intr but NO IRQ resource)\n",
	    saved_irq_rid, saved_irq_res, saved_intr_hdl);
	kprintf("DF5546: MSI cap @0x%02x ctrl=0x%04x (enable bit %s)\n",
	    msi_cap_off, pre,
	    (pre & PCIM_MSICTRL_MSI_ENABLE) ? "SET" : "clear");

	kprintf("DF5546: calling device_detach(%s)\n",
	    device_get_nameunit(xhci_dev));
	err = device_detach(xhci_dev);
	kprintf("DF5546: device_detach(xhci0) returned %d\n", err);
	if (err != 0) {
		kprintf("DF5546: detach refused (children?) โ€” no leak test\n");
		return;
	}

	post = df5546_msi_ctrl(xhci_dev);
	kprintf("DF5546: post-detach MSI ctrl=0x%04x (enable bit %s)\n",
	    post, (post & PCIM_MSICTRL_MSI_ENABLE) ? "STILL SET" : "clear");

	/*
	 * The probe: on a correct detach there is nothing left to release.
	 * 0      -> vectors WERE still allocated: the driver leaked them.
	 * ENODEV -> nothing to release: the driver cleaned up.
	 */
	rel = pci_release_msi(xhci_dev);
	kprintf("DF5546: pci_release_msi(xhci0) probe returned %d (%s)\n",
	    rel, rel == 0 ?
	    "LEAK: driver left MSI vectors allocated" :
	    rel == ENODEV ? "clean: nothing left to release" : "?");
}

static void
df5546_thread(void *arg)
{
	int c;

	kprintf("DF5546: worker thread ready (sysctl hw.df5546=N)\n");
	for (;;) {
		tsleep(&kick, 0, "df5546", 0);
		c = kick;
		kick = 0;
		switch (c) {
		case 1:
			df5546_cmd1();
			break;
		default:
			break;
		}
		if (c == 9)
			break;
	}
	dead = 1;
	wakeup(&dead);
	kthread_exit();
}

static int
df5546_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, cmd = 0;

	error = sysctl_handle_int(oidp, &cmd, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	kick = cmd;
	wakeup(&kick);
	return (0);
}
SYSCTL_PROC(_hw, OID_AUTO, df5546, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, df5546_sysctl, "I", "DF-5546 harness control (1=detach+leak-probe)");

static int
msileak_modevent(module_t mod, int type, void *data)
{
	devclass_t dc;

	switch (type) {
	case MOD_LOAD:
		dc = devclass_find("xhci");
		if (dc == NULL) {
			kprintf("DF5546: no xhci devclass\n");
			return (ENXIO);
		}
		xhci_dev = devclass_get_device(dc, 0);
		if (xhci_dev == NULL) {
			kprintf("DF5546: no xhci0 device\n");
			return (ENXIO);
		}
		if (device_get_softc(xhci_dev) == NULL) {
			kprintf("DF5546: xhci0 has no driver bound\n");
			return (ENXIO);
		}
		kprintf("DF5546: found %s (driver %s)\n",
		    device_get_nameunit(xhci_dev),
		    device_get_driver(xhci_dev) ?
		    device_get_driver(xhci_dev)->name : "(none)");
		dead = 0;
		kthread_create(df5546_thread, NULL, &df_td, "df5546_td");
		kprintf("DF5546: harness loaded\n");
		return (0);
	case MOD_UNLOAD:
		kick = 9;
		wakeup(&kick);
		while (!dead)
			tsleep(&dead, 0, "df5546x", hz / 4);
		kprintf("DF5546: module unload\n");
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}
static moduledata_t msileak_mod = { "msileak", msileak_modevent, 0 };
DECLARE_MODULE(msileak, msileak_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_VERSION(msileak, 1);