# DF-5432 — `ioapic_map()` of unvalidated firmware IOAPIC address: arbitrary-physical-address kernel MMIO reads/writes (DF-5365's IOAPIC twin)

## What

`madt_ioapic_probe_callback` rejects only `Address == 0`
(acpi_madt.c:619-622); `madt_ioapic_enum_callback` then maps whatever
32-bit physical address the firmware names — `ioapic_map()`
(acpi_madt.c:733 → ioapic.c:603-607 `pmap_mapdev_uncacheable(pa,
PAGE_SIZE)`, only `KKASSERT(pa < 4GB)`) — and the kernel performs MMIO
reads AND writes through it (`ioapic_read`/`ioapic_write`, ioapic_ipl.s:
117-131: 32-bit store at +0x00 and +0x10) including redirection-entry
programming (`ioapic_pin_prog`, ioapic.c:541-543) and APIC-ID rewrite
(`ioapic_set_apic_id`, ioapic.c:312-322). This is the IOAPIC-side twin of
DF-5365 (lapic_map, Medium): the LAPIC base at least has IA32_APICBASE to
cross-check; the IOAPIC address is adopted with zero validation.

## Demonstration (guest RAM as IOAPIC — reproduced on real kernel boots)

The MADT names `Address = 0x40001000` — **ordinary guest RAM**, planted via
QEMU `-device loader` with a fake register file (IOWIN=0x00F00000 → VER
read yields npin=241). Result (run.log:87-88, run.3.log on the with-src
kernel):

    ACPI MADT: IOAPIC addr 0x40001000, apic id 0, gsi base 0
    IOAPIC: idx 0, apic id 2, gsi base 0, npin 241

The kernel adopted the RAM page, rewrote its "APIC ID" through it, and
programmed 241 redirection entries into it; the machine then continued
booting on a RAM page pretending to be the interrupt router.

Physical-memory proof (QEMU monitor, monitor_baseline.txt /
run.log-era dump):

    0000000040001000: 0x0000018d 0x00000000 ...   <- select-index WRITE by kernel
    0000000040001010: 0x00f00000 ...              <- window (kernel-written value;
                                                       equals planted 0x00f00000 because
                                                       the written target word preserves
                                                       reserved bits read back from the
                                                       same page)

`0x0000018d` at +0x00 is the kernel's register-select store (planted blob
had zeros there) — a kernel write to attacker-chosen physical memory.

## Build

    python3 gen_madt.py fake_ioapic /tmp/madt_fake_ioapic.bin

## Run

    vm.sh run_root 'printf "hint.acpi.0.rsdp=0x40000000\nboot_verbose=YES\n" >> /boot/loader.conf; sync'
    vm.sh down
    QEMU_EXTRA="-device loader,file=/tmp/madt_fake_ioapic.bin,addr=0x40000000,force-raw=on" vm.sh up
    vm.sh log        # IOAPIC: idx 0 ... npin 241
    # phys dump: QEMU monitor  xp /8wx 0x40001000

Note: the guest later stops in DDB at `AcpiTbLoadNamespace` — a rig
artifact (the minimal injected RSDT has no FADT/DSDT; the healthy `control`
table crashes there too, *after* completing all MADT/lapic/ioapic/SMP
stages). The IOAPIC adoption happens strictly earlier (SI_BOOT2_IOAPIC).

## Fix validation

`fix.diff` (probe-side rejection of IOAPIC addresses inside system RAM via
`phys_avail[]`) was built as a single-fix kernel in the guest
(`make -j6 nativekernel KERNCONF=X86_64_GENERIC`) and re-tested with the
exact same blob: the patched kernel REJECTS the table
("madt_ioapic_probe: IOAPIC address 0x40001000 is in system RAM, rejecting
MADT"), no RAM page is mapped/written (monitor dump all zeros), and the
machine degrades to "IOAPIC disabled" instead. See run.fixed.log +
monitor_fixed.txt.
