188 lines
9.5 KiB
Python
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"]))
|