Unvalidated user-controlled datasize/namesize in EFIIOC_VAR_GET/VAR_NEXT/VAR_SET: kmalloc(M_WAITOK) panics on huge sizes; sizes near SIZE_MAX wrap round_page() to 0 and corrupt kernel_map
Summary
The user size_t goes straight into kmalloc(M_WAITOK) in all three VAR ioctls: huge sizes panic 'kmem_slab_alloc(): kernel_map ran out of space!' (M_WAITOK without M_NULLOK panics); sizes near SIZE_MAX wrap round_page() to 0 -> zero-length kernel_map entry + wired-quick panic. Runs BEFORE efi_enter so reachable even on BIOS boxes via kldload efirt (efidev still creates /dev/efi). Both panic variants REPRODUCED deterministically on guest from a single root ioctl; 1MiB-cap fix validated (EINVAL both modes, ENXIO control unchanged). Root-only 0700 device. Also general lesson: any DF ioctl doing kmalloc(M_WAITOK) on a user size inherits panic-or-wrap.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5119 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| efi_sz.c | β | 3.2 KB | view raw | |
| build.sh | β | 128 B | view raw | |
| run.sh | β | 190 B | view raw | |
| build.log | β | 115 B | view raw | |
| run.log | β | 2.3 KB | view raw | |
| panic.txt | β | 1.1 KB | view raw | |
| panic.m1.txt | β | 1.1 KB | view raw | |
| panic.m2.txt | β | 862 B | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| fix_build.log | β | 1.1 KB | view raw | |
| fix_run.log | β | 593 B | view raw | |
| run.patched.log | β | 593 B | view raw | |
| VERDICT.md | β | 4.1 KB | β raw | |
| README.md | β | 3.6 KB | β raw | |
| env.txt | β | 735 B | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-5119 β Unvalidated user-controlled allocation sizes in /dev/efi ioctls (efidev.c)
What
efidev_ioctl() (sys/dev/misc/efirt/efidev.c) passes the user-supplied
struct efi_var_ioc sizes straight into kmalloc(..., M_TEMP, M_WAITOK)
with no upper-bound validation, in all three variable ioctls:
| ioctl | line | call |
|---|---|---|
EFIIOC_VAR_GET |
efidev.c:105 | data = kmalloc(ev->datasize, ...) |
EFIIOC_VAR_GET |
efidev.c:106 | name = kmalloc(ev->namesize, ...) |
EFIIOC_VAR_NEXT |
efidev.c:140 | name = kmalloc(ev->namesize, ...) |
EFIIOC_VAR_SET |
efidev.c:165 | data = kmalloc(ev->datasize, ...) |
EFIIOC_VAR_SET |
efidev.c:166 | name = kmalloc(ev->namesize, ...) |
Two failure modes, both proven on the guest:
-
Huge, non-wrapping size (e.g.
datasize = 0x4000000000000000):_kmalloc()takes the large-allocation path (kern_slaballoc.c:942),kmem_slab_alloc()βvm_map_findspace()fails, and becauseM_WAITOKwas given withoutM_NULLOKthe kernel panics outright:panic("kmem_slab_alloc(): kernel_map ran out of space!")(kern_slaballoc.c:1721-1724). -
Size near
SIZE_MAX(e.g.0xFFFFFFFFFFFFFFFF):round_page()(kern_slaballoc.c:945) wraps to 0, sokmem_slab_alloc(0, ...)runs:vm_map_findspace(len=0)succeeds,vm_map_insertinserts a zero-length entry into kernel_map, and the immediately followingvm_map_set_wired_quick()detects the corrupted map:panic("vm_map_entry_link: dup addr map ..."). (Even if the insert survived, the returned pointer has zero pages mapped and*kup = 0, so the subsequentkfree()at efidev.c:184 would take the SLZone path (kern_slaballoc.c:1475-1478) on a zone-less, unmapped pointer.)
The allocations happen before any EFI runtime call, so the bug is
reachable even on legacy-BIOS systems: kldload efirt reports
module_register_init: MOD_LOAD (efirt, ...) error 6 but the co-packaged
efidev module still registers and creates /dev/efi.
Threat model
/dev/efi is created 0700 root:wheel (efidev.c:205-206), so the trigger
requires root (or a setuid-root helper proxying the ioctl; jails/devfs rules
could also expose the node). Impact: deterministic kernel panic / kernel-map
corruption from one ioctl(2) β privileged-user DoS and a violation of the
kernel's own allocator invariants (same privilege calibration as DF-2121).
Reproduce (guest, as root)
kldload efirt # /dev/efi appears even on BIOS boxes
cc -O -o efi_sz efi_sz.c
./efi_sz 0 # control: ENXIO, guest survives
./efi_sz 1 # panic: kmem_slab_alloc(): kernel_map ran out of space!
# (guest reboots/dies; reset, kldload efirt again)
./efi_sz 2 # panic: vm_map_entry_link: dup addr map ...
Fix
fix.diff (validated on guest): reject datasize/namesize > 1 MiB and
namesize < sizeof(efi_char) in all three VAR ioctls before any allocation
(1 MiB matches Linux efivarfs' cap; real EFI variables are far smaller).
Patched module: both modes return EINVAL, control mode still ENXIO,
guest survives. The namesize >= sizeof(efi_char) clause also closes the
DF-2121 underflow as defense in depth.
Files
efi_sz.cβ PoC (modes 0/1/2) Β·build.sh/run.shbuild.logβ guest build + mode-0 controlrun.logβ baseline transcript (both panics)panic.txt,panic.m1.txt,panic.m2.txtβ serial-console panic excerptsfix_build.log,fix_run.logβ patched-module build + rerun (EINVAL, alive)fix.diffβ the fix (applied to guest /usr/src for validation only)verdict.json,manifest.json,VERDICT.md,env.txt
DF-5119 VERDICT β reproduced (panic), fix validated
Status: reproduced / impact: panic / confidence: certain.
Root cause (code trace)
efidev_ioctl() trusts struct efi_var_ioc::datasize / ::namesize
(size_t, fully user-controlled; sys/sys/efiio.h:40-48) and hands them to
kmalloc(..., M_TEMP, M_WAITOK) with no upper bound β efidev.c:105, 106
(EFIIOC_VAR_GET), efidev.c:140 (EFIIOC_VAR_NEXT), efidev.c:165, 166
(EFIIOC_VAR_SET). The SET path allocates datasize first (line 164-165),
so a single ioctl reaches the primitive with nothing but open("/dev/efi").
- Huge non-wrapping sizes:
_kmalloc()β large path (sys/kern/kern_slaballoc.c:942) βkmem_slab_alloc()βvm_map_findspace()fails βpanic("kmem_slab_alloc(): kernel_map ran out of space!")becauseM_WAITOKwithoutM_NULLOKis a hard requirement (kern_slaballoc.c:1721-1724). kmalloc(M_WAITOK) never returns NULL for unsatisfiable sizes on DragonFly β it panics. - Sizes β₯ 2^64βPAGE_MASK+1:
size = round_page(size)(kern_slaballoc.c:945) wraps to 0 βkmem_slab_alloc(0, ...):vm_map_findspace(len=0)succeeds,vm_map_insert()inserts a zero-length entry intokernel_map, andvm_map_set_wired_quick()(kern_slaballoc.c:1743) immediately trips the map invariant βpanic("vm_map_entry_link: dup addr map ..."). Even absent that check, the returned KVA has no pages and*kup = 0(line 953-954), sokfree()atefidev.c:184would fall into the SLZone path (kern_slaballoc.c:1475-1478,*kup==0is not>0) on a zone-less unmapped pointer β KKASSERT/unmapped-read fault.
Reachability does not require UEFI: all allocation happens before
efi_enter() (efirt.c:343-359). On the BIOS-booted QEMU guest,
kldload efirt prints module_register_init: MOD_LOAD (efirt, ...) error 6
(sys/kern/kern_module.c:110-116 tolerates the failure) while the co-packaged
efidev module registers and creates /dev/efi (0700 root:wheel,
efidev.c:205-206). Trigger therefore requires root β same privilege
calibration as DF-2121 (known, not re-reported here).
What was run (all on DragonFly 6.5-DEVELOPMENT x86_64 guest)
- Control (mode 0): sane sizes β
ENXIO(efi_enteron BIOS), guest up. - Mode 1 (
datasize=0x4000000000000000): guest died withpanic: kmem_slab_alloc(): kernel_map ran out of space!β trace_kmalloc β efidev_ioctl β dev_dioctl(panic.m1.txt). Reproduced first try. - Mode 2 (
datasize=0xFFFFFFFFFFFFFFFF): guest died withpanic: vm_map_entry_link: dup addr map ...β tracevm_map_set_wired_quick β kmem_slab_alloc β _kmalloc(panic.m2.txt). Reproduced first try.
Both panics are deterministic, single-syscall, and observed via serial console
with vm.sh status β down immediately after.
Why not uid0
The trigger surface is 0700 root:wheel β the caller is already root, so
there is no unprivβroot ladder for this node. The memory-corruption flavor
(zero-length kernel_map entry / kup-misclassified pointer) is real but
manifests as a map-invariant panic in this allocator; no controllable
corruption primitive was pursued beyond demonstrating the invariant break.
Fix validation
fix.diff adds EFI_IOC_MAX_DATA/EFI_IOC_MAX_NAME (1 MiB, Linux-efivarfs
parity) plus namesize >= sizeof(efi_char) checks to all three VAR ioctls
(the latter also neutralizes DF-2121's underflow as defense in depth).
Applied to the guest's /usr/src, module rebuilt (-Werror clean), loaded
as efirt-patched.ko:
| run | stock module | patched module |
|---|---|---|
| mode 1 (2^62) | panic kmem_slab_alloc | EINVAL, guest up |
| mode 2 (SIZE_MAX) | panic vm_map dup addr | EINVAL, guest up |
| mode 0 control | ENXIO |
ENXIO (unchanged) |
fix_status: fixed.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to guest /usr/src/sys/dev/misc/efirt/efidev.c, module rebuilt with -Werror clean and loaded as efirt-patched.ko: mode 1 and mode 2 both return EINVAL (errno 22) with the guest surviving and uptime advancing; mode 0 control still returns ENXIO so legitimate size-query semantics are preserved. Baseline panics were reproduced minutes earlier on the stock module in the same session.
fix_build.log (patched efidev.c compiles, links efirt.ko); fix_run.log (mode1 EINVAL, mode2 EINVAL, mode0 ENXIO, 'guest still alive', efirt-patched.ko in kldstat); compare run.log + panic.m1.txt/panic.m2.txt for baseline.
Confirmed kernel references
Detail
Exploit chain
open("/dev/efi", O_RDWR) [root] -> ioctl(EFIIOC_VAR_SET, {datasize: 0x4000000000000000, name: L"", namesize: 4, data: valid}) -> efidev.c:165 kmalloc(2^62, M_WAITOK) -> _kmalloc large path -> kmem_slab_alloc -> vm_map_findspace fails -> panic (variant A); datasize: 0xFFFFFFFFFFFFFFFF -> round_page wraps to 0 -> zero-length vm_map_insert -> vm_map_set_wired_quick -> dup-addr panic (variant B). No uid0 chain: device is root-only; corruption manifests as deterministic kernel-map invariant panic, not a controllable primitive.
Evidence (decisive lines)
panic.m1.txt: 'panic: kmem_slab_alloc(): kernel_map ran out of space!' trace _kmalloc<-efidev_ioctl<-dev_dioctl; panic.m2.txt: 'panic: vm_map_entry_link: dup addr map ...' trace vm_map_set_wired_quick<-kmem_slab_alloc<-_kmalloc; run.log full transcript incl. mode-0 ENXIO control; fix_run.log patched module -> EINVAL x2 + ENXIO control, guest alive; fix.diff the validated bounds check.
PoC changes
Wrote from scratch for this finding (no seed): single C file, three modes (0 control / 1 huge / 2 wrap); struct efi_var_ioc from
Verified recommended fix
Validate sizes before allocation in all three VAR ioctls: reject datasize/namesize > 1 MiB and namesize < sizeof(efi_char) with EINVAL (see fix.diff); alternatively make _kmalloc's large path fail with NULL instead of panicking when M_NULLOK was not requested.
Verdict
All three EFI variable ioctls in sys/dev/misc/efirt/efidev.c pass user-controlled efi_var_ioc::datasize/namesize (size_t) to kmalloc(M_TEMP, M_WAITOK) with no upper bound (efidev.c:105,106,140,165,166). A single EFIIOC_VAR_SET as root with datasize=2^62 panics the stock kernel deterministically ('kmem_slab_alloc(): kernel_map ran out of space!', kern_slaballoc.c:1721-1724, because M_WAITOK was given without M_NULLOK); with datasize=SIZE_MAX, round_page() wraps to 0 (kern_slaballoc.c:945), kmem_slab_alloc(0) inserts a zero-length duplicate entry into kernel_map and the next vm_map op panics ('vm_map_entry_link: dup addr map'). Both reproduced on a BIOS-booted DragonFly 6.5-DEVELOPMENT guest (kldload efirt tolerates efirt's ENXIO init failure but the co-packaged efidev module still creates /dev/efi; allocations precede efi_enter). Root-only device (0700 root:wheel) so impact is privileged-user kernel panic / allocator-invariant corruption, same calibration as DF-2121 (known, not re-reported). fix.diff (1 MiB caps + namesize>=sizeof(efi_char)) validated: patched module returns EINVAL for both modes, control ENXIO unchanged, guest survives.
No comments yet.