⬢ DragonFlyBSD Kernel Audit
DF-5578 / ufm5578_harness.c
← back to finding ↓ download raw
/*
 * DF-5578 KLD harness -- production-code demonstration of the ufm(4)
 * (Cypress FM radio) EP0 helper's uninitialized-stack read, its runtime
 * CONTAINMENT on the current stack, and the one-flag counterfactual that
 * turns it into the DF-1062-class disclosure.
 *
 * WHAT IT PROVES:
 *
 *   ufm_do_req() (ufm.c:176-200) declares `uint8_t buf[1];' UNINITIALIZED,
 *   submits a 1-byte vendor control-IN through the PLAIN usbd_do_request
 *   macro (usbdi.h:511-512: flags=0, actlen=NULL), and then -- BEFORE the
 *   error check -- does `*retbuf = buf[0];' (ufm.c:193-194).  Two callers
 *   feed userspace: ufm_get_stat() copies `ret' into the ioctl word
 *   (ufm.c:292 -> copyout via sys_generic.c:730) and ufm_start() branches
 *   on it (ufm.c:257).
 *
 *   Production containment (traced, and exercised here as modes 0/1):
 *     - short control-IN with flags=0 -> USB_ERR_SHORT_XFER
 *       (usb_transfer.c:2979-2985; the default EP0 template carries no
 *       short-ok flags, usb_transfer.c:76-88) -> ufm_do_req returns ENXIO
 *       -> both callers return EIO and DISCARD `ret' (ufm.c:253-256,
 *       288-291).  err==0 without a 1-byte copy-out is provably
 *       impossible: actlen == sumlen is required for success
 *       (usb_transfer.c:2952-2985) and actlen is summed over completed
 *       frames (usb_transfer.c:2936-2940), so a full 1-byte DATA stage
 *       was DMA'd and copied (usb_request.c:680-681).
 *
 *   Counterfactual (mode 3) -- the DF-1062 delta:
 *     - the SAME call site with USB_SHORT_XFER_OK (uchcom.c:398) or via
 *       usbd_do_request_proc with actlen=NULL (usb_serial.h:220) yields
 *       err==0 with nothing copied (the zero-pad-on-short only exists
 *       behind a non-NULL actlen, usb_request.c:782-787) -> FM_GET_STAT
 *       hands one stale kernel-stack byte to the ioctl caller.
 *
 * HOW IT RUNS PRODUCTION CODE:
 *
 *   This module #includes "ufm_prod_guarded.c" -- a byte-identical copy
 *   of /usr/src/sys/bus/u4b/misc/ufm.c (sha256 845738f2...) except ONE
 *   guard hunk (prod_guard.diff): the usbd_do_request macro is rerouted
 *   to the cross-TU shim ufm_shim_request() exactly like production's
 *   opaque cross-module call.  The real ufm_ioctl() dispatch (including
 *   the FWRITE|FREAD gate at ufm.c:304), ufm_set_freq(), ufm_get_freq(),
 *   ufm_start(), ufm_stop(), ufm_get_stat() and ufm_do_req() are ALL the
 *   verbatim production functions, called through a fabricated
 *   struct usb_fifo whose priv_sc0 is the softc stand-in
 *   (usb_fifo_softc(), usb_dev.c:2499-2502).  The DRIVER_MODULE
 *   registration is kept (inert: no Cypress 0x04b4:0x1002 radio exists
 *   on the QEMU guest, so nothing ever probes/attaches).
 *
 *   Deterministic stale-stack stand-in: recursive poison frames scribble
 *   a known byte over the stack region ufm_do_req()'s buf[0] will reuse;
 *   the matching recursion depth is calibrated once at load.
 *
 *   Counterfactual fixed twin: verbatim ufm_do_req() body with the
 *   ONE-LINE FIX (`uint8_t buf[1] = {0};') -- under identical poisoning
 *   it must report 0x00 in every mode.
 */
#include <sys/param.h>

#include "ufm_prod_guarded.c"
#include <bus/u4b/usb_dev.h>		/* struct usb_fifo (public) */

extern int		ufm_shim_mode;
extern int		ufm_shim_requests;
extern uint8_t		ufm_shim_devbyte;

/* request-capture accessors (see ufm5578_shim.c) */
struct ufm_shim_cap {
	uint8_t		bm;
	uint8_t		breq;
	uint16_t	wval;
	uint16_t	widx;
	uint16_t	wlen;
	int		mode;
	int		err;
};
extern void	ufm_shim_cap_reset(void);
extern int	ufm_shim_cap_dump(struct ufm_shim_cap *, int);

static struct ufm_softc	fm_sc;		/* zeroed softc stand-in */
static struct usb_fifo	fm_fifo;	/* fabricated: priv_sc0 = &fm_sc */

static int		fm_depth = 6;	/* calibrated poison depth */

/* ------------------------------------------------------------------ */
/* production entry points (all statics reachable: we #included the .c) */

static int __noinline
fm_prod_ioctl(u_long cmd, void *addr, int fflags)
{
	return (ufm_ioctl(&fm_fifo, cmd, addr, fflags));	/* PRODUCTION */
}

static int __noinline
fm_prod_do_req(uint8_t request, uint16_t value, uint16_t index, uint8_t *retbuf)
{
	return (ufm_do_req(&fm_sc, request, value, index, retbuf)); /* PROD */
}

/*
 * Counterfactual fixed twin: verbatim ufm_do_req() with the one-line fix.
 */
static int __noinline
fm_prod_do_req_fixed(uint8_t request, uint16_t value, uint16_t index,
    uint8_t *retbuf)
{
	int error;

	struct usb_device_request req;
	uint8_t buf[1] = {0};		/* <-- THE FIX (fix.diff) */

	req.bmRequestType = UT_READ_VENDOR_DEVICE;
	req.bRequest = request;
	USETW(req.wValue, value);
	USETW(req.wIndex, index);
	USETW(req.wLength, 1);

	error = usbd_do_request(fm_sc.sc_udev, NULL, &req, buf);

	if (retbuf) {
		*retbuf = buf[0];
	}
	if (error) {
		return (ENXIO);
	}
	return (0);
}

/* ------------------------------------------------------------------ */
/* deterministic stale-stack stand-in (family standard) */

static void __noinline
fm_poison(uint8_t pat, int depth)
{
	volatile uint8_t pad[256];
	int i;

	for (i = 0; i < 256; ++i)
		pad[i] = pat;
	if (depth > 0)
		fm_poison(pat, depth - 1);
}

/* ------------------------------------------------------------------ */

static void
fm_calibrate(void)
{
	int d;
	uint8_t ret = 0;

	/*
	 * Find the recursion depth at which ufm_do_req()'s buf[0] reuses the
	 * poisoned region (mode 3 = err==0 without copy-out).
	 */
	for (d = 1; d <= 14; ++d) {
		ufm_shim_mode = 3;
		fm_poison(0xa5, d);
		ret = 0;
		(void)fm_prod_do_req(UFM_CMD0, 0x00, 0x24, &ret);
		if (ret == 0xa5) {
			fm_depth = d;
			kprintf("UFM5578: calibrate: poison depth %d MATCHES "
			    "buf[0] slot\n", d);
			return;
		}
	}
	kprintf("UFM5578: calibrate: NO exact depth match (stack too "
	    "volatile); using depth %d\n", fm_depth);
}

static int
fm_matrix(void)
{
	static const uint8_t pats[] = { 0x00, 0xff, 0xa5, 0x5a, 0x0f, 0xf0 };
	int i, rep;
	int match = 0, total = 0, fixed_clean = 0;

	/*
	 * Counterfactual (mode 3): err==0, nothing copied -> the production
	 * *retbuf load (ufm.c:193-194) returns the poisoned stack byte,
	 * which ufm_get_stat() would copy to the FM_GET_STAT caller
	 * (ufm.c:292).
	 */
	for (i = 0; i < (int)(sizeof(pats) / sizeof(pats[0])); ++i) {
		for (rep = 0; rep < 3; ++rep) {
			uint8_t ret = 0xee;

			ufm_shim_mode = 3;
			fm_poison(pats[i], fm_depth);
			(void)fm_prod_do_req(UFM_CMD0, 0x00, 0x24, &ret);
			kprintf("UFM5578: pat=0x%02x rep=%d mode=short-ok   : "
			    "PROD ret=0x%02x %s\n", pats[i], rep, ret,
			    ret == pats[i] ? "MATCH" : "MISMATCH");
			if (ret == pats[i])
				match++;
			total++;

			ret = 0xee;
			fm_poison(pats[i], fm_depth);
			(void)fm_prod_do_req_fixed(UFM_CMD0, 0x00, 0x24, &ret);
			kprintf("UFM5578: pat=0x%02x rep=%d FIXED(buf={0}) : "
			    "ret=0x%02x %s\n", pats[i], rep, ret,
			    ret == 0 ? "(clean)" : "(LEAK SURVIVES FIX?!)");
			if (ret == 0)
				fixed_clean++;
		}
	}
	kprintf("UFM5578: RESULT: counterfactual short-ok: %d/%d "
	    "pattern-match; fixed twin clean %d/18\n", match, total,
	    fixed_clean);
	return (match == total && fixed_clean == 18);
}

static void
fm_full_chain(void)
{
	int out, rc;

	/* the privilege gate: O_RDONLY-only fd -> EACCES (ufm.c:304-306) */
	out = 0x12345678;
	rc = fm_prod_ioctl(FM_GET_STAT, &out, FREAD);
	kprintf("UFM5578: gate fflags=FREAD        : rc=%d (%s) "
	    "out=0x%08x\n", rc, rc == EACCES ? "EACCES" : "?", out);

	/* containment: production short/timeout semantics through the REAL
	 * ioctl dispatch (incl. the 250ms settle sleep at ufm.c:286) */
	ufm_shim_mode = 0;			/* short ZLP -> SHORT_XFER */
	out = 0xcc;
	rc = fm_prod_ioctl(FM_GET_STAT, &out, FREAD | FWRITE);
	kprintf("UFM5578: FULL FM_GET_STAT short  : rc=%d (%s) out=0x%08x "
	    "%s\n", rc, rc == EIO ? "EIO" : "?", out,
	    (rc == EIO && out == 0xcc) ? "-- LEAK BLOCKED (discarded)" :
	    "-- UNEXPECTED");

	ufm_shim_mode = 1;			/* silence -> TIMEOUT */
	out = 0xcc;
	rc = fm_prod_ioctl(FM_GET_STAT, &out, FREAD | FWRITE);
	kprintf("UFM5578: FULL FM_GET_STAT silent : rc=%d (%s) out=0x%08x "
	    "%s\n", rc, rc == EIO ? "EIO" : "?", out,
	    (rc == EIO && out == 0xcc) ? "-- LEAK BLOCKED (discarded)" :
	    "-- UNEXPECTED");

	/* happy path: full 1-byte DATA stage */
	ufm_shim_mode = 2;
	out = 0xcc;
	rc = fm_prod_ioctl(FM_GET_STAT, &out, FREAD | FWRITE);
	kprintf("UFM5578: FULL FM_GET_STAT full   : rc=%d out=0x%08x %s\n",
	    rc, out, (rc == 0 && out == ufm_shim_devbyte) ?
	    "-- device byte delivered" : "-- UNEXPECTED");

	/* counterfactual through the full dispatch: err==0, nothing copied;
	 * the stale byte (poison and/or usb_pause/tsleep residue) is handed
	 * to the ioctl caller (ufm.c:292). */
	ufm_shim_mode = 3;
	out = 0xcc;
	fm_poison(0x81, fm_depth);
	rc = fm_prod_ioctl(FM_GET_STAT, &out, FREAD | FWRITE);
	kprintf("UFM5578: FULL FM_GET_STAT shortok: rc=%d out=0x%08x %s\n",
	    rc, out, (rc == 0 && (out & 0xff) != 0xcc) ?
	    "-- STALE STACK BYTE REACHES IOCTL CALLER" : "-- UNEXPECTED");
}

static void
fm_capture(void)
{
	static const int freqs[] = { 100700000, 87500000, 108000000, 0,
	    2147483647, -2147483647 - 1 };
	struct ufm_shim_cap cap[16];
	int out, rc, i, n;

	/* FM_SET_FREQ / FM_GET_FREQ state lifecycle (ufm.c:202-242) */
	for (i = 0; i < (int)(sizeof(freqs) / sizeof(freqs[0])); ++i) {
		int f = freqs[i];

		ufm_shim_cap_reset();
		ufm_shim_mode = 2;
		rc = fm_prod_ioctl(FM_SET_FREQ, &f, FREAD | FWRITE);
		n = ufm_shim_cap_dump(cap, 16);
		kprintf("UFM5578: SET_FREQ %d Hz -> rc=%d, %d reqs: "
		    "SET_FREQ bReq=%u wVal=0x%04x wIdx=0x%04x ; "
		    "CMD0 bReq=%u wVal=0x%04x wIdx=0x%04x (all bm=0x%02x "
		    "wLen=%u)\n", f, rc, n, cap[0].breq, cap[0].wval,
		    cap[0].widx, cap[1].breq, cap[1].wval, cap[1].widx,
		    cap[0].bm, cap[0].wlen);

		out = -1;
		rc = fm_prod_ioctl(FM_GET_FREQ, &out, FREAD | FWRITE);
		kprintf("UFM5578:   GET_FREQ -> rc=%d val=%d (sc_freq==%d "
		    "%s)\n", rc, out, f, out == f ? "OK" : "RACE?");
	}

	/* FM_START / FM_STOP tuples (ufm.c:244-275) */
	ufm_shim_cap_reset();
	ufm_shim_mode = 2;
	rc = fm_prod_ioctl(FM_START, &out, FREAD | FWRITE);
	n = ufm_shim_cap_dump(cap, 16);
	kprintf("UFM5578: FM_START rc=%d: [0] bReq=%u 0x%04x/0x%04x  "
	    "[1] bReq=%u 0x%04x/0x%04x\n", rc, cap[0].breq, cap[0].wval,
	    cap[0].widx, cap[1].breq, cap[1].wval, cap[1].widx);

	ufm_shim_cap_reset();
	rc = fm_prod_ioctl(FM_STOP, &out, FREAD | FWRITE);
	n = ufm_shim_cap_dump(cap, 16);
	kprintf("UFM5578: FM_STOP  rc=%d: [0] bReq=%u 0x%04x/0x%04x  "
	    "[1] bReq=%u 0x%04x/0x%04x\n", rc, cap[0].breq, cap[0].wval,
	    cap[0].widx, cap[1].breq, cap[1].wval, cap[1].widx);

	kprintf("UFM5578: (expected per ufm.c: START=CMD0 0x00/0xc7 then "
	    "CMD2 0x01/0x00; STOP=CMD0 0x16/0x1c then CMD2 0x00/0x00; "
	    "SET_FREQ then CMD0 0x96/0xb7)\n");
}

static int
fm_modevent(module_t mod, int type, void *data)
{
	int error = 0;

	switch (type) {
	case MOD_LOAD:
		/* mirror ufm_attach()'s softc setup (ufm.c:140-146) */
		memset(&fm_fifo, 0, sizeof(fm_fifo));
		fm_fifo.priv_sc0 = &fm_sc;	/* usb_fifo_softc() target */
		lockinit(&fm_sc.sc_lock, "ufmh", 0, LK_CANRECURSE);

		kprintf("UFM5578: DF-5578 ufm uninit-read harness: "
		    "production ufm.c compiled in (guarded copy, 1 hunk, "
		    "see prod_guard.diff; sha256 845738f2...)\n");
		kprintf("UFM5578: requests seen by shim so far: %d\n",
		    ufm_shim_requests);

		fm_capture();
		fm_full_chain();
		fm_calibrate();
		fm_matrix();

		kprintf("UFM5578: total shim requests: %d\n",
		    ufm_shim_requests);
		kprintf("UFM5578: done\n");
		break;
	case MOD_UNLOAD:
		lockuninit(&fm_sc.sc_lock);
		break;
	default:
		error = EOPNOTSUPP;
		break;
	}
	return (error);
}

static moduledata_t ufm5578_mod = { "ufmunleak", fm_modevent, 0 };
DECLARE_MODULE(ufmunleak, ufm5578_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_DEPEND(ufmunleak, usb, 1, 1, 1);