sots-re/verify/results/research-completion-abi/capture_run_35e59053176da458d8b46fea.py

188 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""Session-bound current-source static ABI capture; refuses output reuse."""
import hashlib
import json
import pathlib
import struct
import subprocess
import sys
from datetime import datetime, timezone
ROOT = pathlib.Path("/home/alex/sots-re")
SESSION = "run-35e59053176da458d8b46fea"
OUT = ROOT / "verify/results/research-completion-abi" / (SESSION + "-repair1")
ENGINE = pathlib.Path("/tmp/opencode/sots-final-research-engine")
RE = pathlib.Path("/tmp/opencode/sots-final-research-re")
CAMPAIGN = ROOT / "tools/campaign.py"
EXE = ROOT / "dumps/sots.exe"
SAVE = ROOT / "verify/results/saves/turn3-state.sav"
OBJDUMP = pathlib.Path("/usr/bin/objdump")
EXPECTED = {
"engine": "ccd8e02083e8d2e2b3e97976ace2273c8f924dfc02a39e919004eaf3544c50fd",
"re": "e5cf1c44761bebcada1aacb53f30722bdaf4784f21a9ec1023a1851c142c124f",
"exe": "970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841",
"objdump": "1eaaef2e7f57c4c7f69115c495e2466f5a8c8e5f3bc42221d092382f30f9d4cd",
"save": "978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921",
}
# Names classify evidence only; these windows expose instructions but execute no branch.
WINDOWS = (
("observed-ctor", 0x8562A0, 0x856310),
("observed-append", 0x7B7320, 0x7B73A2),
("observed-copy", 0x79A150, 0x79A1DC),
("observed-growth", 0x7B34E0, 0x7B35F2),
("observed-alloc", 0x57E590, 0x57E5E7),
("player-ctor", 0x84EE30, 0x84EEF4),
("player-append", 0x86C580, 0x86C631),
("player-growth", 0x869500, 0x869620),
("player-copy", 0x7693F0, 0x7694C3),
("player-dtor", 0x61AE90, 0x61AEFC),
("string-assign", 0x4249A0, 0x424ADD),
("import-thunks", 0x924FAA, 0x924FBC),
("turn-get-create", 0x885380, 0x88544E),
("turn-append", 0x884CB0, 0x884D93),
("turn-growth", 0x8841A0, 0x884300),
("nested-copy-authority", 0x779850, 0x779930),
("nested-dtor", 0x629580, 0x6295CE),
("turn-virtual-dtor", 0x62E120, 0x62E170),
("dedup", 0x825D40, 0x825E67),
("string-not-equal", 0x46F8C0, 0x46F8F0),
("prune", 0x879EB0, 0x879FC0),
("nested-copy-stop-9913", 0x779850, 0x779913),
("nested-copy-stop-9915", 0x779850, 0x779915),
("nested-copy-stop-a20", 0x779850, 0x779A20),
("nested-copy-stop-a21", 0x779850, 0x779A21),
("nested-copy-stop-a23", 0x779850, 0x779A23),
("nested-copy-stop-a28", 0x779850, 0x779A28),
)
def sha(data):
return hashlib.sha256(data).hexdigest()
def ident(path):
target = path.resolve()
data = target.read_bytes()
return {"path": str(path), "resolved_path": str(target), "bytes": len(data), "sha256": sha(data)}
def binding():
argv = [sys.executable, str(CAMPAIGN), "--state-root", str(ROOT), "source-binding",
"research-completion-abi", "--engine-worktree", str(ENGINE),
"--re-worktree", str(RE)]
p = subprocess.run(argv, cwd=ROOT, capture_output=True, text=True, check=True)
return {"argv": argv, "value": json.loads(p.stdout)}
def rows(data):
return [line.rstrip() for line in data.decode("utf-8").splitlines()
if ":" in line and line.lstrip()[:1].isdigit()]
def pe_reader(data):
pe = struct.unpack_from("<I", data, 0x3C)[0]
assert data[pe:pe + 4] == b"PE\0\0"
count = struct.unpack_from("<H", data, pe + 6)[0]
opt_size = struct.unpack_from("<H", data, pe + 20)[0]
opt = pe + 24
assert struct.unpack_from("<H", data, opt)[0] == 0x10B
base = struct.unpack_from("<I", data, opt + 28)[0]
sections = []
for i in range(count):
off = opt + opt_size + 40 * i
name = data[off:off + 8].rstrip(b"\0").decode("ascii")
vsize, va, raw_size, raw = struct.unpack_from("<IIII", data, off + 8)
sections.append((name, va, max(vsize, raw_size), raw, raw_size))
def read_va(address, size):
rva = address - base
for name, va, span, raw, raw_size in sections:
if va <= rva and rva + size <= va + span:
delta = rva - va
assert delta + size <= raw_size
return data[raw + delta:raw + delta + size], name, raw + delta
raise AssertionError(("unmapped", hex(address), size))
return base, read_va
if len(sys.argv) != 2 or sys.argv[1] != SESSION:
raise SystemExit("usage: capture script with exact session argument")
if OUT.exists():
raise SystemExit("refusing existing output directory")
before = binding()
files = {"binary": ident(EXE), "objdump": ident(OBJDUMP), "python": ident(pathlib.Path(sys.executable)),
"save": ident(SAVE), "campaign": ident(CAMPAIGN),
"save_reader": ident(ROOT / "verify/save-reader/save_reader.py"),
"state_checksum": ident(ROOT / "verify/state-checksum/state_checksum.py")}
identity_failures = [key for key in ("exe", "objdump", "save")
if files[{"exe":"binary", "objdump":"objdump", "save":"save"}[key]]["sha256"] != EXPECTED[key]]
if before["value"]["engine"]["sha256"] != EXPECTED["engine"] or before["value"]["re"]["sha256"] != EXPECTED["re"]:
identity_failures.append("source")
if identity_failures:
raise SystemExit("identity drift: " + ",".join(identity_failures))
OUT.mkdir(exist_ok=False)
records, streams = [], {}
for name, start, stop in WINDOWS:
argv = [str(OBJDUMP), "-D", "-Mintel", f"--start-address=0x{start:08x}",
f"--stop-address=0x{stop:08x}", str(EXE)]
p = subprocess.run(argv, cwd=ROOT, capture_output=True)
(OUT / f"{name}.stdout.txt").write_bytes(p.stdout)
(OUT / f"{name}.stderr.txt").write_bytes(p.stderr)
streams[name] = p.stdout
records.append({"name": name, "argv": argv, "returncode": p.returncode,
"stdout": {"bytes": len(p.stdout), "sha256": sha(p.stdout)},
"stderr": {"bytes": len(p.stderr), "sha256": sha(p.stderr)},
"instruction_rows": len(rows(p.stdout))})
exe_data = EXE.read_bytes()
image_base, read_va = pe_reader(exe_data)
direct = {}
for name, address, size in (("ret4", 0x779912, 3), ("a20-family", 0x779A1F, 9),
("observed-stride", 0x7B7333, 8), ("player-stride", 0x86C593, 8),
("turn-stride", 0x884CC3, 8), ("constructor-defaults", 0xAF0DC8, 12)):
data, section, file_offset = read_va(address, size)
direct[name] = {"va": f"0x{address:08x}", "bytes": data.hex(), "section": section,
"file_offset": file_offset}
comparisons = {
"9913_is_truncated_terminal": (rows(streams["nested-copy-stop-9913"])[:-1] == rows(streams["nested-copy-stop-9915"])[:-1]
and "779912:" in rows(streams["nested-copy-stop-9913"])[-1].replace(" ", "")
and "c2" in rows(streams["nested-copy-stop-9913"])[-1].lower()
and "c2 04 00" not in rows(streams["nested-copy-stop-9913"])[-1].lower()),
"9915_terminal_ret4": "c2 04 00" in rows(streams["nested-copy-stop-9915"])[-1].lower(),
"authority_contains_ret4": any("c2 04 00" in x.lower() for x in rows(streams["nested-copy-authority"])),
"a20_is_truncated_terminal": (rows(streams["nested-copy-stop-a20"])[:-1] == rows(streams["nested-copy-stop-a21"])[:-1]
and "779a1f:" in rows(streams["nested-copy-stop-a20"])[-1].replace(" ", "")
and "74" in rows(streams["nested-copy-stop-a20"])[-1].lower()
and "74 10" not in rows(streams["nested-copy-stop-a20"])[-1].lower()),
"a21_adds_complete_je": "74 10" in rows(streams["nested-copy-stop-a21"])[-1].lower(),
"a23_adds_complete_mov": "8b cf" in rows(streams["nested-copy-stop-a23"])[-1].lower(),
"a28_adds_complete_call": "e8 28 6a ff ff" in rows(streams["nested-copy-stop-a28"])[-1].lower(),
"direct_ret4": direct["ret4"]["bytes"] == "c20400",
"direct_a20_family": direct["a20-family"]["bytes"] == "74108bcfe8286affff",
"direct_defaults": direct["constructor-defaults"]["bytes"] == "ffff7f7fffff7f7fffff7f7f",
}
after = binding()
checks = {
"all_commands_zero": all(x["returncode"] == 0 for x in records),
"all_stdout_nonempty": all(x["stdout"]["bytes"] > 0 for x in records),
"all_stderr_empty": all(x["stderr"]["bytes"] == 0 for x in records),
"source_stable": before["value"] == after["value"],
**comparisons,
}
manifest = {
"schema": "sots-abi-static-capture/2", "session": SESSION,
"actor": "research-abi-analyst", "role": "analyst", "model": "openai/gpt-5.6-sol",
"timestamp": datetime.now(timezone.utc).isoformat(),
"decisions": ["d-e9d6d48cd77538556d1a1c1f", "d-6e30d172051af0385f5979eb"],
"scope": "Static instruction/byte capture and immutable input identity; no game/runtime execution.",
"source_before": before, "source_after": after, "files": files,
"image_base": f"0x{image_base:08x}", "commands": records, "direct_pe": direct,
"checks": [{"name": k, "status": "pass" if v else "fail"} for k, v in checks.items()],
"failed": [k for k, v in checks.items() if not v],
"static_branch_exposure": ["existing-match/miss", "spare/full capacity", "empty/nonempty nested copy",
"null/nonnull destruction", "short/long string", "normal/unwind", "bad/aligned boundaries"],
"runtime_unexercised": ["all listed static branches", "allocator safety", "event construction and IDs",
"PostEvent caller defaults/write order", "exception throw", "RNG state/draws"],
"original_helpers": ["0x00924fb6 allocation thunk", "0x00924faa deallocation thunk",
"0x0046f8c0 original string comparison when called"],
}
(OUT / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(json.dumps({"output": str(OUT.relative_to(ROOT)), "commands": len(records), "failed": manifest["failed"]}, indent=2))
raise SystemExit(bool(manifest["failed"]))