#!/usr/bin/env python3
"""
Drives Chrome via the DevTools remote-debugging-pipe to load poc.html and
invoke Page.printToPDF, which on desktop builds routes the renderer-produced
MSKP through the PrintCompositor utility's SkPDF backend. With the
accompanying instrumentation patch applied, the PrintCompositor utility
process logs "[POC-SINK] ..." lines to stderr each time SkClipStack_AsPath
feeds renderer-authored SkPath geometry into PathOps Op().

This is the proof for the attack-surface-routing finding: the report does
not claim a concrete memory-corruption primitive, only that renderer
geometry reaches iterated Op() inside a separately-sandboxed utility.
"""

import json
import os
import subprocess
import sys
import threading
import time

CHROME = os.environ.get(
    "CHROME", "/chromium/chromium/src/out/asan/chrome")
POC_URL = os.environ.get("POC_URL", "http://127.0.0.1:8080/poc.html")
PROFILE = "/tmp/chrome-poc-profile"


def main():
    os.makedirs(PROFILE, exist_ok=True)

    import fcntl

    def dup_high(fd):
        """Duplicate fd to a number >= 100 and close the original."""
        new = fcntl.fcntl(fd, fcntl.F_DUPFD, 100)
        os.close(fd)
        return new

    # fd 3: Chrome reads commands. fd 4: Chrome writes events/responses.
    a, b = os.pipe()
    cmd_r, cmd_w = dup_high(a), dup_high(b)   # we write cmd_w, child reads
    a, b = os.pipe()
    evt_r, evt_w = dup_high(a), dup_high(b)   # child writes, we read evt_r
    # Place child-facing ends on exactly 3 and 4 (pass_fds clears CLOEXEC).
    os.dup2(cmd_r, 3); os.close(cmd_r)
    os.dup2(evt_w, 4); os.close(evt_w)
    os.set_inheritable(3, True)
    os.set_inheritable(4, True)

    args = [
        CHROME,
        "--headless",
        "--no-first-run", "--no-default-browser-check",
        "--disable-gpu", "--disable-software-rasterizer",
        "--no-sandbox",
        "--disable-dev-shm-usage",
        "--user-data-dir=" + PROFILE,
        "--remote-debugging-pipe",
    ]
    sys.stderr.write("[run_poc] launching: %s\n" % " ".join(args))

    proc = subprocess.Popen(
        args,
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.PIPE,
        pass_fds=(3, 4),
    )
    os.close(3)
    os.close(4)

    sink_hits = []
    stderr_lines = []

    def pump_stderr():
        for raw in iter(proc.stderr.readline, b""):
            try:
                line = raw.decode("utf-8", "replace").rstrip()
            except Exception:
                line = repr(raw)
            stderr_lines.append(line)
            if "POC-SINK" in line:
                sink_hits.append(line)
                sys.stderr.write(line + "\n")

    t = threading.Thread(target=pump_stderr, daemon=True)
    t.start()

    # Give Chrome a moment to start; ASAN binaries are slow to initialise.
    time.sleep(5)
    if proc.poll() is not None:
        sys.stderr.write("[run_poc] chrome exited early rc=%r\n" % proc.returncode)
        for ln in stderr_lines[-40:]:
            sys.stderr.write("  " + ln + "\n")
        return 1

    pending = {}
    next_id = [1]
    buf = bytearray()
    lock = threading.Lock()
    events = []
    evt_cond = threading.Condition(lock)

    def reader():
        while True:
            try:
                chunk = os.read(evt_r, 65536)
            except OSError:
                break
            if not chunk:
                break
            buf.extend(chunk)
            while True:
                idx = buf.find(b"\x00")
                if idx < 0:
                    break
                msg = bytes(buf[:idx])
                del buf[:idx + 1]
                try:
                    obj = json.loads(msg)
                except Exception:
                    continue
                with lock:
                    if "id" in obj and obj["id"] in pending:
                        pending.pop(obj["id"]).append(obj)
                    else:
                        events.append(obj)
                    evt_cond.notify_all()

    rt = threading.Thread(target=reader, daemon=True)
    rt.start()

    def send(method, params=None, session_id=None, timeout=60):
        mid = next_id[0]
        next_id[0] += 1
        msg = {"id": mid, "method": method, "params": params or {}}
        if session_id:
            msg["sessionId"] = session_id
        slot = []
        with lock:
            pending[mid] = slot
        os.write(cmd_w, json.dumps(msg).encode() + b"\x00")
        deadline = time.time() + timeout
        with lock:
            while not slot and time.time() < deadline:
                evt_cond.wait(1.0)
        if not slot:
            raise RuntimeError("timeout waiting for %s" % method)
        resp = slot[0]
        if "error" in resp:
            raise RuntimeError("%s -> %s" % (method, resp["error"]))
        return resp.get("result", {})

    def wait_event(name, session_id=None, pred=None, timeout=60):
        deadline = time.time() + timeout
        with lock:
            while time.time() < deadline:
                for i, e in enumerate(events):
                    if e.get("method") != name:
                        continue
                    if session_id and e.get("sessionId") != session_id:
                        continue
                    if pred and not pred(e.get("params", {})):
                        continue
                    events.pop(i)
                    return e.get("params", {})
                evt_cond.wait(1.0)
        raise RuntimeError("timeout waiting for event %s" % name)

    try:
        sys.stderr.write("[run_poc] creating target for %s\n" % POC_URL)
        r = send("Target.createTarget", {"url": "about:blank"})
        target_id = r["targetId"]
        r = send("Target.attachToTarget",
                 {"targetId": target_id, "flatten": True})
        sid = r["sessionId"]

        send("Page.enable", session_id=sid)
        send("Page.navigate", {"url": POC_URL}, session_id=sid)
        wait_event("Page.loadEventFired", session_id=sid, timeout=120)
        sys.stderr.write("[run_poc] page loaded; calling Page.printToPDF\n")

        r = send("Page.printToPDF",
                 {"displayHeaderFooter": False, "printBackground": True},
                 session_id=sid, timeout=300)
        pdf_b64 = r.get("data", "")
        sys.stderr.write("[run_poc] Page.printToPDF returned %d bytes (b64)\n"
                         % len(pdf_b64))

    finally:
        try:
            send("Browser.close", timeout=10)
        except Exception:
            pass
        try:
            os.close(cmd_w)
        except Exception:
            pass
        proc.wait(timeout=30)

    # Write captured stderr / sink hits.
    with open("/tmp/chrome-stderr.log", "w") as f:
        f.write("\n".join(stderr_lines))

    print("\n=== POC-SINK HITS (%d) ===" % len(sink_hits))
    for line in sink_hits:
        print(line)

    if sink_hits:
        sys.stderr.write(
            "\n[run_poc] PROOF: renderer-authored clip-path geometry reached "
            "PathOps Op() inside the PrintCompositor utility process.\n")
        return 0
    sys.stderr.write("[run_poc] no POC-SINK hits observed\n")
    return 1


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