sots-re/verify/results/research-completion-abi-independent/reproduce_run_7d85d45cb2196e07025e5096.py

215 lines
10 KiB
Python

#!/usr/bin/env python3
"""Fresh verifier-owned ABI/static-state reproduction; refuses overwrite."""
import hashlib
import json
import pathlib
import struct
import subprocess
ROOT = pathlib.Path("/home/alex/sots-re")
SESSION = "run-7d85d45cb2196e07025e5096"
OUT = ROOT / "verify/results/research-completion-abi-independent" / SESSION
EXE = ROOT / "dumps/sots.exe"
SAVE = ROOT / "verify/results/saves/turn3-state.sav"
OBJDUMP = pathlib.Path("/usr/bin/objdump")
WINDOWS = [
("observed-alloc-narrow", 0x57E590, 0x57E5E4),
("observed-alloc-wide", 0x57E590, 0x57E5E6),
("player-append-narrow", 0x86C580, 0x86C62E),
("player-append-wide", 0x86C580, 0x86C630),
("player-copy-narrow", 0x7693F0, 0x7694C0),
("player-copy-wide", 0x7693F0, 0x7694C2),
("observed-push-narrow", 0x7B7320, 0x7B739F),
("observed-push-wide", 0x7B7320, 0x7B73A1),
("observed-realloc-narrow", 0x7B34E0, 0x7B35EF),
("observed-realloc-wide", 0x7B34E0, 0x7B35F1),
("string-alloc-narrow", 0x4249A0, 0x424ADA),
("string-alloc-wide", 0x4249A0, 0x424ADC),
("observed-ctor-control", 0x8562A0, 0x85630D),
("observed-copy-control", 0x79A150, 0x79A1D9),
("player-dtor-control", 0x61AE90, 0x61AEFC),
("import-thunks-control", 0x924FAA, 0x924FBC),
("dedup-narrow", 0x825D40, 0x825E65),
("dedup-wide", 0x825D40, 0x825E67),
("string-not-equal", 0x46F8C0, 0x46F8F0),
("string-compare", 0x4236A0, 0x42370D),
("byte-compare", 0x422720, 0x422796),
("player-ctor", 0x84EE30, 0x84EEF4),
]
EXPECTED = {
"observed-alloc": "c2 04 00", "player-append": "c2 04 00",
"player-copy": "c2 04 00", "observed-push": "c2 04 00",
"observed-realloc": "c2 04 00", "string-alloc": "c2 08 00",
"dedup": "c2 08 00",
}
DIRECT = {
"observed-alloc": (0x57E5E3, "c20400"),
"player-append": (0x86C62D, "c20400"),
"player-copy": (0x7694BF, "c20400"),
"observed-push": (0x7B739E, "c20400"),
"observed-realloc": (0x7B35EE, "c20400"),
"string-alloc": (0x424AD9, "c20800"),
"dedup": (0x825E64, "c20800"),
"plain-ret": (0x85630C, "c3"),
"constructor-defaults": (0xAF0DC8, "ffff7f7fffff7f7fffff7f7f"),
}
def sha(data):
return hashlib.sha256(data).hexdigest()
def rows(data):
return [x.rstrip() for x in data.splitlines()
if b":" in x and x.lstrip()[:1].isdigit()]
def pe_reader(data):
pe = struct.unpack_from("<I", data, 0x3c)[0]
assert data[pe:pe + 4] == b"PE\0\0"
sections = 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
image_base = struct.unpack_from("<I", data, opt + 28)[0]
table = opt + opt_size
entries = []
for i in range(sections):
off = table + 40 * i
name = data[off:off + 8].rstrip(b"\0").decode("ascii")
vsize, va, raw_size, raw = struct.unpack_from("<IIII", data, off + 8)
entries.append((name, va, max(vsize, raw_size), raw, raw_size))
def read_va(address, size):
rva = address - image_base
for name, va, span, raw, raw_size in entries:
if va <= rva and rva + size <= va + span:
delta = rva - va
assert delta + size <= raw_size, (name, hex(address), size)
return data[raw + delta:raw + delta + size], name, raw + delta
raise AssertionError(("unmapped", hex(address), size))
return image_base, entries, read_va
def containers(obj):
if isinstance(obj, dict):
if isinstance(obj.get("_items"), list):
yield obj["_items"]
for value in obj.values():
yield from containers(value)
elif isinstance(obj, list):
for value in obj:
yield from containers(value)
def named(items, name):
return [x for x in items if isinstance(x, dict) and x.get("name") == name]
OUT.mkdir(exist_ok=False)
exe_data = EXE.read_bytes()
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}", "dumps/sots.exe"]
p = subprocess.run(argv, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
streams[name] = p.stdout
(OUT / f"{name}.stdout.txt").write_bytes(p.stdout)
(OUT / f"{name}.stderr.txt").write_bytes(p.stderr)
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))})
comparisons = []
for base, expected in EXPECTED.items():
narrow, wide = rows(streams[base + "-narrow"]), rows(streams[base + "-wide"])
ok = (narrow[:-1] == wide[:-1] and b"\tc2 " in narrow[-1]
and expected.encode() in wide[-1])
comparisons.append({"name": base, "preceding_rows_equal": narrow[:-1] == wide[:-1],
"narrow_terminal": narrow[-1].decode(),
"wide_terminal": wide[-1].decode(), "pass": ok})
image_base, sections, read_va = pe_reader(exe_data)
direct = []
for name, (address, expected_hex) in DIRECT.items():
expected = bytes.fromhex(expected_hex)
actual, section, file_offset = read_va(address, len(expected))
direct.append({"name": name, "address": hex(address), "section": section,
"file_offset": file_offset, "expected": expected_hex,
"actual": actual.hex(), "pass": actual == expected})
before, section, file_offset = read_va(0x85630B, 1)
direct.append({"name": "plain-ret-minus-one-negative-control", "address": "0x85630b",
"section": section, "file_offset": file_offset, "actual": before.hex(),
"expected_not": "c3", "pass": before != b"\xc3"})
commands = [
("save-reader", ["python3", "verify/save-reader/save_reader.py", str(SAVE.relative_to(ROOT)),
"--dump", "--json", "--strict"]),
("state-checksum", ["python3", "verify/state-checksum/state_checksum.py",
str(SAVE.relative_to(ROOT)), "--json"]),
]
tool_runs = []
for name, argv in commands:
p = subprocess.run(argv, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(OUT / f"turn3-{name}.json").write_bytes(p.stdout)
(OUT / f"turn3-{name}.stderr.txt").write_bytes(p.stderr)
tool_runs.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)}})
save_json = json.loads((OUT / "turn3-save-reader.json").read_text())
checksum = json.loads((OUT / "turn3-state-checksum.json").read_text())
evnx = [x[0]["value"] for x in (named(c, "EvNxID") for c in containers(save_json)) if x]
event3 = []
turn3_buckets = []
for c in containers(save_json):
eid = named(c, "EvEID")
if eid and eid[0]["value"] == 3:
event3.append({x.get("name", x.get("_name")): x for x in c})
turn = named(c, "EvTurn")
if turn and turn[0]["value"] == 3:
nested = [x for x in c if isinstance(x, dict) and x.get("_name") == "Events"]
if nested:
counts = named(nested[0]["_items"], ".")
if counts:
turn3_buckets.append(counts[0]["value"])
assert event3
ev = event3[0]
pos = [x["value"] for x in ev["EvPos"]["_items"]]
state = {
"stats": save_json["stats"], "evnxid_values": evnx, "turn3_bucket_counts": turn3_buckets,
"event3": {"description": ev["EvDsc"]["value"], "message": ev["EvMsg"]["value"],
"image": ev["EvImg"]["value"], "location": ev["EvLoc"]["value"],
"action": ev["EvAct"]["value"], "chain": ev["EvCID"]["value"],
"position_words": pos},
"coverage": checksum["coverage"], "root": checksum["root"],
"rng": next(x for x in checksum["subsystems"] if x["path"] == "/Sim/RNG"),
}
state["pass"] = (
save_json["stats"]["resyncs"] == 0 and save_json["stats"]["hint_failures"] == 0
and save_json["stats"]["best_effort"] == 0 and 4 in evnx and 2 in turn3_buckets
and state["event3"] == {"description": "Research Over Budget",
"message": "Research for Waldo Units has gone overbudget.",
"image": "EVENT_RESEARCH_OVERBUDGET", "location": 0, "action": 1,
"chain": 0, "position_words": [2139095039] * 3}
and checksum["coverage"] == {"ok": True, "rebuiltBytes": 609080,
"inflatedBytes": 609080, "firstDiff": None}
and state["rng"]["leaves"] == 1 and state["rng"]["valueBytes"] == 2503
and state["rng"]["digest"] == "0978fdf34ff7962f76c2de810dc93e0a")
(OUT / "independent-state.json").write_text(json.dumps(state, indent=2) + "\n")
manifest = {"schema": "sots-abi-independent-static/2", "session": SESSION,
"scope": "independent static reproduction plus archived-state inspection; no live game/allocator/RNG execution",
"input": {"path": "dumps/sots.exe", "bytes": len(exe_data), "sha256": sha(exe_data)},
"save": {"path": str(SAVE.relative_to(ROOT)), "bytes": SAVE.stat().st_size,
"sha256": sha(SAVE.read_bytes())},
"tool": {"path": str(OBJDUMP), "sha256": sha(OBJDUMP.read_bytes())},
"windows": records, "comparisons": comparisons,
"direct_pe": {"image_base": hex(image_base), "sections": [x[0] for x in sections],
"records": direct},
"state_tools": tool_runs,
"all_processes_positive": all(x["returncode"] == 0 and x["stdout"]["bytes"] > 0
and x["stderr"]["bytes"] == 0
for x in records + tool_runs),
"all_boundary_predictions_hold": all(x["pass"] for x in comparisons),
"direct_pe_challenge_pass": all(x["pass"] for x in direct),
"independent_state_pass": state["pass"]}
(OUT / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
if not all((manifest["all_processes_positive"], manifest["all_boundary_predictions_hold"],
manifest["direct_pe_challenge_pass"], manifest["independent_state_pass"])):
raise SystemExit(1)