DragonFlyBSD Kernel Audit
DF-1137 / harness.c
← back to finding ↓ download raw
/*
 * DF-1137 harness: radeon/si_dpm.c si_thermal_setup_fan_table divide-by-zero
 *
 * This is a userspace logic harness (the actual bug is in radeon.ko which is
 * only loaded when AMD SI hardware is present; the QEMU audit guest has no AMD
 * GPU so radeon.ko is never kldload'ed at runtime). The harness demonstrates
 * the divisor reaches zero with BIOS-controlled u16 fan-table inputs that are
 * never validated before being used as divisors.
 *
 * Reproduces the integer division that traps as #DE in kernel context.
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness
 */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <string.h>

/* Mirror of struct radeon_fan_info fields as filled by r600_dpm.c from VBIOS:
 *   fan.t_min = le16_to_cpu(fan_info->fan.usTMin);    r600_dpm.c:897
 *   fan.t_med = le16_to_cpu(fan_info->fan.usTMed);    r600_dpm.c:898
 *   fan.t_high= le16_to_cpu(fan_info->fan.usTHigh);   r600_dpm.c:899
 */
struct fan { uint16_t t_min, t_med, t_high, pwm_min, pwm_med, pwm_high; };

/* Faithful copy of si_dpm.c:6046-6064 math.
 * duty100 is computed at runtime from a MMIO register (CG_FDO_CTRL1); we use
 * a typical non-zero value (50) to isolate the t_diff divisor. */
static void si_thermal_setup_fan_table_buggy(struct fan *fan, uint32_t duty100)
{
	int32_t t_diff1, t_diff2;
	int32_t pwm_diff1, pwm_diff2;
	int32_t slope1, slope2;

	/* si_dpm.c:6057-6058 -- NO validation */
	t_diff1 = fan->t_med - fan->t_min;
	t_diff2 = fan->t_high - fan->t_med;

	pwm_diff1 = fan->pwm_med - fan->pwm_min;
	pwm_diff2 = fan->pwm_high - fan->pwm_med;

	/* si_dpm.c:6063-6064 -- divide-by-zero if t_diff1 or t_diff2 == 0 */
	slope1 = (int32_t)((50 + ((16 * duty100 * pwm_diff1) / t_diff1)) / 100);
	slope2 = (int32_t)((50 + ((16 * duty100 * pwm_diff2) / t_diff2)) / 100);
	printf("  buggy: slope1=%d slope2=%d\n", slope1, slope2);
}

/* Faithful copy of the fix: validate deltas before use */
static int si_thermal_setup_fan_table_fixed(struct fan *fan, uint32_t duty100)
{
	int32_t t_diff1, t_diff2;

	t_diff1 = fan->t_med - fan->t_min;
	t_diff2 = fan->t_high - fan->t_med;

	if (t_diff1 == 0 || t_diff2 == 0) {
		printf("  fixed: REJECTED zero delta (t_min=%u t_med=%u t_high=%u) -> -EINVAL\n",
		       fan->t_min, fan->t_med, fan->t_high);
		return -1;
	}
	si_thermal_setup_fan_table_buggy(fan, duty100);
	return 0;
}

static sigjmp_buf jb;
static void fpe_handler(int s) { (void)s; siglongjmp(jb, 1); }

int main(void)
{
	struct sigaction sa = { .sa_handler = fpe_handler };
	struct fan crafted_vbios = { .t_min = 5000, .t_med = 5000, .t_high = 9000,
		                     .pwm_min = 100, .pwm_med = 200, .pwm_high = 300 };
	uint32_t duty100 = 50;

	sigaction(SIGFPE, &sa, NULL);

	printf("=== DF-1137 harness: crafted VBIOS with usTMed==usTMin ===\n");
	printf("attempting buggy version (mirrors si_dpm.c:6046-6064)...\n");
	if (sigsetjmp(jb, 1) == 0) {
		si_thermal_setup_fan_table_buggy(&crafted_vbios, duty100);
		printf("  (no trap -- unexpected)\n");
	} else {
		printf("  buggy: SIGFPE / #DE divide-by-zero raised!\n");
		printf("  >>> In kernel context this is a kernel panic. <<<\n");
	}

	printf("\nattempting fixed version (mirrors fix.diff)...\n");
	si_thermal_setup_fan_table_fixed(&crafted_vbios, duty100);

	printf("\nfixed version with benign VBIOS (t_min<t_med<t_high)...\n");
	struct fan good = { .t_min = 5000, .t_med = 7000, .t_high = 9000,
		           .pwm_min = 100, .pwm_med = 200, .pwm_high = 300 };
	si_thermal_setup_fan_table_fixed(&good, duty100);

	return 0;
}