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

mptable_map() adopts firmware pap/base_table_length with no 'PCMP' signature, no MPFPS/MPCTH checksum, and no PA-range validation β€” arbitrary-physical-address kernel read and parse at boot

Summary

The floating-pointer scan matches only '_MP_' (4 bytes); mptable_map accepts any nonzero pap, maps 44B there, adopts base_table_length and re-maps up to 64KB - fps->checksum/cth 'PCMP'/cth checksum are NEVER read (FreeBSD heritage verified all three; the DFly port dropped them), and pmap_mapdev installs PTEs for ANY pa: a stray aligned '_MP_' dword promotes MMIO/PCI/wrap-crossing memory into a parsed MP table driving CPU/IOAPIC enumeration. Guest decision-table reproducer: pap 0xa0000000/0xffffff00/0xdeadbeef all accepted, one crosses the 4GB wrap. Restore-the-two-cheapest-checks fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5341 Β· 11 files
FileTypeDescriptionSize
pap_trust.c β€” 3.3 KB view raw
build.sh β€” 100 B view raw
run.sh β€” 92 B view raw
README.md β€” 745 B ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
build.log β€” 10 B view raw
run.log β€” 1.3 KB view raw
env.txt β€” 190 B view raw
fix.diff β€” 1.2 KB view raw
manifest.json β€” 881 B view raw
verdict.json β€” 3.8 KB view raw

DF-5341 β€” mptable_map trusts firmware pap/length with no PCMP-signature, checksum, or PA-range validation

Boot-time (firmware/loader-gated) defect in sys/platform/pc64/x86_64/mptable.c. The kernel-boot manifestation cannot be armed on the standard QEMU/SeaBIOS guest (no MP-table injection path, no unprivileged in-guest trigger), so per the DF-5326 precedent this pack carries a verbatim-logic reproducer that was built and executed ON the audited guest and proves the exact code-level divergence (see run.log). Kernel impact classification: untested.

Build (guest or host)

cc -O2 -Wall -Wextra -o pap_trust pap_trust.c

Run

./pap_trust

Expected

See run.log (decisive lines quoted in verdict.json:evidence).

VERDICT.md
↓ download raw

DF-5341 β€” mptable_map() trusts firmware table pointers with no signature / checksum / PA-range validation

Verdict: defect certain at code level; acceptance table proven on the audited guest; kernel-boot manifestation untested (needs injected firmware table memory)

1. Root cause

The floating-pointer search matches only the 4-byte _MP_ signature (mptable.c:359, 375). Everything after that is trust:

mptable_map() β€” sys/platform/pc64/x86_64/mptable.c:501-539:

fps = pmap_mapdev(mptable_fps_phyaddr, sizeof(*fps));
if (fps->pap != 0) {                    /* mptable.c:512 β€” the ONLY check */
    /*
     * Map configuration table header to get the base table size
     */
    cth = pmap_mapdev(fps->pap, sizeof(*cth));  /* mptable.c:517 β€” arbitrary PA read */
    cth_mapsz = cth->base_table_length;     /* mptable.c:518 β€” u_short from that PA */
    pmap_unmapdev((vm_offset_t)cth, sizeof(*cth));
    if (cth_mapsz < sizeof(*cth)) { ... EINVAL }
    cth = pmap_mapdev(fps->pap, cth_mapsz);     /* mptable.c:531 β€” up to 65535 bytes */
}

Never validated anywhere in the file (verified by grep of every field use):

  • MPFPS checksum byte (fps->checksum, struct at mptable.c:93) β€” 0 reads
  • MPFPS spec_rev / length sanity β€” 0 reads
  • MPCTH "PCMP" signature (cth->signature, struct at mptable.c:103) β€” 0 reads
  • MPCTH checksum (cth->checksum, mptable.c:106) β€” 0 reads
  • pap / pap + base_table_length inside mapped RAM β€” no range check exists

pmap_mapdev_attr() (pmap.c:6192-6218) installs PTEs for ANY physical address with no bounds check, so:

  • every accepted pap (any nonzero u_int32) becomes an early-boot kernel read of an arbitrary physical address (MMIO holes, PCI space, ROM),
  • pap + cth_mapsz can cross the 4GB wrap (e.g. pap=0xFFFFFF00, len=0x1000 β†’ span [0xFFFFFF00, 0x100000F00)),
  • the parsed garbage then drives CPU/IOAPIC enumeration; its failure modes are the sibling findings' KKASSERT panics (DF-5339) and mis-enumerated topology (DF-5342/DF-5343), or benign EINVAL.

Because neither structure's signature nor checksum is verified, a single stray 4-byte-aligned _MP_ dword anywhere in the scanned 1 KB EBDA / 128 KB BIOS regions (530 KB of address space total) promotes arbitrary memory into a "MP floating pointer" whose pap field is then followed blindly.

Historical note: FreeBSD's mptable code this file descends from verified the MPFPS checksum and the PCMP signature before use; the DragonFly port dropped all of it.

2. Reproduction (decision table, on the audited guest)

pap_trust.c prints the acceptance predicate (pap != 0) and the mapdev span math. run.log (guest cc 8.3):

  pap=0xfebc0000 (MMIO hole          ) -> ACCEPTED, mapped + parsed as MP config table
  pap=0xa0000000 (PCI MMIO           ) -> ACCEPTED, mapped + parsed as MP config table
  pap=0xffffff00 (top of 4GB - 0x100 ) -> ACCEPTED, mapped + parsed as MP config table
  pap=0xffffffff (last byte of 4G    ) -> ACCEPTED, mapped + parsed as MP config table
  pap=0xdeadbeef (total garbage      ) -> ACCEPTED, mapped + parsed as MP config table
  ...
  4GB-crossing   pap=0xffffff00 len=4096  map span [0x0ffffff00..0x100000f00)  ** CROSSES THE 4GB WRAP **

3. Threat model / why kernel-boot manifestation is untested

The pointer and table live in firmware-provided memory; only malicious or compromised firmware/boot-loader controls them. The standard QEMU/SeaBIOS guest generates its own (absent) MP tables and offers no injection path (-fw_cfg cannot place _MP_ structures into the scanned regions), and mptable_probe runs exactly once at SI_BOOT2_PRESMP β€” no runtime, no unprivileged reach. Consequence ceiling on this class: wrong/absent SMP enumeration, arbitrary-PA reads, or the sibling findings' boot panics.

fix.diff β€” restore the two cheapest, highest-value checks from the MP spec / FreeBSD heritage before adopting pap:

  1. MPFPS checksum over sizeof(struct MPFPS) must be 0;
  2. the mapped config-table header must start with "PCMP".

(A full fix would also checksum the MPCTH and range-check pap/pap+len against the physmap.)

5. Fix validation

git apply --check passes. A patched-kernel A/B boot test is not constructible on this guest (no table injection), hence fix_status: not_testable.

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 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 injected firmware tables), so there is no baseline to compare a patched kernel against.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log β€” pap 0xfebc0000/0xa0000000/0xffffff00/0xffffffff/0xdeadbeef all 'ACCEPTED, mapped + parsed as MP config table'; 'map span [0x0ffffff00..0x100000f00) ** CROSSES THE 4GB WRAP **'; the four never-performed validations listed with grep counts.

PoC changes

No seed PoC existed; authored from scratch. Reproducer copies the cited mptable.c logic verbatim (u_int/u_char semantics preserved); KKASSERT is modeled as a kernel-panic path (setjmp-resumable in the harness), and pmap_mapdev/MMIO access is elided where userland cannot express it (clearly marked in the source header).

Verified recommended fix

verify MPFPS checksum and MPCTH 'PCMP' signature in mptable_map before adopting pap/length

Verdict

Defect is certain at code level; acceptance table proven on the audited guest. The floating-pointer scan matches only the 4-byte 'MP' signature (mptable.c:375). mptable_map() then adopts fps->pap with the sole check pap != 0 (mptable.c:512), maps 44 bytes at that arbitrary 32-bit physical address, adopts base_table_length from it (mptable.c:517-518), and re-maps up to 65535 bytes there for full parsing (mptable.c:531). pmap_mapdev_attr() (pmap.c:6192-6218) installs PTEs for ANY physical address with no bounds check, so every firmware-controlled value becomes an early-boot kernel read of an arbitrary physical address (MMIO holes, PCI space, spans crossing the 4GB wrap such as pap=0xFFFFFF00 + len=0x1000). The MPFPS checksum byte, MPCTH 'PCMP' signature, and MPCTH checksum are NEVER read anywhere in the file (verified by grep), so any stray 4-byte-aligned 'MP' dword in the scanned 1K EBDA / 128K BIOS regions promotes arbitrary garbage into a parsed MP table; the downstream failure modes are the KKASSERT panics of the sibling findings and mis-enumerated CPU/IOAPIC topology. Guest-compiled decision-table reproducer prints every garbage pap accepted and the 4GB-crossing span. End-to-end manifestation UNTESTED (needs injected firmware tables; standard QEMU/SeaBIOS guest cannot express it). FreeBSD's historic mptable code validated both checksums and the PCMP signature; the fix restores the signature + floating-checksum checks.