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)
PoC verification
Evidence pack
findings/poc/DF-5341 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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 atmptable.c:93) β 0 reads - MPFPS
spec_rev/lengthsanity β 0 reads - MPCTH
"PCMP"signature (cth->signature, struct atmptable.c:103) β 0 reads - MPCTH checksum (
cth->checksum,mptable.c:106) β 0 reads pap/pap + base_table_lengthinside 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_mapszcan 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.
4. Recommended fix
fix.diff β restore the two cheapest, highest-value checks from the MP
spec / FreeBSD heritage before adopting pap:
- MPFPS checksum over
sizeof(struct MPFPS)must be 0; - 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_testablefix.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
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.
No comments yet.