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

mp_bootaddress() integer underflow on degenerate base-memory size corrupts the kernel physical memory map

Summary

Unchecked u_int arithmetic on the firmware/loader-supplied first-region size: basemem <= 12KB wraps mptramp_pagetables to ~0xFFFFC000 which is laundered into physmap[1] (kernel believes the whole low 4GB incl. MMIO holes is RAM); basemem 13-16KB lands the AP page tables on the IVT/BDA at PA 0, mutually corrupting the warm-reset vector. Guest arithmetic reproducer (verbatim fn, real bootMP_size): wrapped/IVT-smash rows proven, 638 row matches this guest's real SeaBIOS basemem. Firmware/boot-chain threat class (same as accepted DF-1034); stock QEMU cannot express the trigger so boot manifestation honestly untested. Fail-fast-lower-bound fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5326 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 5.9 KB ↓ raw
mp_bootaddr_underflow.c β€” 2.8 KB view raw
build.sh β€” 252 B view raw
run.sh β€” 119 B view raw
build.log β€” 26 B view raw
run.log β€” 1.4 KB view raw
env.txt β€” 410 B view raw
fix.diff β€” 1.2 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 4.4 KB view raw

DF-5326 β€” mp_bootaddress() integer underflow (Low)

sys/platform/pc64/x86_64/mp_machdep.c:206-219 β€” mp_bootaddress(basemem) performs unchecked 32-bit unsigned arithmetic on the firmware/loader-supplied size of the first usable memory region. For basemem <= 12 (KB), mptramp_pagetables = boot_address - 3*PAGE_SIZE wraps to ~0xFFFFC000-0xFFFFF000 and is stored into physmap[1] (machdep.c:2272), making the kernel believe the entire low 4GB (incl. MMIO/ROM holes) is usable RAM. For basemem 13-16 it returns 0 and the AP trampoline page tables are written over the IVT/BDA.

Threat: malicious/compromised firmware or boot chain (loader-collected BIOS E820/EFI map). Boot-time only; no unprivileged runtime surface.

Reproduce (arithmetic proof on the audit guest)

The guest is DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (see env.txt). Push the reproducer to the guest, build with the guest cc, run:

B64=$(openssl base64 -A -in mp_bootaddr_underflow.c)
./dfbsd-qemu/vm.sh run_root "echo $B64 | openssl base64 -d -A > /tmp/mpb.c && \
    cc -O2 -Wall -Wextra -o /tmp/mpb /tmp/mpb.c && echo BUILD_OK && /tmp/mpb"

(Or on any Unix host with a C compiler: cc -O2 -o mp_bootaddr_underflow mp_bootaddr_underflow.c && ./mp_bootaddr_underflow.)

Expected

BUILD_OK
bootMP_size = 170 (0xaa) bytes [from mptramp_end-mptramp_start]
...
         0   0xfffff000      0xffffc000        WRAPPED: physmap[1] ~= 4GB -> whole low 4GB marked RAM
        13   0x00003000      0x00000000        ZERO: trampoline PTs smash IVT/BDA at PA 0
        17   0x00004000      0x00001000        sane
       638   0x0009f000      0x0009c000        sane

basemem ≀ 12 β†’ WRAPPED; 13–16 β†’ ZERO (PA 0); β‰₯ 17 β†’ sane. The 638 row is the value a real SeaBIOS produces on this guest (trampoline at 0x9F000).

The kernel-boot manifestation (physmap corruption) is NOT exercised: the trigger needs a crafted SMAP which stock QEMU/SeaBIOS cannot express (see VERDICT.md Β§3).

VERDICT.md
↓ download raw

DF-5326 β€” mp_bootaddress() integer underflow corrupts the kernel physical memory map

Verdict: defect certain at code level; arithmetic reproduced on the audited guest; end-to-end kernel-boot manifestation untested (boot-time trigger requires control of the loader-provided BIOS SMAP, which the standard QEMU/SeaBIOS guest cannot express)

1. Root cause

sys/platform/pc64/x86_64/mp_machdep.c:206-219:

u_int
mp_bootaddress(u_int basemem)
{
    bootMP_size = mptramp_end - mptramp_start;
    boot_address = trunc_page(basemem * 1024); /* round down to 4k boundary */
    if (((basemem * 1024) - boot_address) < bootMP_size)
        boot_address -= PAGE_SIZE;  /* not enough, lower by 4k */
    /* 3 levels of page table pages */
    mptramp_pagetables = boot_address - (PAGE_SIZE * 3);

    return mptramp_pagetables;
}

basemem is the size in KB of the first usable memory region reported by the firmware, laundered through the boot chain:

There is no lower-bound validation on basemem anywhere on this path. For a degenerate first usable region the unsigned 32-bit arithmetic wraps:

  • basemem <= 12 KB β†’ mptramp_pagetables = boot_address - 0x3000 wraps to 0xFFFFD000-0xFFFFC000, and that wrapped value is written into physmap[1] β€” the kernel now believes the first usable RAM region is [0, ~0xFFFFC000), i.e. the entire low 4GB including all PCI/MMIO/ROM holes is RAM. Maxmem/physmem accounting (machdep.c:2285+), vm_page probing and early allocations are then derived from a bogus map β†’ kernel memory corruption / unbootable system.
  • basemem 13–16 KB β†’ mptramp_pagetables == 0: the three AP-initial page table pages written by start_all_aps() (mp_machdep.c:414-438, pt4/pt3/pt2) land on the real-mode IVT/BDA at PA 0, and the warm-reset vector written at mp_machdep.c:446 (WARMBOOT_OFF == KERNBASE+0x467) is written inside the pt4 page β€” mutual corruption, AP startup undefined.
  • Secondary (theoretical, needs a β‰₯4TB first region): basemem = physmap[1]/1024 truncates to u_int and basemem * 1024 itself can wrap in u_int.

2. Reproduction (arithmetic level, on the audited guest)

mp_bootaddr_underflow.c copies the function verbatim (u_int semantics preserved) and uses the real bootMP_size from the running audited kernel (nm /boot/kernel/kernel: mptramp_start=0xffffffff8113b8c0, mptramp_end=0xffffffff8113b96a β†’ 170 bytes). Built and executed on the guest with the stock cc 8.3 [DragonFly]. Output (run.log):

basemem(KB)  boot_address  mptramp_pagetables   effect on physmap[1]
         0   0xfffff000      0xffffc000        WRAPPED: physmap[1] ~= 4GB -> whole low 4GB marked RAM
         1   0x00000000      0xffffd000        WRAPPED ...
        ...
        12   0x00002000      0xfffff000        WRAPPED ...
        13   0x00003000      0x00000000        ZERO: trampoline PTs smash IVT/BDA at PA 0
        ...
        16   0x00003000      0x00000000        ZERO ...
        17   0x00004000      0x00001000        sane
       638   0x0009f000      0x0009c000        sane   <- real SeaBIOS value on this guest

The sane row (638 KB) matches the classic trampoline placement (0x9F000 / PTs at 0x9C000), confirming the reproducer models the live code path.

3. Why the kernel-boot manifestation was not exercised

Triggering the real code path requires the loader to hand the kernel a SMAP whose first usable region ends below ~17 KB. On the audit guest:

  • the SMAP is generated by SeaBIOS at loader boot time (fixed 0–639KB layout); QEMU exposes no interface to shrink the first usable region below 640 KB (etc/e820 fw_cfg only adds reserved entries);
  • the SMAP blob is collected by the loader from the BIOS at loader runtime β€” it is not read from any file an in-guest attacker can write;
  • marking low memory reserved via fw_cfg moves the first usable region start, not its end β€” physmap[1] stays β‰ˆ639 KB, so basemem stays sane.

So the only realistic carriers of the trigger are malicious/compromised firmware or a tampered boot chain (boot area / loader binary), which matches the Low severity and the same threat class as the accepted DF-1034.

4. Impact ceiling

Boot-time kernel memory-map corruption from crafted firmware/boot chain: unbootable machine at best; at worst a physmem map that hands out MMIO/ROM pages as RAM (silent memory corruption). No runtime (post-boot), unprivileged, or remote reachability β€” the function runs exactly once, early in init386().

5. Fix

fix.diff (authored against the read-only sys/ tree, never applied there): fail fast on a degenerate basemem instead of silently wrapping β€” consistent with the tree's existing boot-time panics on malformed firmware tables (e.g. madt_lapic_pass1() panics on a failed MADT walk). Not kernel-build-validated (finding is Low / boot-time / firmware-gated; fix validation reserved for memory-corruption findings that reproduce).

6. Artifacts

  • mp_bootaddr_underflow.c β€” verbatim-logic reproducer (guest-compiled)
  • build.sh / run.sh β€” exact commands
  • run.log β€” full guest build+run output (BUILD_OK + table)
  • env.txt β€” guest uname, dmesg SMP path, kernel symbols, cc version
  • fix.diff β€” proposed fix

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff (boot-time panic on degenerate basemem, mirroring the tree's existing panics on malformed firmware tables) is authored against the read-only sys/ tree and passes git apply --check, but could not be kernel-build-validated: the bug's kernel manifestation cannot be armed on this guest (needs crafted SMAP), so there is no baseline to compare a patched kernel against.

findings/poc/DF-5326/fix.diff (git apply --check -p1 passes); reasoning in VERDICT.md section 5
↓ fix.diffper-fix-DF-5326

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5326/run.log β€” guest-compiled reproducer output: basemem 0/1/2/4/8/9/12 -> 0xffffc000..0xfffff000 (WRAPPED), 13-16 -> 0x00000000 (PTs at PA 0), 17/32/638 -> sane; the 638 KB row equals the real SeaBIOS basemem on this guest. VERDICT.md section 1 traces basemem from physmap[1] (machdep.c:2270) through the wrap and back into physmap[1] (machdep.c:2272); fix.diff applies cleanly (git apply --check).

PoC changes

No seed PoC existed; authored from scratch. Reproducer copies mp_bootaddress() verbatim (u_int wrap semantics preserved) and parameterizes bootMP_size with the real value (mptramp_end-mptramp_start = 170) taken from the running guest kernel's symbols.

Verified recommended fix

panic() in mp_bootaddress() when basemem1024 < bootMP_size + 4PAGE_SIZE (fail fast on degenerate firmware memory map) β€” see fix.diff

Verdict

Defect is certain at code level and its arithmetic is proven on the audited guest: mp_bootaddress() (sys/platform/pc64/x86_64/mp_machdep.c:206-219) performs unchecked u_int arithmetic on the firmware/loader-supplied size of the first usable memory region; for basemem <= 12 KB, mptramp_pagetables = boot_address - 3*PAGE_SIZE wraps to 0xFFFFC000-0xFFFFF000 and is stored into physmap[1] (machdep.c:2272), making the kernel treat the whole low 4GB (incl. MMIO/ROM holes) as usable RAM; for basemem 13-16 KB it returns 0 so the AP trampoline page tables (mp_machdep.c:414-438) are written over the real-mode IVT/BDA at PA 0. The verbatim-logic reproducer, parameterized with the real bootMP_size (170 bytes) from the guest kernel symbols, was compiled with the guest cc and produced exactly the predicted wrap/zero/sane table (run.log). The end-to-end kernel-boot manifestation is UNTESTED: arming it requires a crafted SMAP (first usable region ending below ~17 KB), which stock QEMU/SeaBIOS cannot express (etc/e820 fw_cfg only adds reserved regions) and which no unprivileged in-guest actor can influence. Low severity, boot-time only, malicious-firmware/boot-chain threat class (same as accepted DF-1034).