โฌข DragonFlyBSD Kernel Audit
DF-4196 / vblank_toctou.c
โ† back to finding โ†“ download raw
/*
 * vblank_toctou.c โ€” DF-4196 model harness.
 *
 * Proves the divide-by-zero (#DE => in-kernel panic) reachable through the
 * lock-ordering hole between:
 *
 *   drm_crtc_vblank_off()                drm_vblank.c:1115-1171
 *     ...
 *     spin_unlock_irqrestore(&dev->event_lock)      :1165
 *     vblank->hwmode.crtc_clock = 0;                :1169   <-- LOCK-FREE
 *
 *   drm_calc_vbltimestamp_from_scanoutpos()  drm_vblank.c:579-693
 *     if (mode->crtc_clock == 0) return false;      :618    <-- check
 *     vbl_status = dev->driver->get_scanout_position(...)  :637  (opaque
 *                                   external call => compiler MUST reload
 *                                   mode->crtc_clock for the division)
 *     delta_ns = div_s64(1000000LL * (vpos * mode->crtc_htotal + hpos),
 *                       mode->crtc_clock);          :671-672 <-- divide
 *
 * The racing reader path drm_vblank_get() -> drm_vblank_enable() (holds
 * dev->vbl_lock + dev->vblank_time_lock, :988/:946) -> drm_update_vblank_count
 * (:961) -> drm_get_last_vbltimestamp -> drm_calc_vbltimestamp_from_scanoutpos
 * holds NEITHER event_lock NOR any lock the :1169 store takes, so the store
 * can land between :618 and :671 and the reloaded divisor is 0.
 *
 * This harness reproduces the exact interleaving with two threads + a barrier
 * (the check passes on the old value; the lock-free store lands; the division
 * reloads). The divide-by-zero manifests as SIGFPE โ€” the same exception the
 * kernel takes as #DE (panic: integer divide fault).
 *
 * Modes:
 *   ./vblank_toctou race      vulnerable interleaving (expect SIGFPE)
 *   ./vblank_toctou race_fixed  same interleaving with the fix applied
 *                             (crtc_clock cached into a local before the
 *                             check, division uses the local) -> clean
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct drm_display_mode_like {
	int crtc_clock;		/* kHz */
	int crtc_htotal;
};

struct vblank_like {
	struct drm_display_mode_like hwmode;
};

static struct vblank_like vblank;
static int vpos, hpos;		/* filled by the fake get_scanout_position */
static pthread_barrier_t barrier;
static volatile int check_passed, store_done;

/* stands in for dev->driver->get_scanout_position(): an OPAQUE external
 * call receiving `mode` โ€” the compiler cannot assume *mode unchanged */
static __attribute__((noinline))
int get_scanout_position(struct drm_display_mode_like *mode,
			 int *v, int *h)
{
	(void)mode;
	/* wait until the vblank_off thread executes its lock-free store
	 * (drm_vblank.c:1169) โ€” models the multi-microsecond MMIO window */
	pthread_barrier_wait(&barrier);
	while (!store_done)
		__asm__ __volatile__("pause");
	*v = 100;
	*h = 50;
	return 1;
}

/* verbatim structure of drm_calc_vbltimestamp_from_scanoutpos() :618-677
 * (retry loop elided; vbl_status handling identical) */
static __attribute__((noinline))
long long calc_vbltimestamp_from_scanoutpos(struct drm_display_mode_like *mode,
					    int fixed)
{
	int v, h;
	long long delta_ns;
	int crtc_clock_cached;

	if (fixed) {
		/* FIX: cache the divisor before the check */
		crtc_clock_cached = mode->crtc_clock;
		if (crtc_clock_cached == 0) {
			printf("[calc] FIXED path: cached crtc_clock==0 -> graceful failure (return false)\n");
			fflush(stdout);
			return -1;
		}
	} else {
		if (mode->crtc_clock == 0) {	/* :618 */
			printf("[calc] crtc_clock==0 at check -> graceful failure\n");
			fflush(stdout);
			return -1;
		}
	}
	check_passed = 1;	/* reader is past :618, now inside the MMIO window */

	if (!get_scanout_position(mode, &v, &h))	/* :637 */
		return -1;

	if (fixed) {
		/* :671-672 with the cached local */
		delta_ns = 1000000LL * ((long long)(v * mode->crtc_htotal + h))
			   / crtc_clock_cached;
	} else {
		/* :671-672 โ€” reloads mode->crtc_clock (opaque call above) */
		delta_ns = 1000000LL * ((long long)(v * mode->crtc_htotal + h))
			   / mode->crtc_clock;
	}
	return delta_ns;
}

static void *crtc_off_thread(void *arg)
{
	(void)arg;
	/* drm_crtc_vblank_off() tail:
	 *   spin_unlock_irqrestore(&dev->event_lock)   :1165
	 *   vblank->hwmode.crtc_clock = 0;             :1169  (lock-free) */
	pthread_barrier_wait(&barrier);	/* reader just passed :618 */
	printf("[vblank_off] event_lock released; executing lock-free "
	       "vblank->hwmode.crtc_clock = 0 (drm_vblank.c:1169)\n");
	fflush(stdout);
	vblank.hwmode.crtc_clock = 0;
	store_done = 1;
	return NULL;
}

int main(int argc, char **argv)
{
	int fixed = argc > 1 && strcmp(argv[1], "race_fixed") == 0;
	pthread_t off;

	printf("[main] mode=%s  (initial hwmode.crtc_clock=148500 kHz, htotal=2200)\n",
	       fixed ? "race_fixed" : "race");
	fflush(stdout);

	vblank.hwmode.crtc_clock = 148500;
	vblank.hwmode.crtc_htotal = 2200;
	pthread_barrier_init(&barrier, NULL, 2);

	pthread_create(&off, NULL, crtc_off_thread, NULL);

	long long r = calc_vbltimestamp_from_scanoutpos(&vblank.hwmode, fixed);
	pthread_join(off, NULL);

	if (r >= 0)
		printf("[main] SURVIVED: delta_ns=%lld (check passed BEFORE zero store, "
		       "division used a %s value)\n",
		       r, fixed ? "cached-nonzero" : "RELOADED-NONZERO?!");
	else
		printf("[main] SURVIVED: calc failed gracefully\n");
	return 0;
}