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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5478 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 iterations
  • md_pages = 2^52 + 1 β†’ product ≑ 4096 β†’ end == md_phys+4096 β†’ PASS
  • generally any md_pages = (W + k*2^64)/4096 with 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)'.
↓ fix.diffper-fix-DF-5478

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.