⬢ DragonFlyBSD Kernel Audit
DF-3665 / df3665.c
← back to finding ↓ download raw
/*
 * DF-3665 PoC: kernel-equivalent reproduction of the divide-by-zero in
 * evergreen_latency_watermark() (sys/dev/drm/radeon/evergreen.c:2048-2049).
 *
 * This is a faithful userspace port of the exact arithmetic the DragonFly
 * kernel executes on every mode set / PM event on Evergreen-class radeon
 * hardware:
 *
 *   atombios_crtc.c:1448  radeon_bandwidth_update(rdev)
 *     -> evergreen.c:2303 evergreen_bandwidth_update()
 *     -> evergreen.c:2323 evergreen_program_watermarks()
 *     -> evergreen.c:2216 evergreen_latency_watermark(&wm_high)
 *     -> evergreen.c:2048 (512 * 8 * 1000) / available_bandwidth   <-- #DE
 *        evergreen.c:2049 (128 * 4 * 1000) / available_bandwidth   <-- #DE
 *
 * The fixed-point helpers are copied verbatim (semantics preserved,
 * including the u32 truncation of fixed20_12) from
 * sys/dev/drm/include/drm/drm_fixed.h:32-63.
 *
 * Trigger 1 (user-controlled): radeon registers NO mode_config->mode_valid
 *   hook (radeon_display.c:1350 radeon_mode_funcs has only fb_create and
 *   output_poll_changed), so drm_mode_validate_driver() reduces to
 *   drm_mode_validate_basic() (drm_modes.c:1114), which only rejects
 *   clock == 0.  A DRM master may submit a mode with clock = 1..~38 kHz;
 *   evergreen_dmif_request_bandwidth() then computes
 *   trunc(32 * (clock/1000) * 0.8) == 0 and available_bandwidth (a min())
 *   becomes 0.
 *
 * Trigger 2 (VBIOS/DPM-controlled): fixed20_12 is a u32;
 *   dfixed_const(wm->yclk) = (mclk*10) << 12 silently wraps for
 *   mclk >= 104857.6 kHz (every real GPU).  For the wrapped residue
 *   windows (e.g. mclk == 524288 / 1048576 / 1572864 kHz exactly, and
 *   bands near k*2^20/10), evergreen_dram_bandwidth() truncates to 0.
 *   Same for sclk in evergreen_data_return_bandwidth().
 *
 * Trigger 3: VBIOS FirmwareInfo reporting mclk/sclk == 0 (the
 *   DF-1708 class) trivially zeroes the bandwidth.
 *
 * Build: cc -O2 -o df3665 df3665.c
 * Run:   ./df3665
 */
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* ---- verbatim from sys/dev/drm/include/drm/drm_fixed.h (u32 semantics) -- */
typedef union dfixed { uint32_t full; } fixed20_12;

#define dfixed_const(A) ((uint32_t)(((A) << 12)))
#define dfixed_mul(A, B) ((uint64_t)((uint64_t)(A).full * (B).full + 2048) >> 12)
#define dfixed_trunc(A) ((A).full >> 12)

static uint32_t dfixed_div(fixed20_12 A, fixed20_12 B)
{
	uint64_t tmp = ((uint64_t)A.full << 13);

	tmp /= B.full;
	tmp += 1;
	tmp /= 2;
	return (uint32_t)tmp;
}

/* ---- verbatim arithmetic of evergreen.c:1929-2017 ---- */
struct evergreen_wm_params {
	uint32_t dram_channels, yclk, sclk, disp_clk;
	uint32_t num_heads;
};

static uint32_t evergreen_dram_bandwidth(struct evergreen_wm_params *wm)
{
	fixed20_12 yclk, dram_channels, eff, bandwidth, a;

	a.full      = dfixed_const(1000);
	yclk.full   = dfixed_const(wm->yclk);
	yclk.full   = dfixed_div(yclk, a);
	dram_channels.full = dfixed_const(wm->dram_channels * 4);
	/* dram_efficiency = 7/10, exactly as evergreen.c:1940-1942 */
	a.full  = dfixed_const(10);
	eff.full = dfixed_const(7);
	eff.full = dfixed_div(eff, a);
	bandwidth.full = dfixed_mul(dram_channels, yclk);
	bandwidth.full = dfixed_mul(bandwidth, eff);
	return dfixed_trunc(bandwidth);
}

static uint32_t evergreen_data_return_bandwidth(struct evergreen_wm_params *wm)
{
	fixed20_12 sclk, eff, bandwidth, a;

	/* return_efficiency = 8/10, exactly as evergreen.c:1976-1981 */
	a.full    = dfixed_const(1000);
	sclk.full = dfixed_const(wm->sclk);
	sclk.full = dfixed_div(sclk, a);
	a.full    = dfixed_const(10);
	eff.full  = dfixed_const(8);
	eff.full  = dfixed_div(eff, a);
	a.full    = dfixed_const(32);
	bandwidth.full = dfixed_mul(a, sclk);
	bandwidth.full = dfixed_mul(bandwidth, eff);
	return dfixed_trunc(bandwidth);
}

static uint32_t evergreen_dmif_request_bandwidth(struct evergreen_wm_params *wm)
{
	fixed20_12 disp_clk, eff, bandwidth, a;

	/* exactly as evergreen.c:1996-2004 */
	a.full        = dfixed_const(1000);
	disp_clk.full = dfixed_const(wm->disp_clk);
	disp_clk.full = dfixed_div(disp_clk, a);
	a.full        = dfixed_const(10);
	eff.full      = dfixed_const(8);
	eff.full      = dfixed_div(eff, a);
	a.full        = dfixed_const(32);
	bandwidth.full = dfixed_mul(a, disp_clk);
	bandwidth.full = dfixed_mul(bandwidth, eff);
	return dfixed_trunc(bandwidth);
}

static uint32_t evergreen_available_bandwidth(struct evergreen_wm_params *wm)
{
	uint32_t dram_bandwidth = evergreen_dram_bandwidth(wm);
	uint32_t data_return_bandwidth = evergreen_data_return_bandwidth(wm);
	uint32_t dmif_req_bandwidth = evergreen_dmif_request_bandwidth(wm);
	uint32_t m = data_return_bandwidth < dmif_req_bandwidth ?
			data_return_bandwidth : dmif_req_bandwidth;
	return dram_bandwidth < m ? dram_bandwidth : m;
}

static void show(const char *label, uint32_t mclk_khz, uint32_t sclk_khz,
		  uint32_t disp_clk_khz, uint32_t ch)
{
	struct evergreen_wm_params wm = {
		.dram_channels = ch,
		.yclk = mclk_khz * 10,	/* evergreen.c:2164-2169 */
		.sclk = sclk_khz * 10,
		.disp_clk = disp_clk_khz,	/* evergreen.c:2172: mode->clock */
		.num_heads = 1,
	};
	uint32_t avail = evergreen_available_bandwidth(&wm);

	printf("%-52s mclk=%7u sclk=%7u clock=%7u -> available_bandwidth=%u\n",
	       label, mclk_khz, sclk_khz, disp_clk_khz, avail);

	if (avail == 0) {
		/* exactly what evergreen.c:2048 does in-kernel:
		 *   u32 worst_chunk_return_time = (512*8*1000) / available_bandwidth;
		 * A userspace SIGFPE here is the same #DE fault that panics the
		 * kernel.  The divisor is fed through a pipe so the compiler
		 * cannot const-fold it; a child performs the division so the
		 * demo can continue. */
		int pfd[2];
		if (pipe(pfd) == 0) {
			pid_t pid = fork();
			if (pid == 0) {
				uint32_t d = 0x55555555u, r;
				signal(SIGFPE, SIG_DFL);
				close(pfd[1]);
				if (read(pfd[0], &d, sizeof(d)) != (ssize_t)sizeof(d))
					_exit(40);
				close(pfd[0]);
				r = (512 * 8 * 1000) / d;
				/* exit code depends on r in a way the
				 * compiler cannot range-fold, so the real
				 * division must be emitted */
				_exit(43 ^ (int)(r & 1u));
			} else {
				int st;
				close(pfd[0]);
				if (write(pfd[1], &avail, sizeof(avail)) ==
				    (ssize_t)sizeof(avail))
					;	/* sent */
				close(pfd[1]);
				waitpid(pid, &st, 0);
				printf("    -> evergreen.c:2048 division executed: "
				       "%s (kernel: #DE -> panic)\n",
				       WIFSIGNALED(st) && WTERMSIG(st) == SIGFPE ?
				       "child killed by SIGFPE" :
				       "child survived (unexpected)");
			}
		}
	}
}

int main(void)
{
	printf("== DF-3665: evergreen_latency_watermark divisor simulation "
	       "(u32 fixed20_12, verbatim math) ==\n\n");

	printf("[controls - typical sane values, no trap]\n");
	show("HD5870-like",		1200000, 850000, 148500, 4);
	show("HD5770-like",		400000, 850000, 74250, 2);
	show("sumo IGP-like",		533000, 400000, 74250, 2);

	printf("\n[trigger 1: DRM-master user mode, clock<=38 kHz "
	       "(drm_mode_validate_basic only rejects clock==0)]\n");
	show("user mode clock=25 kHz (mclk/sclk sane)", 1200000, 850000, 25, 4);
	show("user mode clock=1 kHz",			    1200000, 850000, 1, 4);
	show("user mode clock=38 kHz",			    1200000, 850000, 38, 4);
	show("user mode clock=39 kHz (just above threshold)",   1200000, 850000, 39, 4);

	printf("\n[trigger 2: u32 wrap windows, BIOS/DPM-supplied clocks]\n");
	show("mclk exactly 524288 kHz (10*2^20/10 wraps to 0)", 524288, 850000, 148500, 4);
	show("mclk exactly 1048576 kHz",		   1048576, 850000, 148500, 4);
	show("mclk exactly 1572864 kHz",		   1572864, 850000, 148500, 4);
	show("mclk 419431 kHz (wrap band k=4)",		    419431, 850000, 148500, 4);

	printf("\n[trigger 3: VBIOS reports zero clocks (DF-1708 class)]\n");
	show("mclk=0 (no FirmwareInfo mem clock)",		    0, 850000, 148500, 4);
	show("sclk=0",					   1200000,      0, 148500, 4);

	printf("\nVERDICT: any available_bandwidth==0 line above is a kernel\n"
	       "divide-by-zero (#DE) panic on the next evergreen_bandwidth_update()\n"
	       "i.e. the next mode set (atombios_crtc.c:1448) or PM clock change.\n");
	return 0;
}