215 lines
10 KiB
Python
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)
|