⬢ DragonFlyBSD Kernel Audit
DF-3855 / harness.c
← back to finding ↓ download raw
/*
 * DF-3855 harness - external-TMDS init parser desynchronisation:
 *                   non-IGP "case 0" record forgets "index += 4".
 *
 * Kernel path (replicated 1:1):
 *   sys/dev/drm/radeon/radeon_legacy_encoders.c:985
 *        if (!radeon_combios_external_tmds_setup(encoder)) ...
 *   sys/dev/drm/radeon/radeon_combios.c:2948-2998, non-IGP branch:
 *        index = offset + 10;
 *        id = RBIOS16(index);
 *        while (id != 0xffff) {
 *                index += 2;                 [skip id word]
 *                switch (id >> 13) {
 *                case 0: reg = (id & 0x1fff) * 4;
 *                        val = RBIOS32(index);
 *                        WREG32(reg, val);   [MISSING index += 4]
 *                        break;
 *                case 2: ... index += 4; ... index += 4; ...
 *
 * The IGP branch of the same function (radeon_combios.c:2900-2905) DOES
 * advance "index += 4" after a case-0 value - the non-IGP branch is
 * missing the advance, so after the first plain register write the parser
 * re-reads the low half of the just-written VALUE as the next command id.
 * A well-formed terminator (0xffff) placed after the value is consumed as
 * operand bytes instead; the stream desynchronises and subsequent words
 * (including data words and whatever follows the table) are executed as
 * commands - MMIO writes / PLL RMWs driven by data.
 *
 * Upstream parity: verified identical (missing advance) in Linux v5.10
 * drivers/gpu/drm/radeon/radeon_combios.c:2965-2969 - inherited, not a
 * DragonFly port regression.
 */
#include <stdio.h>
#include <stdint.h>
#include <string.h>

static uint8_t bios[256];
#define RBIOS8(i)  (bios[i])
#define RBIOS16(i) (RBIOS8(i) | (RBIOS8((i) + 1) << 8))
#define RBIOS32(i) (RBIOS16(i) | (RBIOS16((i) + 2) << 16))

static int writes;
static void WREG32(uint32_t reg, uint32_t v)
{
	writes++;
	printf("    WREG32(0x%04x, 0x%08x)\n", reg, v);
}

/* radeon_combios.c:2948 non-IGP walk, bug included (case 0: no advance) */
static void ext_tmds_setup_nonigp_buggy(uint16_t offset)
{
	uint32_t index = offset + 10;
	uint32_t id = RBIOS16(index);

	printf("  [non-IGP, buggy] walking from 0x%04x:\n", (unsigned)index);
	while (id != 0xffff) {
		uint32_t reg, val, and_mask, or_mask;
		index += 2;
		switch (id >> 13) {
		case 0:
			reg = (id & 0x1fff) * 4;
			val = RBIOS32(index);
			printf("  case 0 (direct write): consumes 4-byte value but DOES NOT advance index\n");
			WREG32(reg, val);
			break;                              /* <-- missing index += 4 */
		case 2:
			reg = (id & 0x1fff) * 4;
			and_mask = RBIOS32(index); index += 4;
			or_mask  = RBIOS32(index); index += 4;
			printf("  case 2 (RMW): reg 0x%04x and 0x%08x or 0x%08x\n",
			       reg, and_mask, or_mask);
			WREG32(reg, (0 & and_mask) | or_mask);
			break;
		case 4:
			val = RBIOS16(index); index += 2;
			printf("  case 4 (udelay %u)\n", val);
			break;
		default:
			printf("  unknown id class %u\n", id >> 13);
			break;
		}
		id = RBIOS16(index);                    /* re-reads value bytes   */
	}
	printf("  terminated at index 0x%04x\n", (unsigned)index);
}

/* same walk with the fix (advance after case-0 value) */
static void ext_tmds_setup_fixed(uint16_t offset)
{
	uint32_t index = offset + 10;
	uint32_t id = RBIOS16(index);

	printf("  [with index += 4 fix]:\n");
	while (id != 0xffff) {
		uint32_t reg, val;
		index += 2;
		switch (id >> 13) {
		case 0:
			reg = (id & 0x1fff) * 4;
			val = RBIOS32(index);
			index += 4;                         /* the fix              */
			WREG32(reg, val);
			break;
		default:
			printf("  unexpected class in fixed walk\n");
			return;
		}
		id = RBIOS16(index);
	}
	printf("  terminated at index 0x%04x (at the 0xffff terminator)\n",
	       (unsigned)index);
}

int main(void)
{
	uint16_t offset = 0x20;

	/* well-formed two-record stream + terminator:
	 *   rec1: case 0 write reg 0x0040 <- 0x55555555
	 *   term: 0xffff                                                   */
	memset(bios, 0, sizeof(bios));
	bios[offset + 10]     = 0x01; bios[offset + 11] = 0x00;   /* id: class0, reg 1   */
	bios[offset + 12]     = 0x55; bios[offset + 13] = 0x55;   /* value lo = 0x5555   */
	bios[offset + 14]     = 0x55; bios[offset + 15] = 0x55;   /* value hi           */
	bios[offset + 16]     = 0xff; bios[offset + 17] = 0xff;   /* terminator         */
	/* following bytes (0) get executed as commands by the buggy walk:   */
	bios[offset + 18]     = 0x01; bios[offset + 19] = 0x00;
	bios[offset + 20]     = 0xaa; bios[offset + 21] = 0xaa;
	bios[offset + 22]     = 0xaa; bios[offset + 23] = 0xaa;
	bios[offset + 24]     = 0xff; bios[offset + 25] = 0xff;   /* real terminator 2  */

	printf("== DF-3855: well-formed ext-TMDS stream, terminator at +16 ==\n");
	writes = 0;
	ext_tmds_setup_nonigp_buggy(offset);
	printf("  buggy walk executed %d MMIO write(s) and ran PAST the "
	       "terminator (value 0x5555 re-read as id -> extra case-0 write of "
	       "0xaaaaaa data)\n", writes);

	writes = 0;
	ext_tmds_setup_fixed(offset);
	printf("  fixed walk executed %d MMIO write(s) and stopped at the "
	       "terminator\n", writes);
	return 0;
}