β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5119

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5119 Β· 16 files
FileTypeDescriptionSize
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:

  1. 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 because M_WAITOK was given without M_NULLOK the kernel panics outright: panic("kmem_slab_alloc(): kernel_map ran out of space!") (kern_slaballoc.c:1721-1724).

  2. Size near SIZE_MAX (e.g. 0xFFFFFFFFFFFFFFFF): round_page() (kern_slaballoc.c:945) wraps to 0, so kmem_slab_alloc(0, ...) runs: vm_map_findspace(len=0) succeeds, vm_map_insert inserts a zero-length entry into kernel_map, and the immediately following vm_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 subsequent kfree() 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.sh
  • build.log β€” guest build + mode-0 control
  • run.log β€” baseline transcript (both panics)
  • panic.txt, panic.m1.txt, panic.m2.txt β€” serial-console panic excerpts
  • fix_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
VERDICT.md
↓ download raw

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!") because M_WAITOK without M_NULLOK is 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 into kernel_map, and vm_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), so kfree() at efidev.c:184 would fall into the SLZone path (kern_slaballoc.c:1475-1478, *kup==0 is 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)

  1. Control (mode 0): sane sizes β†’ ENXIO (efi_enter on BIOS), guest up.
  2. Mode 1 (datasize=0x4000000000000000): guest died with panic: kmem_slab_alloc(): kernel_map ran out of space! β€” trace _kmalloc ← efidev_ioctl ← dev_dioctl (panic.m1.txt). Reproduced first try.
  3. Mode 2 (datasize=0xFFFFFFFFFFFFFFFF): guest died with panic: vm_map_entry_link: dup addr map ... β€” trace vm_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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.
↓ fix.diffper-fix-DF-5119

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 ; EFIIOC_VAR_SET used because it allocates datasize first; name kept valid (L"", namesize=4) so DF-2121's known underflow is not what triggers. Guest /tmp is wiped by vm.sh reset, so re-scp + rebuild between runs is required.

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.