#!/usr/bin/env python3
# DF-5617 latch_dance.py — play the DW I2C ENABLE_STATUS latch from the host.
#
# ig4_iic.c set_controller() (lines 112-134) writes IG4_REG_I2C_EN (0x6C) and
# polls IG4_REG_ENABLE_STATUS (0x9C) until bit0 (IG4_I2C_ENABLE) matches the
# written value.  The planted device's MMIO window is the SSDT's own pad
# buffer (plain guest RAM), so the latch does not exist in "hardware"; this
# script supplies it by flipping the dword at guest-physical
# window+0x9C = 0xBFFE1E7C right after the kernel prints the "comp_version"
# attach line (ig4_iic.c:536), just before the three set_controller() gates
# at ig4_iic.c:632/637/642:
#
#   bit0 must read 0  for set_controller(sc, 0)          (seeded 0; passes fast)
#   bit0 must read 1  for set_controller(sc, ENABLE)     (we set 1 -> passes)
#   bit0 must read 0  for set_controller(sc, 0)          (we clear -> passes)
#
# Then ig4iic_attach() reaches lwkt_serialize_enter (ig4_iic.c:648) +
# bus_setup_intr(GSI 4) which fails on register_int's exclusive-owner check
# against the live sio0 handler (kern_intr.c:276/284) -> serializer leaked ->
# ig4_acpi.c:134 ig4iic_acpi_detach() -> ig4_iic.c:678 lwkt_serialize_enter()
# re-entry -> INVARIANTS KKASSERT panic (hard self-deadlock without).
#
# Physical write path: QEMU monitor "gpa2hva" + /proc/<qemu-pid>/mem
# (ptrace_scope=0 on this host; qemu runs as the same uid).  The hva is
# resolved BEFORE the marker watch so the reaction latency after the serial
# marker is a single pwrite (~microseconds).
import socket, sys, time, os, struct, re, json

CFG = json.load(open(os.path.join(os.path.dirname(__file__), "window.json")))
W0 = CFG["w0"]                       # first register-file window (guest-physical)
GPA_STATUS = [W0 + 0x9C, W0 + 0x1000 + 0x9C]   # both IG4_REG_ENABLE_STATUS dwords
BOOTLOG = "dfbsd-qemu/boot.log"
MONSOCK = "dfbsd-qemu/boot-mon.sock"
MARKER = b"comp_version"

def mon(cmds, wait=0.08):
    s = socket.socket(socket.AF_UNIX)
    s.connect(MONSOCK)
    time.sleep(0.08)
    try: s.recv(65536)
    except Exception: pass
    out = []
    for c in cmds:
        s.sendall((c + "\n").encode())
        time.sleep(wait)
        s.setblocking(False)
        buf = b""
        try:
            while True:
                d = s.recv(65536)
                if not d: break
                buf += d
        except Exception: pass
        s.setblocking(True)
        out.append(buf.decode(errors="replace").replace("\x1b[K","").replace("\x1b[D","").replace("\r",""))
    s.close()
    return out

def main():
    pid = None
    with open("dfbsd-qemu/vm.pid") as f:
        pid = int(f.read().strip())
    print(f"[dance] qemu pid {pid}; windows: " +
          ", ".join(hex(g) for g in GPA_STATUS), flush=True)

    # pre-resolve gpa -> hva for both status dwords (guest RAM exists from t=0)
    hvas = []
    for g in GPA_STATUS:
        (hva,) = mon([f"gpa2hva {g:#x}"])
        m = re.search(r"Host virtual address for.* is (0x[0-9a-f]+)", hva)
        if not m:
            print(f"[dance] gpa2hva failed for {g:#x}: {hva!r}", flush=True)
            return 1
        hvas.append(int(m.group(1), 16))
    print(f"[dance] hvas: " + ", ".join(hex(h) for h in hvas), flush=True)
    mem = open(f"/proc/{pid}/mem", "r+b", 0)

    def wr(val):
        for h in hvas:
            os.pwrite(mem.fileno(), struct.pack("<I", val), h)

    # wait for the attach marker on the serial console
    size = 0
    t0 = time.time()
    while time.time() - t0 < 240:
        try:
            st = os.stat(BOOTLOG)
            if st.st_size < size:
                size = 0                      # truncated -> new boot
            with open(BOOTLOG, "rb") as f:
                f.seek(size)
                data = f.read()
            size += len(data)
            if MARKER in data:
                print(f"[dance] marker seen at serial offset {size-len(data)}; "
                      f"t={time.time()-t0:.1f}s", flush=True)
                break
        except FileNotFoundError:
            pass
        time.sleep(0.005)
    else:
        print("[dance] MARKER NOT SEEN", flush=True)
        return 1

    # square wave, 120ms half period, 4.6s total: covers hz=100 (1s windows)
    # across TWO sequential device attaches.
    for _ in range(19):
        wr(1); time.sleep(0.12)
        wr(0); time.sleep(0.12)
    wr(0)
    print("[dance] latch sequence done; watching for setup-irq error/panic",
          flush=True)

    size2 = os.path.getsize(BOOTLOG)
    t0 = time.time()
    while time.time() - t0 < 60:
        time.sleep(0.5)
        with open(BOOTLOG, "rb") as f:
            f.seek(size2)
            data = f.read()
        size2 += len(data)
        if b"Unable to setup irq" in data:
            print("[dance] SEEN: Unable to setup irq (ig4_iic.c:652)", flush=True)
        if b"panic" in data or b"lwkt_serialize_enter" in data:
            print("[dance] SEEN: panic / serializer assert -- REPRODUCED", flush=True)
            return 0
        if b"controller error during attach" in data:
            print("[dance] latch missed (clean ENXIO path) -- retry needed",
                  flush=True)
            return 2
    return 3

if __name__ == "__main__":
    sys.exit(main())
