efi_create_1t1_map(): md_pages*EFI_PAGE_SIZE wraparound passes the runtime-map guard β multi-petabyte kmalloc(M_WAITOK) fill loop wedges the boot
Summary
The guard tests only the computed end pointer: md_pages=2^52 (product wraps to 0) or 2^52+1 (wraps to 4096) PASS, and the fill loop iterates ~4.5e15 times each doing kmalloc(M_WAITOK) + vm_page_insert - an unkillable allocation engine at MOD_LOAD that exhausts memory and wedges the boot. Arithmetic model with the verbatim guard expressions on the guest: both wrap rows PASS the production guard (>3.69e8 GiB); page-count-cap fix rejects both while accepting the sane case.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5478 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5478_wrap.c | β | 4.1 KB | view raw | |
| build.sh | β | 155 B | view raw | |
| run.sh | β | 94 B | view raw | |
| run.log | β | 1.9 KB | view raw | |
| fix.diff | β | 463 B | view raw | |
| env.txt | β | 206 B | view raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 3.3 KB | view raw |
DF-5478 VERDICT β md_pages*PAGE_SIZE wrap passes the 1:1-map guard
Status: reproduced (arithmetic/model evidence class). Impact: dos (boot-time, firmware-fed, effectively unbounded kmalloc(M_WAITOK) loop β memory-exhaustion wedge at module load).
Source trace
efi_create_1t1_map() (efirt.c:198-310) walks firmware-fed EFI memory
descriptors. For each EFI_MD_ATTR_RT descriptor it validates:
/* efirt.c:255-263 */
if (p->md_phys + p->md_pages * EFI_PAGE_SIZE < p->md_phys || /* wrap */
p->md_phys + p->md_pages * EFI_PAGE_SIZE >= VM_MAX_USER_ADDRESS)
goto fail;
then fills 1:1 pages:
/* efirt.c:288-297 */
for (va = p->md_phys, idx = 0; idx < p->md_pages; idx++, va += PAGE_SIZE) {
m = kmalloc(sizeof(*m), M_EFI, M_WAITOK | M_ZERO);
...
vm_page_insert(m, efi_obj, OFF_TO_IDX(va));
}
The guard only tests the computed end pointer. md_pages (uint64_t,
sys/efi.h:75) is never bounded directly, so when md_pages * 4096 itself
wraps mod 2^64 to a small value W, the end pointer falls back into
[md_phys, VM_MAX_USER_ADDRESS) and the guard passes β while the loop bound
remains the wrapped-around md_pages:
md_pages = 2^52β product β‘ 0 β end == md_phys β PASS β 4.5e15 iterationsmd_pages = 2^52 + 1β product β‘ 4096 β end == md_phys+4096 β PASS- generally any
md_pages = (W + k*2^64)/4096with small W and low md_phys
Each iteration kmallocs a struct vm_page (~88 bytes) with M_WAITOK. At
MOD_LOAD time (SI_SUB_DRIVERS, efirt.c:635) nothing will ever free those
allocations; the loop cannot be killed; the allocator exhausts RAM and
M_WAITOK sleep-forevers under pressure. Boot wedges. (Trigger is a
crafted EFI memory map β firmware- or loader-fed β the unvalidated-table
family; runtime reachability does not exist.)
Reproduction
df5478_wrap.c (guest, cc -O2, run as root/user β pure userspace model
of the exact production arithmetic): see run.log β
wrap: md_pages = 2^52 product=0x0 end=0x0000000001000000 guard: PASS (BUG)
-> fill-loop iterations=4503599627370496 (4.5e+15), kmalloc(M_WAITOK) bytes > 3.69e+08 GiB
wrap: md_pages = 2^52+1 product=0x1000 end=0x0000000001001000 guard: PASS (BUG)
no-wrap 2^40 pages product=0x0010000000000000 guard: REJECT (correct)
(In the "sane 256 pages" control the guard also PASSes and the loop runs β that is correct behavior there; only the wrap rows are the bug.)
Rig limit (honest)
BIOS-boot guest: efi_init() returns ENXIO at the missing MODINFOMD_EFI_MAP
metadata (efirt.c:421-427) long before efi_create_1t1_map() runs, so the
in-situ loop cannot be driven. The model uses identical integer widths and
constants; nothing in the loop/guard depends on kernel-only semantics
beyond M_WAITOK availability.
Fix validation
fix.diff adds the direct page-count cap
p->md_pages >= VM_MAX_USER_ADDRESS / EFI_PAGE_SIZE ahead of the existing
checks. The model's FIXED-guard section (same run.log) shows the wrap rows
now REJECT ("loop never starts") while the sane 256-page case is still
accepted β the previously-demonstrated bad arithmetic no longer passes.
Kernel-rebuild validation is not meaningful on this rig (the vulnerable
statics/path have no inputs on a BIOS boot); the arithmetic equivalence is
the strongest test this rig admits.
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 (direct md_pages cap ahead of the existing wrap/bound checks) applied to the model: the previously-passing wrap inputs are now rejected and the sane control still passes. Kernel-rebuild validation not meaningful on this BIOS rig (path has no inputs here); arithmetic equivalence is the strongest validation this rig admits.
run.log '=== under FIXED guard (fix.diff) ===' section: 'wrap: md_pages = 2^52 ... fixed guard: REJECT (loop never starts)'; 'sane 256 pages @16MB ... fixed guard: accept (loop runs)'.
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log rows: 'wrap: md_pages = 2^52 product=0x0 end=0x0000000001000000 guard: PASS (BUG) -> fill-loop iterations=4503599627370496 (4.5e+15), kmalloc(M_WAITOK) bytes > 3.69e+08 GiB'; 'wrap: md_pages = 2^52+1 ... guard: PASS (BUG)'; 'no-wrap 2^40 pages ... guard: REJECT (correct)'; FIXED-guard section: both wrap rows 'REJECT (loop never starts)', sane case 'accept'.
PoC changes
No seed existed. Model authored from scratch: verbatim guard arithmetic (uint64_t, EFI_PAGE_SIZE 4096, VM_MAX_USER_ADDRESS 0x00007fffffffe000) plus a FIXED-guard section for fix validation.
Verified recommended fix
Reject md_pages >= VM_MAX_USER_ADDRESS / EFI_PAGE_SIZE before the end-pointer checks (fix.diff).
Verdict
The runtime-map guard at efirt.c:255-263 tests only the computed end pointer md_phys + md_pages*EFI_PAGE_SIZE; md_pages is never bounded directly, so firmware-fed md_pages = 2^52 (product wraps to 0) or 2^52+1 (product wraps to 4096) pass the guard while the fill loop at efirt.c:288-297 still runs md_pages (~4.5e15) iterations of kmalloc(M_EFI, M_WAITOK|ZERO) + vm_page_insert β an unkillable allocation engine at module-load time that exhausts memory and wedges the boot. Reproduced arithmetically on the audit guest with the verbatim guard expressions (df5478_wrap.c): both wrap inputs PASS the production guard with multi-petabyte loop bounds, while non-wrapping huge extents are correctly rejected. Fix validated in the same run: with the fix.diff md_pages cap, the wrap rows are rejected ('loop never starts') and the sane 256-page case still passes. Trigger is a crafted EFI memory map (firmware/loader-fed at boot) β the unvalidated-table family; no runtime or unprivileged route.
No comments yet.