⬢ DragonFlyBSD Kernel Audit
DF-3573 / fwsize_bypass.c
← back to finding ↓ download raw
/*
 * DF-3573 trigger: legacy-firmware size-check bypass in cik_init_microcode()
 *
 * This is a userland transcription of the *exact* acceptance state machine of
 * sys/dev/drm/radeon/cik.c:cik_init_microcode() (DragonFly BSD tree, lines
 * 2049-2272, CHIP_BONAIRE / non-IGP path), followed by a simulation of the
 * legacy ucode load loops that consume the accepted firmware
 * (cik_cp_gfx_load_microcode cik.c:3960-3983, cik_cp_compute_load_microcode
 * cik.c:4312-4329, cik_rlc_resume cik.c:5979-6004; constants from
 * radeon_ucode.h:38-66).
 *
 * It exists because the audit guest is virtio-only (no CIK radeon hardware),
 * so the kernel path cannot run; this harness proves the control-flow bug
 * deterministically: a legacy firmware file with a WRONG (truncated) size is
 * ACCEPTED (err==0, kept in rdev->X_fw) whenever any later request_firmware()
 * succeeds and overwrites err -- and the legacy loaders then read
 * CIK_*_UCODE_SIZE*4 bytes out of the smaller allocation (heap OOB read).
 *
 * Build:  cc -O -o fwsize_bypass fwsize_bypass.c        (baseline, stock code)
 *         cc -O -DFIXED -o fwsize_bypass_fixed ...      (with the fix.diff applied)
 * Run:    ./fwsize_bypass
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>

/* ---- constants transcribed from sys/dev/drm/radeon/radeon_ucode.h ---- */
#define CIK_PFP_UCODE_SIZE      2144   /* radeon_ucode.h:38 */
#define CIK_ME_UCODE_SIZE       2144   /* radeon_ucode.h:39 */
#define CIK_CE_UCODE_SIZE       2144   /* radeon_ucode.h:40 */
#define CIK_MEC_UCODE_SIZE      4192   /* radeon_ucode.h:43 */
#define BONAIRE_RLC_UCODE_SIZE  2048   /* radeon_ucode.h:52 */
#define CIK_SDMA_UCODE_SIZE     1050   /* cik_sdma.c:33 */
#define BONAIRE_MC_UCODE_SIZE   7866   /* radeon_ucode.h:65 */
#define BONAIRE_MC2_UCODE_SIZE  7948   /* radeon_ucode.h:66 */

/* one firmware "file" on disk: name + actual size */
struct fw_file {
	const char *name;	/* radeonkmsfw_bonaire_<x> (new lower-case path) */
	size_t datasize;	/* bytes actually present */
	int exists;		/* request_firmware() success if exists */
};

/* per-fw driver state after cik_init_microcode() */
struct fw_state {
	const char *var;	/* rdev-><x>_fw */
	size_t datasize;	/* 0 == NULL/rejected */
	int accepted;
};

/*
 * cik_init_microcode() for CHIP_BONAIRE, new-style (lower-case) files ABSENT
 * so every block takes the legacy branch -- exactly like a stock system whose
 * /boot/kernel only has radeonkmsfw_BONAIRE_*.ko (uppercase legacy names).
 * This mirrors cik.c:2049-2272 statement for statement:
 *   - err = -EINVAL after "Bogus length" WITHOUT goto out (all but pfp),
 *   - err re-assigned by every subsequent request/validate,
 *   - final `if (new_fw == 0) ... else if (new_fw < num_fw) ... else`.
 * `files` holds the scenario; `out` receives the installed fw table.
 */
static int
cik_init_microcode_bonaire_legacy(struct fw_file files[], int nfiles,
    struct fw_state out[], int nout, const char **installed_names,
    int *ninstalled, const char **err_last_fw)
{
	int new_fw = 0, err = 0, num_fw = 8;
	size_t pfp_req = CIK_PFP_UCODE_SIZE * 4, me_req = CIK_ME_UCODE_SIZE * 4;
	size_t ce_req = CIK_CE_UCODE_SIZE * 4, mec_req = CIK_MEC_UCODE_SIZE * 4;
	size_t rlc_req = BONAIRE_RLC_UCODE_SIZE * 4, sdma_req = CIK_SDMA_UCODE_SIZE * 4;
	size_t mc_req = BONAIRE_MC_UCODE_SIZE, mc2_req = BONAIRE_MC2_UCODE_SIZE;
	struct fw_file *f;
	int i;

	struct fw_file *find_fw(struct fw_file *, int, const char *);
	struct fw_file dummy = { NULL, 0, 0 };
#define FIND(var) (find_fw(files, nfiles, var))

	(void)nfiles; (void)installed_names; (void)ninstalled;
	for (i = 0; i < nout; i++)
		memset(&out[i], 0, sizeof(out[0]));
	*ninstalled = 0;

	/*
	 * Each stage models cik.c exactly:
	 *   err = request_firmware(...)   <-- err = 0 ON SUCCESS (the clobber!)
	 *   if (err) goto out;
	 *   if (datasize != req) { ...; err = -22; [goto out;] }
	 * Lower-case (new-style) modules are absent, so the fallback to the
	 * legacy uppercase name succeeds and takes the legacy branch.
	 */
#define STAGE(var, idx) do {						\
	f = FIND(var); if (!f) f = &dummy;				\
	err = f->exists ? 0 : -1;	/* err = request_firmware() */	\
	if (err) goto out;						\
} while (0)
#define BOGUS(var, req) do {						\
	if (f->datasize != (req)) {					\
		warnx("[" var "] Bogus length %zu (req %zu)",		\
		    f->datasize, (size_t)(req));			\
		err = -22;						\
		goto out;	/* parity with cik.c:2060 (pfp) */		\
	}								\
} while (0)
#define BOGUS_NOGOTO(var, req) do {					\
	if (f->datasize != (req)) {					\
		warnx("[" var "] Bogus length %zu (req %zu)",		\
		    f->datasize, (size_t)(req));			\
		err = -22;		/* stock cik.c: NO goto out */		\
	}								\
} while (0)
#define KEEP(idx, varname) do {						\
	out[(idx)].var = (varname); out[(idx)].datasize = f->datasize;	\
	out[(idx)].accepted = 1;					\
} while (0)

	/* ---- pfp (cik.c:2049-2071): bogus length HAS goto out ---- */
	STAGE("pfp", 0);
	BOGUS("pfp", pfp_req);
	KEEP(0, "pfp_fw");

	/* ---- me (cik.c:2073-2094): bogus length, NO goto out ---- */
	STAGE("me", 1);
	BOGUS_NOGOTO("me", me_req);
#ifdef FIXED
	if (err == -22) goto out;		/* fix.diff adds goto out */
#endif
	KEEP(1, "me_fw");

	/* ---- ce (cik.c:2096-2117) ---- */
	STAGE("ce", 2);
	BOGUS_NOGOTO("ce", ce_req);
#ifdef FIXED
	if (err == -22) goto out;
#endif
	KEEP(2, "ce_fw");

	/* ---- mec (cik.c:2119-2140) ---- */
	STAGE("mec", 3);
	BOGUS_NOGOTO("mec", mec_req);
#ifdef FIXED
	if (err == -22) goto out;
#endif
	KEEP(3, "mec_fw");

	/* KAVERI-only mec2 skipped (BONAIRE) */

	/* ---- rlc (cik.c:2157-2178) ---- */
	STAGE("rlc", 4);
	BOGUS_NOGOTO("rlc", rlc_req);
#ifdef FIXED
	if (err == -22) goto out;
#endif
	KEEP(4, "rlc_fw");

	/* ---- sdma (cik.c:2180-2201) ---- */
	STAGE("sdma", 5);
	BOGUS_NOGOTO("sdma", sdma_req);
#ifdef FIXED
	if (err == -22) goto out;
#endif
	KEEP(5, "sdma_fw");

	/* ---- mc (cik.c:2205-2232), non-IGP ---- */
	STAGE("mc", 6);
	if (f->datasize != mc_req && f->datasize != mc2_req) {
		warnx("[mc] Bogus length %zu (req %zu|%zu)", f->datasize,
		    mc_req, mc2_req);
		err = -22;
#ifdef FIXED
		goto out;
#endif
	}
	KEEP(6, "mc_fw");

	/* ---- smc (cik.c:2234-2262); new_smc=false for this BONAIRE stepping */
	STAGE("smc", 7);
	if (0) {			/* size check elided: file is stock */
	}
	KEEP(7, "smc_fw");

	/* ---- final new_fw decision (cik.c:2265-2272) ---- */
	if (new_fw == 0) {
		/* rdev->new_fw = false; err keeps whatever the LAST assignment left */
	} else if (new_fw < num_fw) {
		warnx("ci_fw: mixing new and old firmware!");
		err = -22;
	} else {
		/* rdev->new_fw = true */
	}
	/* fall through to out: */
out:
	/*
	 * cik.c:2275-2297: `out:` releases every firmware and NULLs the
	 * pointers when err != 0 -- model that: nothing survives an error.
	 */
	if (err != 0)
		for (i = 0; i < 8; i++) {
			out[i].datasize = 0;
			out[i].accepted = 0;
		}
	for (i = 0; i < 8; i++)
		if (out[i].accepted)
			installed_names[(*ninstalled)++] = out[i].var;
	*err_last_fw = NULL;
	return err;
#undef STAGE
#undef BOGUS
#undef BOGUS_NOGOTO
#undef KEEP
}

struct fw_file *
find_fw(struct fw_file *files, int n, const char *suffix)
{
	char name[64];
	int i;
	snprintf(name, sizeof(name), "radeonkmsfw_bonaire_%s", suffix);
	for (i = 0; i < n; i++)
		if (strcmp(files[i].name, name) == 0)
			return (&files[i]);
	return (NULL);
}

/*
 * Simulate the LEGACY load loops (rdev->new_fw == false) and report the
 * out-of-bounds span each accepted firmware would suffer:
 *   gfx:  cik.c:3966/3973/3980  -- CIK_{PFP,CE,ME}_UCODE_SIZE dwords
 *   mec:  cik.c:4318/4326       -- CIK_MEC_UCODE_SIZE dwords
 *   rlc:  cik.c:6001-6002       -- BONAIRE_RLC_UCODE_SIZE dwords
 *   sdma: cik_sdma.c:501/508    -- CIK_SDMA_UCODE_SIZE dwords
 *   mc:   cik.c:1880            -- datasize/4 (SELF-BOUNDED, safe)
 * The loops index fw_data++ from rdev->X_fw->data -- reading
 * (CONSTANT*4 - datasize) bytes past the allocation when datasize < req.
 */
static void
simulate_legacy_loaders(struct fw_state out[], int nout)
{
	static const struct { const char *var; size_t dwords; const char *loop; } legacy[] = {
		{ "pfp_fw", CIK_PFP_UCODE_SIZE,     "cik.c:3966" },
		{ "ce_fw",  CIK_CE_UCODE_SIZE,      "cik.c:3973" },
		{ "me_fw",  CIK_ME_UCODE_SIZE,      "cik.c:3980" },
		{ "mec_fw", CIK_MEC_UCODE_SIZE,     "cik.c:4318,4326" },
		{ "rlc_fw", BONAIRE_RLC_UCODE_SIZE, "cik.c:6001" },
		{ "sdma_fw", CIK_SDMA_UCODE_SIZE,   "cik_sdma.c:501,508" },
	};
	int i, j;

	for (i = 0; i < nout; i++) {
		if (!out[i].accepted)
			continue;
		for (j = 0; j < (int)(sizeof(legacy)/sizeof(legacy[0])); j++) {
			if (strcmp(out[i].var, legacy[j].var) != 0)
				continue;
			size_t want = legacy[j].dwords * 4;
			if (out[i].datasize < want) {
				printf("    OOB READ: %s holds %zu bytes, %s loop "
				    "reads %zu -> %zd bytes past allocation\n",
				    out[i].var, out[i].datasize, legacy[j].loop,
				    want, (ssize_t)(want - out[i].datasize));
			} else {
				printf("    ok: %s (%zu bytes >= %zu)\n",
				    out[i].var, out[i].datasize, want);
			}
		}
		/* mc_fw: legacy loader uses datasize/4 -- never OOB */
		if (strcmp(out[i].var, "mc_fw") == 0)
			printf("    ok: mc_fw (legacy loader bounded by datasize, cik.c:1880)\n");
	}
}

static void
run_scenario(const char *title, struct fw_file files[], int nfiles)
{
	struct fw_state out[8];
	const char *names[8];
	int ninstalled = 0, err, i;
	const char *last = NULL;

	printf("== %s ==\n", title);
	err = cik_init_microcode_bonaire_legacy(files, nfiles, out, 8,
	    names, &ninstalled, &last);
	printf("  cik_init_microcode -> err=%d (%s), installed fw: ", err,
	    err == 0 ? "ACCEPTED" : "rejected");
	if (ninstalled == 0)
		printf("(none)");
	for (i = 0; i < ninstalled; i++) {
		struct fw_state *s = NULL; int k;
		for (k = 0; k < 8; k++) if (out[k].accepted && strcmp(out[k].var, names[i])==0) s=&out[k];
		printf("%s(%zuB)%s", names[i], s ? s->datasize : 0,
		    i + 1 < ninstalled ? ", " : "");
	}
	printf("\n");
	if (err == 0 && ninstalled)
		simulate_legacy_loaders(out, 8);
	else if (err == 0)
		printf("  (no fw kept: all rejected)\n");
	printf("\n");
}

int
main(void)
{
	/* stock sizes for a real BONAIRE system (legacy uppercase modules) */
	struct fw_file stock[] = {
		{ "radeonkmsfw_bonaire_pfp", CIK_PFP_UCODE_SIZE * 4, 1 },
		{ "radeonkmsfw_bonaire_me",  CIK_ME_UCODE_SIZE * 4,  1 },
		{ "radeonkmsfw_bonaire_ce",  CIK_CE_UCODE_SIZE * 4,  1 },
		{ "radeonkmsfw_bonaire_mec", CIK_MEC_UCODE_SIZE * 4, 1 },
		{ "radeonkmsfw_bonaire_rlc", BONAIRE_RLC_UCODE_SIZE * 4, 1 },
		{ "radeonkmsfw_bonaire_sdma", CIK_SDMA_UCODE_SIZE * 4, 1 },
		{ "radeonkmsfw_bonaire_mc",  BONAIRE_MC_UCODE_SIZE, 1 },
		{ "radeonkmsfw_bonaire_smc", 0x1FDEC, 1 },
	};

	/* S1: root installs a TRUNCATED legacy me firmware (e.g. bad copy,
	 *     interrupted install, or cross-ASIC mixup); everything else fine */
	struct fw_file me_bad[8];
	memcpy(me_bad, stock, sizeof(stock));
	me_bad[1].datasize = 1024;	/* != CIK_ME_UCODE_SIZE*4 */

	/* S2 (control): truncated pfp -- pfp's bogus-length block HAS goto out */
	struct fw_file pfp_bad[8];
	memcpy(pfp_bad, stock, sizeof(stock));
	pfp_bad[0].datasize = 512;

	/* S3 (control): all stock */
	run_scenario("S3 control: all firmware stock", stock, 8);
	run_scenario("S2 control: truncated PFP (block has goto out)", pfp_bad, 8);
	run_scenario("S1: truncated legacy ME firmware", me_bad, 8);
	return 0;
}