mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-23 12:00:30 +02:00
1854 lines
82 KiB
Python
1854 lines
82 KiB
Python
"""Bounded frozen-byte oracle for ActorLooking direction decode + movement caller.
|
|
|
|
The harness runs the January target, the first natural candidate, and the
|
|
frozen ordinary-Ninja candidate. It enters the genuine private decoder ABI
|
|
and the genuine public ``actor_look_affect_movement`` caller. The reached
|
|
``normalize3d``, ``vector_from_points3d``, and ``valid_real_normal3d`` COFF
|
|
bodies execute as machine code. Only genuine outside engine/CRT APIs are
|
|
modeled, with their stack arguments decoded and recorded.
|
|
|
|
This is deliberately finite evidence. In particular, the January decoder
|
|
inlines its first normal-vector validity test while the candidates call the
|
|
byte-identical out-of-line helper. The corpus exercises both x87 control
|
|
words but is not exhaustive proof at floating-point decision boundaries.
|
|
|
|
The script writes only a new JSON path under ``scratch``. It never compiles,
|
|
invokes Ninja, edits production/configuration files, or mutates input objects.
|
|
Negative controls alter copies of already-relocated candidate bytes in memory.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections import Counter
|
|
import hashlib
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
import unicorn as u
|
|
from unicorn.x86_const import (
|
|
UC_X86_REG_EAX,
|
|
UC_X86_REG_EBP,
|
|
UC_X86_REG_EBX,
|
|
UC_X86_REG_ECX,
|
|
UC_X86_REG_EDI,
|
|
UC_X86_REG_EDX,
|
|
UC_X86_REG_EFLAGS,
|
|
UC_X86_REG_EIP,
|
|
UC_X86_REG_ESI,
|
|
UC_X86_REG_ESP,
|
|
UC_X86_REG_FPCW,
|
|
UC_X86_REG_FPSW,
|
|
UC_X86_REG_FPTAG,
|
|
)
|
|
|
|
|
|
SCRIPT = Path(__file__).resolve()
|
|
ROOT = next(parent for parent in SCRIPT.parents if (parent / "tools/coff_compare.py").is_file())
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from tools import coff_compare as cc # noqa: E402
|
|
from tools.campaign._coff_tools import function_sections # noqa: E402
|
|
from tools.regression_gate import _section_aux_selection # noqa: E402
|
|
|
|
|
|
TARGET = ROOT / "scratch/actor-looking-decoder-before-20260906/files/build/split/source/ai/actor_looking.obj"
|
|
FIRST = ROOT / "scratch/actor-looking-decoder-first-20260906.obj"
|
|
ACTUAL = ROOT / "scratch/actor-looking-decoder-first-20260906/source/ai/actor_looking.obj"
|
|
BEFORE_MANIFEST = ROOT / "scratch/actor-looking-decoder-before-20260906.json"
|
|
FIRST_MANIFEST = ROOT / "scratch/actor-looking-decoder-first-20260906.json"
|
|
BEFORE_STABLE = ROOT / "scratch/actor-looking-decoder-before-stable-20260906.json"
|
|
FIRST_STABLE = ROOT / "scratch/actor-looking-decoder-first-stable-20260906.json"
|
|
CACHEBETA = ROOT / "cachebeta.exe"
|
|
TARGET_ALERT = ROOT / "build/split/source/ai/action_alert.obj"
|
|
TARGET_CHARGE = ROOT / "build/split/source/ai/action_charge.obj"
|
|
TARGET_COMBAT = ROOT / "build/split/source/ai/actor_combat.obj"
|
|
OUTPUT_DEFAULT = ROOT / "scratch/actor-looking-decoder-runtime-report.json"
|
|
|
|
PINNED_HASHES = {
|
|
"target": "443e269f49461dcaf5ff3804fc8770cc5579798427289c60f56a1ce533a226de",
|
|
"first": "4dc689d2ca1e996ed9786915663ed0962dfbb8ef3556b6edb36d4bcd42f43387",
|
|
"actual": "0d3378d771f780f909a6d7d29d2c9eec14cfd65f5901c6efbec82a5fd737b175",
|
|
"before_manifest": "5edecbfb16f421a7eedafb89121ef42348cee3ecdbfe45ce8a0f272b6263e3ef",
|
|
"first_manifest": "513b356efb3903cc6f3d0f012e5506e6152601c0d4a09c5843cb89d7346003e8",
|
|
"before_stable": "b13c8826c3323d4a472bf46c94af12b818cc7b8d67fd402e0eb13aafe200b1a6",
|
|
"first_stable": "edbbb60142e71b3132b38bc00563c945c4a06038c998a204c4685c4f25cdff98",
|
|
"cachebeta": "4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520",
|
|
"target_alert": "351ff0de9ddfea9b3c03a9038abab75b0c4b48290443b33e09c33105b41ab61a",
|
|
"target_charge": "76c4c0b62850ffaa42fe5027c3e68b2cc42f7640c1156342c9a9cc4d1125afea",
|
|
"target_combat": "6030e4891bf64d0cb0c41028a6c816ef07c4442f6c573ac70f37fb1e049ca45b",
|
|
"coff_compare": "22f8e11c7b5b5b1e1cb9bc51d2e48c0fa4912def9c1d48bd09db80735ee8204e",
|
|
"coff_tools": "6784bd1a1e241942f301e2691862af596554bf8fbf4333437c0451988660d3c5",
|
|
"regression_gate": "299f15d1569096730508c85eee9271ba4798c8e4f42f51bdccb89f1c1d84ea3f",
|
|
}
|
|
|
|
DECODER = "_actor_look_decode_direction"
|
|
AFFECT = "_actor_look_affect_movement"
|
|
TARGET_DECODER = "_code_00016c70"
|
|
HELPERS = (
|
|
"_square_root",
|
|
"_vector_from_points3d",
|
|
"_scale_vector3d",
|
|
"_magnitude_squared3d",
|
|
"_magnitude3d",
|
|
"_normalize3d",
|
|
"_valid_real",
|
|
"_valid_realcmp",
|
|
"_valid_real_normal3d",
|
|
)
|
|
EXECUTED_HELPERS = {
|
|
"_vector_from_points3d",
|
|
"_normalize3d",
|
|
"_valid_real_normal3d",
|
|
}
|
|
APIS = (
|
|
"_datum_get",
|
|
"_datum_try_and_get",
|
|
"_object_try_and_get_and_verify_type",
|
|
"_unit_get_head_position",
|
|
"_object_get_origin",
|
|
"_actor_path_has_path",
|
|
"_display_assert",
|
|
"_system_exit",
|
|
"_sprintf",
|
|
"_csprintf",
|
|
)
|
|
|
|
FUNCTION_FACTS = {
|
|
"target": {
|
|
DECODER: (1632, 98, "4f3714fab510f0c407dc0792b3cbea003ba4d1dfdb4aee74d201d9a9e5dc14a9", 2),
|
|
AFFECT: (128, 4, "5d7d253b925542d2e1c1c21c83c6a2f2049df1a6fe051b6f713b377c47f85dde", 2),
|
|
},
|
|
"first": {
|
|
DECODER: (1616, 99, "bea2ec692aedec4276c07fb076ad2dd53888277627541c42fb56dea1670b3008", 3),
|
|
AFFECT: (128, 4, "d096436ec233acf74823c8ce959e2ded832eb046b0c8b441be976c5e6cf5dafa", 2),
|
|
},
|
|
"actual": {
|
|
DECODER: (1616, 99, "bea2ec692aedec4276c07fb076ad2dd53888277627541c42fb56dea1670b3008", 3),
|
|
AFFECT: (128, 4, "d096436ec233acf74823c8ce959e2ded832eb046b0c8b441be976c5e6cf5dafa", 2),
|
|
},
|
|
}
|
|
HELPER_FACTS = {
|
|
"_square_root": (16, 0, "0717923dacb4356a94553a32f0ba8a314904fae6d7d0bfc42ecc4af6f10415b7"),
|
|
"_vector_from_points3d": (48, 0, "905f56dbe4597d2bacc201456ffc33464b166e0b6392dda24a1a3e109b637aa2"),
|
|
"_scale_vector3d": (48, 0, "6436919fbdc80b5d5fe76d9c92fa146e5ac7d3b4637c9b75aaa62401f0c54a3b"),
|
|
"_magnitude_squared3d": (48, 0, "0d04579096eeab3a1c45f87774e1eb20ce4856dbac219f624a855e8c9ae99725"),
|
|
"_magnitude3d": (48, 0, "114f10e7ee4543446daaaafc859a4ec729ba6c8c74c38825d05bd0f00a42352c"),
|
|
"_normalize3d": (96, 3, "67fbd33f204d0c93cce135fcfd37d4ce5b443642edc6ad99230846b91c893e5b"),
|
|
"_valid_real": (32, 0, "83977ed5f0ae820e1554588c354ae57295d798d933e591ee898dc39d29731d4f"),
|
|
"_valid_realcmp": (64, 1, "af811d6df5f204708d247a7d50e091f7828989672ded12773edd93431f74852c"),
|
|
"_valid_real_normal3d": (96, 2, "0958b20f958923b9a2b2561eebda24ae8838b04180389aaaf3926319b19599c0"),
|
|
}
|
|
|
|
CODE_ADDRESSES = {DECODER: 0x100000, AFFECT: 0x110000}
|
|
CODE_ADDRESSES.update({name: 0x120000 + index * 0x2000 for index, name in enumerate(HELPERS)})
|
|
STUB_BASE = 0x200000
|
|
STUB_ADDRESSES = {name: STUB_BASE + 0x100 * (index + 1) for index, name in enumerate(APIS)}
|
|
STOP = 0x20F000
|
|
RDATA = 0x300000
|
|
RDATA_SIZE = 0x40000
|
|
GLOBALS = 0x400000
|
|
ACTOR_DATA_SLOT = GLOBALS + 0x100
|
|
PROP_DATA_SLOT = GLOBALS + 0x104
|
|
ACTOR_DATA_VALUE = 0x2468ACE0
|
|
PROP_DATA_VALUE = 0x13579BDF
|
|
ACTOR = 0x500000
|
|
SPEC = 0x502000
|
|
DIRECTION = 0x504000
|
|
PROP = 0x506000
|
|
OBJECT = 0x508000
|
|
TEMPORARY = 0x50A000
|
|
STACK = 0x600000
|
|
STACK_SIZE = 0x20000
|
|
ENTRY_SP = STACK + 0x10000
|
|
|
|
ACTOR_SIZE = 0x800
|
|
SMALL_SIZE = 0x200
|
|
TEMPORARY_SIZE = 0x1000
|
|
PAGE_SIZE = 0x1000
|
|
CALLER_FRAME_SIZE = 0x80
|
|
FPCW_VALUES = (0x027F, 0x037F)
|
|
NONE = 0xFFFFFFFF
|
|
|
|
HEAD_POSITION = 0x120
|
|
TARGET_PROP_INDEX = 0x270
|
|
DANGER_TYPE = 0x280
|
|
DANGER_POSITION = 0x2B0
|
|
PRIMARY_PRIORITY = 0x3E8
|
|
PRIMARY_DIRECTION = 0x3EC
|
|
MOVING = 0x504
|
|
FORCED_BY_AIMING = 0x505
|
|
MOVING_POINT = 0x50C
|
|
MOVING_VECTOR = 0x518
|
|
FORCED_DIRECTION = 0x524
|
|
FIRE_STATE = 0x5F2
|
|
FIRE_TARGET_TYPE = 0x60C
|
|
AIMING_AT_FIRE_TARGET = 0x628
|
|
FIRE_AIM_VECTOR = 0x63C
|
|
BURST_AIM_VECTOR = 0x68C
|
|
|
|
PROP_OWNER = 0x04
|
|
PROP_CENTER = 0xC8
|
|
PROP_HEAD = 0x104
|
|
OBJECT_TYPE = 0x64
|
|
|
|
DIRECTION_MOVEMENT = 0
|
|
DIRECTION_PROP = 1
|
|
DIRECTION_TARGET = 2
|
|
DIRECTION_POINT = 3
|
|
DIRECTION_VECTOR = 4
|
|
DIRECTION_DANGER = 5
|
|
DIRECTION_OBJECT = 6
|
|
FIRE_BURSTING = 2
|
|
PRIMARY_AIMING = 3
|
|
|
|
NONVOLATILE = {
|
|
"ebp": UC_X86_REG_EBP,
|
|
"ebx": UC_X86_REG_EBX,
|
|
"esi": UC_X86_REG_ESI,
|
|
"edi": UC_X86_REG_EDI,
|
|
}
|
|
SAVED = {
|
|
"ebp": 0x1122AABB,
|
|
"ebx": 0x2233BBCC,
|
|
"esi": 0x3344CCDD,
|
|
"edi": 0x4455DDEE,
|
|
}
|
|
|
|
EXE_CONSTANTS = {
|
|
"??_C@_00CNPNBAHC@?$AA@": (2372623, b""),
|
|
"??_C@_0CL@GJIEKICC@?$CFs?3?5assert_valid_real_normal3d?$CI?$CF@": (
|
|
2377156,
|
|
b"%s: assert_valid_real_normal3d(%f, %f, %f)",
|
|
),
|
|
}
|
|
FORMAT_RE = re.compile(r"%(?P<flags>[-+ #0]*)(?P<width>\d*)(?:\.(?P<precision>\d+))?(?P<kind>[fsdXu])")
|
|
|
|
|
|
def sha256_bytes(raw: bytes) -> str:
|
|
return hashlib.sha256(raw).hexdigest()
|
|
|
|
|
|
def sha256_path(path: Path) -> str:
|
|
return sha256_bytes(path.read_bytes())
|
|
|
|
|
|
def verify_hash(path: Path, expected: str, label: str) -> dict:
|
|
actual = sha256_path(path)
|
|
if actual.lower() != expected.lower():
|
|
raise AssertionError((label, "SHA-256 mismatch", str(path), actual, expected))
|
|
return {"path": str(path), "sha256": actual, "bytes": path.stat().st_size}
|
|
|
|
|
|
def p16(value: int) -> bytes:
|
|
return struct.pack("<H", value & 0xFFFF)
|
|
|
|
|
|
def p32(value: int) -> bytes:
|
|
return struct.pack("<I", value & 0xFFFFFFFF)
|
|
|
|
|
|
def r32(machine: u.Uc, address: int) -> int:
|
|
return struct.unpack("<I", machine.mem_read(address, 4))[0]
|
|
|
|
|
|
def s32(value: int) -> int:
|
|
return struct.unpack("<i", p32(value))[0]
|
|
|
|
|
|
def f32(value: float) -> float:
|
|
return struct.unpack("<f", struct.pack("<f", value))[0]
|
|
|
|
|
|
def f32_bits(value: float) -> int:
|
|
return struct.unpack("<I", struct.pack("<f", f32(value)))[0]
|
|
|
|
|
|
def bits_f32(value: int) -> float:
|
|
return struct.unpack("<f", p32(value))[0]
|
|
|
|
|
|
def write16(raw: bytearray, offset: int, value: int) -> None:
|
|
raw[offset : offset + 2] = p16(value)
|
|
|
|
|
|
def write32(raw: bytearray, offset: int, value: int) -> None:
|
|
raw[offset : offset + 4] = p32(value)
|
|
|
|
|
|
def write_float(raw: bytearray, offset: int, value: float) -> None:
|
|
raw[offset : offset + 4] = struct.pack("<f", f32(value))
|
|
|
|
|
|
def write_vector(raw: bytearray, offset: int, value: tuple[float, float, float]) -> None:
|
|
for index, component in enumerate(value):
|
|
write_float(raw, offset + 4 * index, component)
|
|
|
|
|
|
def seeded_bytes(size: int, seed: int) -> bytes:
|
|
return bytes(((index * 37 + seed * 53 + 11) & 0xFF) for index in range(size))
|
|
|
|
|
|
def changed_offsets(before: bytes, after: bytes) -> list[int]:
|
|
if len(before) != len(after):
|
|
raise AssertionError("change comparison length mismatch")
|
|
return [index for index, (left, right) in enumerate(zip(before, after)) if left != right]
|
|
|
|
|
|
def float_token(value: float) -> str:
|
|
if math.isnan(value):
|
|
return "nan"
|
|
if math.isinf(value):
|
|
return "+inf" if value > 0 else "-inf"
|
|
return value.hex()
|
|
|
|
|
|
class Owner:
|
|
def __init__(self, label: str, path: Path, expected_hash: str):
|
|
self.label = label
|
|
self.path = path
|
|
self.input = verify_hash(path, expected_hash, label)
|
|
self.obj = cc.load(path)
|
|
self.functions = function_sections(self.obj)
|
|
|
|
|
|
class ConstantPool:
|
|
def __init__(self, owners: list[Owner], cachebeta: bytes):
|
|
self.owners = owners
|
|
self.cachebeta = cachebeta
|
|
self.section_addresses: dict[tuple[str, int], int] = {}
|
|
self.blocks: dict[int, bytes] = {}
|
|
self.records: list[dict] = []
|
|
self.next_slot = 0
|
|
|
|
def _find(self, name: str, preferred: Owner | None) -> tuple[Owner, dict] | None:
|
|
ordered = ([preferred] if preferred is not None else []) + [
|
|
owner for owner in self.owners if owner is not preferred
|
|
]
|
|
for owner in ordered:
|
|
matches = [
|
|
symbol
|
|
for symbol in owner.obj["symbols"]
|
|
if symbol["name"] == name and symbol["section"] > 0
|
|
]
|
|
if len(matches) > 1:
|
|
raise AssertionError((name, "ambiguous definition", owner.label, len(matches)))
|
|
if matches:
|
|
symbol = matches[0]
|
|
section = owner.obj["sections"][symbol["section"] - 1]
|
|
if section["flags"] & cc.IMAGE_SCN_CNT_CODE:
|
|
raise AssertionError((name, "constant resolved to code", owner.label))
|
|
return owner, symbol
|
|
return None
|
|
|
|
def address(self, name: str, addend: int, preferred: Owner | None = None) -> int:
|
|
found = self._find(name, preferred)
|
|
if found is None:
|
|
if name not in EXE_CONSTANTS:
|
|
raise AssertionError((name, "no authenticated non-code owner"))
|
|
offset, expected = EXE_CONSTANTS[name]
|
|
end = self.cachebeta.find(b"\0", offset)
|
|
if end < 0:
|
|
raise AssertionError((name, "unterminated executable string"))
|
|
raw = self.cachebeta[offset : end + 1]
|
|
if raw[:-1] != expected:
|
|
raise AssertionError((name, "executable string mismatch", raw[:-1], expected))
|
|
key = ("cachebeta.exe", offset)
|
|
symbol_value = 0
|
|
provenance = {"kind": "cachebeta.exe", "file_offset": offset}
|
|
else:
|
|
owner, symbol = found
|
|
section_number = symbol["section"]
|
|
section = owner.obj["sections"][section_number - 1]
|
|
raw = bytes(cc._section_bytes(owner.obj, section))
|
|
key = (owner.label, section_number)
|
|
symbol_value = symbol["value"]
|
|
provenance = {
|
|
"kind": "coff-section",
|
|
"owner": owner.label,
|
|
"path": str(owner.path),
|
|
"section": section_number,
|
|
}
|
|
if key not in self.section_addresses:
|
|
if len(raw) > 0x1000 or (self.next_slot + 1) * 0x1000 > RDATA_SIZE:
|
|
raise AssertionError((name, "constant arena bound", len(raw), self.next_slot))
|
|
base = RDATA + self.next_slot * 0x1000
|
|
self.next_slot += 1
|
|
self.section_addresses[key] = base
|
|
self.blocks[base] = raw
|
|
self.records.append(
|
|
{
|
|
"base": base,
|
|
"bytes": len(raw),
|
|
"sha256": sha256_bytes(raw),
|
|
**provenance,
|
|
}
|
|
)
|
|
return self.section_addresses[key] + symbol_value + addend
|
|
|
|
|
|
def direct_case(name: str, direction_type: int | None, expected_al: int | None, **values) -> dict:
|
|
result = {
|
|
"name": name,
|
|
"entry": "decoder",
|
|
"direction_type": direction_type,
|
|
"expected_al": expected_al,
|
|
"moving": 0,
|
|
"movement_vector": (3.0, 4.0, 0.0),
|
|
"movement_point": (11.0, 22.0, 33.0),
|
|
"head": (1.0, 2.0, 3.0),
|
|
"spec_vector": (0.6, 0.8, 0.0),
|
|
"spec_point": (4.0, 6.0, 3.0),
|
|
"prop_present": True,
|
|
"prop_head": (4.0, 6.0, 3.0),
|
|
"prop_center": (4.0, 6.0, 3.0),
|
|
"prop_try_sequence": ["prop"],
|
|
"target_prop": NONE,
|
|
"fire_state": 0,
|
|
"aiming": 0,
|
|
"fire_vector": (0.0, 0.0, 1.0),
|
|
"burst_vector": (0.6, 0.8, 0.0),
|
|
"danger_type": 0,
|
|
"danger_position": (4.0, 6.0, 3.0),
|
|
"object_present": True,
|
|
"object_type": 0,
|
|
"object_point": (4.0, 6.0, 3.0),
|
|
"null_spec": False,
|
|
"null_direction": False,
|
|
"expected_api": ["_datum_get"],
|
|
"vector_calls": 0,
|
|
"normalize_calls": 0,
|
|
"decoder_result_true": expected_al == 1,
|
|
"expected_direction": "unchanged",
|
|
"expected_display": [],
|
|
"terminated": False,
|
|
}
|
|
result.update(values)
|
|
return result
|
|
|
|
|
|
def affect_case(name: str, direction_type: int, priority: int, **values) -> dict:
|
|
result = {
|
|
"name": name,
|
|
"entry": "affect",
|
|
"direction_type": direction_type,
|
|
"priority": priority,
|
|
"path_result": False,
|
|
"head": (1.0, 2.0, 3.0),
|
|
"spec_vector": (0.6, 0.8, 0.0),
|
|
"spec_point": (1.0, 2.0, 3.0),
|
|
"expected_api": ["_datum_get"],
|
|
"vector_calls": 0,
|
|
"normalize_calls": 0,
|
|
"decoder_result_true": False,
|
|
"nested_decoder": False,
|
|
"expected_priority": priority,
|
|
"expected_flag": 0,
|
|
"expected_forced_direction": "unchanged",
|
|
"terminated": False,
|
|
"expected_display": [],
|
|
}
|
|
result.update(values)
|
|
return result
|
|
|
|
|
|
def cases() -> list[dict]:
|
|
diagnostic_tail = ["_sprintf", "_display_assert", "_csprintf", "_display_assert", "_system_exit"]
|
|
direct = [
|
|
direct_case(
|
|
"movement_success", DIRECTION_MOVEMENT, 1, moving=1,
|
|
normalize_calls=1, expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case("movement_not_moving", DIRECTION_MOVEMENT, 0, moving=0),
|
|
direct_case(
|
|
"prop_success", DIRECTION_PROP, 1,
|
|
expected_api=["_datum_get", "_datum_try_and_get"], vector_calls=1,
|
|
normalize_calls=1, expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case(
|
|
"prop_missing", DIRECTION_PROP, 0, prop_present=False,
|
|
prop_try_sequence=[None], expected_api=["_datum_get", "_datum_try_and_get"],
|
|
),
|
|
direct_case(
|
|
"target_burst", DIRECTION_TARGET, 1, fire_state=FIRE_BURSTING,
|
|
expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case(
|
|
"target_fire_aim", DIRECTION_TARGET, 1, aiming=1,
|
|
expected_direction=(0.0, 0.0, 1.0),
|
|
),
|
|
direct_case(
|
|
"target_prop", DIRECTION_TARGET, 1, target_prop="prop",
|
|
expected_api=["_datum_get", "_datum_get"], vector_calls=1,
|
|
normalize_calls=1, expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case("target_none", DIRECTION_TARGET, 0),
|
|
direct_case(
|
|
"point_success", DIRECTION_POINT, 1, vector_calls=0,
|
|
normalize_calls=1, expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case(
|
|
"point_zero", DIRECTION_POINT, 0, spec_point=(1.0, 2.0, 3.0),
|
|
vector_calls=0, normalize_calls=1, decoder_result_true=False,
|
|
expected_direction=(0.0, 0.0, 0.0),
|
|
),
|
|
direct_case(
|
|
"vector_success", DIRECTION_VECTOR, 1,
|
|
expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case(
|
|
"danger_success", DIRECTION_DANGER, 1, danger_type=1,
|
|
vector_calls=1, normalize_calls=1, expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case("danger_none", DIRECTION_DANGER, 0),
|
|
direct_case(
|
|
"object_unit", DIRECTION_OBJECT, 1, object_type=0,
|
|
expected_api=["_datum_get", "_object_try_and_get_and_verify_type", "_unit_get_head_position"],
|
|
vector_calls=1, normalize_calls=1, expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case(
|
|
"object_general", DIRECTION_OBJECT, 1, object_type=2,
|
|
expected_api=["_datum_get", "_object_try_and_get_and_verify_type", "_object_get_origin"],
|
|
vector_calls=1, normalize_calls=1, expected_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
direct_case(
|
|
"object_missing", DIRECTION_OBJECT, 0, object_present=False,
|
|
expected_api=["_datum_get", "_object_try_and_get_and_verify_type"],
|
|
),
|
|
direct_case(
|
|
"null_specification", None, None, null_spec=True,
|
|
expected_api=["_datum_get", "_display_assert", "_system_exit"],
|
|
expected_display=[["specification", 349, 1]], terminated=True,
|
|
),
|
|
direct_case(
|
|
"null_direction", DIRECTION_VECTOR, None, null_direction=True,
|
|
expected_api=["_datum_get", "_display_assert", "_system_exit"],
|
|
expected_display=[["direction", 350, 1]], terminated=True,
|
|
),
|
|
direct_case(
|
|
"invalid_type_7", 7, None,
|
|
expected_api=["_datum_get", "_display_assert", "_system_exit"],
|
|
expected_display=[[None, 449, 1]], terminated=True,
|
|
),
|
|
direct_case(
|
|
"vector_denormalized", DIRECTION_VECTOR, None, spec_vector=(2.0, 0.0, 0.0),
|
|
expected_api=["_datum_get"] + diagnostic_tail,
|
|
expected_direction=(2.0, 0.0, 0.0), decoder_result_true=True,
|
|
expected_display=[["temporary", 526, 0], ["temporary", 529, 1]], terminated=True,
|
|
),
|
|
direct_case(
|
|
"vector_lower_boundary_outside_0x3f7fdf39", DIRECTION_VECTOR, None,
|
|
spec_vector=(bits_f32(0x3F7FDF39), 0.0, 0.0),
|
|
expected_api=["_datum_get"] + diagnostic_tail,
|
|
expected_direction=(bits_f32(0x3F7FDF39), 0.0, 0.0), decoder_result_true=True,
|
|
expected_display=[["temporary", 526, 0], ["temporary", 529, 1]], terminated=True,
|
|
),
|
|
direct_case(
|
|
"vector_lower_boundary_inside_0x3f7fdf3a", DIRECTION_VECTOR, 1,
|
|
spec_vector=(bits_f32(0x3F7FDF3A), 0.0, 0.0),
|
|
expected_direction=(bits_f32(0x3F7FDF3A), 0.0, 0.0),
|
|
),
|
|
direct_case(
|
|
"vector_upper_boundary_inside_0x3f801061", DIRECTION_VECTOR, 1,
|
|
spec_vector=(bits_f32(0x3F801061), 0.0, 0.0),
|
|
expected_direction=(bits_f32(0x3F801061), 0.0, 0.0),
|
|
),
|
|
direct_case(
|
|
"vector_upper_boundary_outside_0x3f801062", DIRECTION_VECTOR, None,
|
|
spec_vector=(bits_f32(0x3F801062), 0.0, 0.0),
|
|
expected_api=["_datum_get"] + diagnostic_tail,
|
|
expected_direction=(bits_f32(0x3F801062), 0.0, 0.0), decoder_result_true=True,
|
|
expected_display=[["temporary", 526, 0], ["temporary", 529, 1]], terminated=True,
|
|
),
|
|
direct_case(
|
|
"movement_infinite_diagnostic", DIRECTION_MOVEMENT, None, moving=1,
|
|
movement_vector=(math.inf, 0.0, 0.0), normalize_calls=1,
|
|
expected_api=["_datum_get"] + diagnostic_tail,
|
|
expected_direction="nonfinite", decoder_result_true=True,
|
|
expected_display=[["temporary", 526, 0], ["temporary", 529, 1]], terminated=True,
|
|
),
|
|
direct_case(
|
|
"prop_infinite_invalid_second_lookup", DIRECTION_PROP, None,
|
|
head=(0.0, 0.0, 0.0), prop_head=(math.inf, 0.0, 0.0),
|
|
prop_try_sequence=["prop", None], vector_calls=1, normalize_calls=1,
|
|
expected_api=["_datum_get", "_datum_try_and_get", "_datum_try_and_get"] + diagnostic_tail,
|
|
expected_direction="nonfinite", decoder_result_true=True,
|
|
expected_display=[["temporary", 526, 0], ["temporary", 529, 1]], terminated=True,
|
|
),
|
|
]
|
|
affect = [
|
|
affect_case(
|
|
"affect_movement_no_path", DIRECTION_MOVEMENT, 4,
|
|
expected_api=["_datum_get", "_actor_path_has_path"],
|
|
expected_priority=0,
|
|
),
|
|
affect_case(
|
|
"affect_movement_has_path", DIRECTION_MOVEMENT, 4, path_result=True,
|
|
expected_api=["_datum_get", "_actor_path_has_path"],
|
|
expected_priority=4,
|
|
),
|
|
affect_case("affect_vector_priority_2", DIRECTION_VECTOR, 2),
|
|
affect_case(
|
|
"affect_vector_priority_3", DIRECTION_VECTOR, 3,
|
|
expected_api=["_datum_get", "_datum_get"], nested_decoder=True,
|
|
decoder_result_true=True, expected_flag=1,
|
|
expected_forced_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
affect_case(
|
|
"affect_vector_priority_4", DIRECTION_VECTOR, 4,
|
|
expected_api=["_datum_get", "_datum_get"], nested_decoder=True,
|
|
decoder_result_true=True, expected_flag=1,
|
|
expected_forced_direction=(0.6, 0.8, 0.0),
|
|
),
|
|
affect_case(
|
|
"affect_point_zero_priority_3", DIRECTION_POINT, 3,
|
|
expected_api=["_datum_get", "_datum_get"], nested_decoder=True,
|
|
vector_calls=0, normalize_calls=1, decoder_result_true=False,
|
|
expected_flag=0, expected_forced_direction=(0.0, 0.0, 0.0),
|
|
),
|
|
]
|
|
return direct + affect
|
|
|
|
|
|
class RoleImage:
|
|
def __init__(self, role: str, primary_path: Path, primary_hash: str):
|
|
self.role = role
|
|
self.primary = Owner(role + ":primary", primary_path, primary_hash)
|
|
self.target_alert = Owner(role + ":target-alert", TARGET_ALERT, PINNED_HASHES["target_alert"])
|
|
self.target_charge = Owner(role + ":target-charge", TARGET_CHARGE, PINNED_HASHES["target_charge"])
|
|
self.target_combat = Owner(role + ":target-combat", TARGET_COMBAT, PINNED_HASHES["target_combat"])
|
|
self.cachebeta = CACHEBETA.read_bytes()
|
|
if sha256_bytes(self.cachebeta) != PINNED_HASHES["cachebeta"]:
|
|
raise AssertionError((role, "cachebeta SHA-256 mismatch"))
|
|
self.constants = ConstantPool(
|
|
[self.primary, self.target_alert, self.target_charge, self.target_combat],
|
|
self.cachebeta,
|
|
)
|
|
if role == "target":
|
|
self.helper_owners = {
|
|
"_square_root": self.target_charge,
|
|
"_vector_from_points3d": self.target_alert,
|
|
"_scale_vector3d": self.target_charge,
|
|
"_magnitude_squared3d": self.target_alert,
|
|
"_magnitude3d": self.target_charge,
|
|
"_normalize3d": self.target_charge,
|
|
"_valid_real": self.target_combat,
|
|
"_valid_realcmp": self.target_combat,
|
|
"_valid_real_normal3d": self.target_combat,
|
|
}
|
|
else:
|
|
self.helper_owners = {name: self.primary for name in HELPERS}
|
|
|
|
self.runtime_raw: dict[str, bytes] = {}
|
|
self.section_raw: dict[str, bytes] = {}
|
|
self.section_checks: dict[str, dict] = {}
|
|
self.relocations: dict[str, list[dict]] = {}
|
|
self.function_owner: dict[str, str] = {}
|
|
decoder_owner_name = TARGET_DECODER if role == "target" else DECODER
|
|
self._materialize(DECODER, self.primary, decoder_owner_name)
|
|
self._materialize(AFFECT, self.primary, AFFECT)
|
|
for name in HELPERS:
|
|
self._materialize(name, self.helper_owners[name], name)
|
|
self._cross_guards()
|
|
|
|
def _destination(self, name: str, addend: int, owner: Owner) -> int:
|
|
if name == "_actor_data":
|
|
return ACTOR_DATA_SLOT + addend
|
|
if name == "_prop_data":
|
|
return PROP_DATA_SLOT + addend
|
|
if name == "_temporary":
|
|
return TEMPORARY + addend
|
|
if name in STUB_ADDRESSES:
|
|
return STUB_ADDRESSES[name] + addend
|
|
if name in CODE_ADDRESSES:
|
|
return CODE_ADDRESSES[name] + addend
|
|
if name == TARGET_DECODER:
|
|
return CODE_ADDRESSES[DECODER] + addend
|
|
return self.constants.address(name, addend, owner)
|
|
|
|
def _materialize(self, canonical_name: str, owner: Owner, owner_name: str) -> None:
|
|
if owner_name not in owner.functions:
|
|
raise AssertionError((self.role, owner.label, "missing function", owner_name))
|
|
function = owner.functions[owner_name]
|
|
symbol = cc.symbol(owner.obj, owner_name)
|
|
section = owner.obj["sections"][function.section_number - 1]
|
|
raw = bytearray(cc._section_bytes(owner.obj, section))
|
|
base = CODE_ADDRESSES[canonical_name]
|
|
relocations = []
|
|
for relocation in function.info["relocations"]:
|
|
target = relocation["target"]
|
|
if target[0] == "internal":
|
|
destination = base + target[1]
|
|
target_name = "$internal"
|
|
addend = target[1]
|
|
elif target[0] == "symbol":
|
|
target_name, addend = target[1], target[2]
|
|
destination = self._destination(target_name, addend, owner)
|
|
elif target[0] == "defined-noncode":
|
|
target_name, addend = target[2], target[3]
|
|
destination = self.constants.address(target_name, addend, owner)
|
|
else:
|
|
raise AssertionError((self.role, canonical_name, "unsupported relocation", target))
|
|
if relocation["type"] == cc.IMAGE_REL_I386_REL32:
|
|
patched = destination - (base + relocation["address"] + 4)
|
|
elif relocation["type"] == cc.IMAGE_REL_I386_DIR32:
|
|
patched = destination
|
|
else:
|
|
raise AssertionError((canonical_name, "unsupported relocation type", relocation))
|
|
raw[relocation["address"] : relocation["address"] + 4] = p32(patched)
|
|
relocations.append(
|
|
{
|
|
"address": relocation["address"],
|
|
"type": relocation["type"],
|
|
"target": target_name,
|
|
"addend": addend,
|
|
"destination": destination & 0xFFFFFFFF,
|
|
}
|
|
)
|
|
|
|
if canonical_name in (DECODER, AFFECT):
|
|
size, relocation_count, normalized_sha256, storage = FUNCTION_FACTS[self.role][canonical_name]
|
|
expected_selection = 1
|
|
else:
|
|
size, relocation_count, normalized_sha256 = HELPER_FACTS[canonical_name]
|
|
storage = 2
|
|
expected_selection = 1 if self.role == "target" else 2
|
|
checks = {
|
|
"size": function.info["size"] == size == len(raw),
|
|
"relocation_count": function.info["relocation_count"] == relocation_count,
|
|
"normalized_sha256": function.info["normalized_sha256"] == normalized_sha256,
|
|
"storage": symbol["storage"] == storage,
|
|
"function_type": symbol["type"] == 0x20,
|
|
"comdat_selection": _section_aux_selection(owner.obj, symbol["section"])
|
|
== expected_selection,
|
|
}
|
|
if not all(checks.values()):
|
|
raise AssertionError((self.role, canonical_name, "COFF guard", checks))
|
|
self.runtime_raw[canonical_name] = bytes(raw)
|
|
self.section_raw[canonical_name] = bytes(cc._section_bytes(owner.obj, section))
|
|
self.section_checks[canonical_name] = checks
|
|
self.relocations[canonical_name] = relocations
|
|
self.function_owner[canonical_name] = owner.label
|
|
|
|
def _cross_guards(self) -> None:
|
|
valid_calls = Counter(item["target"] for item in self.relocations[DECODER])[
|
|
"_valid_real_normal3d"
|
|
]
|
|
if valid_calls != (1 if self.role == "target" else 2):
|
|
raise AssertionError((self.role, "valid_real_normal3d relocation count", valid_calls))
|
|
affect_decoder_targets = [
|
|
item["target"]
|
|
for item in self.relocations[AFFECT]
|
|
if item["target"] in (TARGET_DECODER, DECODER)
|
|
]
|
|
expected = [TARGET_DECODER if self.role == "target" else DECODER]
|
|
if affect_decoder_targets != expected:
|
|
raise AssertionError((self.role, "public/private edge", affect_decoder_targets, expected))
|
|
|
|
def guards(self) -> dict:
|
|
return {
|
|
"sections": self.section_checks,
|
|
"owners": self.function_owner,
|
|
"relocations": self.relocations,
|
|
"constant_sections": self.constants.records,
|
|
"target_first_validity_boundary": (
|
|
"one out-of-line valid_real_normal3d relocation in target; two in candidate"
|
|
),
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
scenario: dict,
|
|
fpcw: int,
|
|
seed: int,
|
|
overrides: dict[str, bytes] | None = None,
|
|
validate: bool = True,
|
|
) -> dict:
|
|
machine = u.Uc(u.UC_ARCH_X86, u.UC_MODE_32)
|
|
for address in CODE_ADDRESSES.values():
|
|
machine.mem_map(address, 0x1000)
|
|
machine.mem_map(STUB_BASE, 0x10000)
|
|
machine.mem_map(RDATA, RDATA_SIZE)
|
|
machine.mem_map(GLOBALS, 0x1000)
|
|
for address in (ACTOR, SPEC, DIRECTION, PROP, OBJECT, TEMPORARY):
|
|
machine.mem_map(address, 0x1000)
|
|
machine.mem_map(STACK, STACK_SIZE)
|
|
selected_raw = dict(self.runtime_raw)
|
|
if overrides:
|
|
selected_raw.update(overrides)
|
|
for name, raw in selected_raw.items():
|
|
machine.mem_write(CODE_ADDRESSES[name], raw)
|
|
machine.mem_write(STUB_BASE, b"\xC3" * 0x10000)
|
|
for address, raw in self.constants.blocks.items():
|
|
machine.mem_write(address, raw)
|
|
machine.mem_write(ACTOR_DATA_SLOT, p32(ACTOR_DATA_VALUE))
|
|
machine.mem_write(PROP_DATA_SLOT, p32(PROP_DATA_VALUE))
|
|
|
|
actor_handle = (0xA5100000 | seed) & 0xFFFFFFFF
|
|
prop_handle = (0xB6200000 | seed) & 0xFFFFFFFF
|
|
object_handle = (0xC7300000 | seed) & 0xFFFFFFFF
|
|
if scenario.get("target_prop") == "prop":
|
|
target_prop_handle = prop_handle
|
|
else:
|
|
target_prop_handle = int(scenario.get("target_prop", NONE)) & 0xFFFFFFFF
|
|
|
|
actor_before = bytearray(seeded_bytes(ACTOR_SIZE, seed))
|
|
write_vector(actor_before, HEAD_POSITION, tuple(scenario.get("head", (1.0, 2.0, 3.0))))
|
|
write32(actor_before, TARGET_PROP_INDEX, target_prop_handle)
|
|
write16(actor_before, DANGER_TYPE, scenario.get("danger_type", 0))
|
|
write_vector(actor_before, DANGER_POSITION, tuple(scenario.get("danger_position", (4.0, 6.0, 3.0))))
|
|
write16(actor_before, PRIMARY_PRIORITY, scenario.get("priority", 0))
|
|
write16(actor_before, PRIMARY_DIRECTION, scenario["direction_type"] or 0)
|
|
primary_value = (
|
|
scenario.get("spec_point", (1.0, 2.0, 3.0))
|
|
if scenario["direction_type"] == DIRECTION_POINT
|
|
else scenario.get("spec_vector", (0.6, 0.8, 0.0))
|
|
)
|
|
write_vector(actor_before, PRIMARY_DIRECTION + 4, tuple(primary_value))
|
|
actor_before[MOVING] = scenario.get("moving", 0) & 0xFF
|
|
actor_before[FORCED_BY_AIMING] = 0x7D
|
|
write_vector(actor_before, MOVING_POINT, tuple(scenario.get("movement_point", (11.0, 22.0, 33.0))))
|
|
write_vector(actor_before, MOVING_VECTOR, tuple(scenario.get("movement_vector", (3.0, 4.0, 0.0))))
|
|
write_vector(actor_before, FORCED_DIRECTION, (9.0, 8.0, 7.0))
|
|
write16(actor_before, FIRE_STATE, scenario.get("fire_state", 0))
|
|
write16(actor_before, FIRE_TARGET_TYPE, 5)
|
|
actor_before[AIMING_AT_FIRE_TARGET] = scenario.get("aiming", 0) & 0xFF
|
|
write_vector(actor_before, FIRE_AIM_VECTOR, tuple(scenario.get("fire_vector", (0.0, 0.0, 1.0))))
|
|
write_vector(actor_before, BURST_AIM_VECTOR, tuple(scenario.get("burst_vector", (0.6, 0.8, 0.0))))
|
|
|
|
spec_before = bytearray(seeded_bytes(SMALL_SIZE, seed + 17))
|
|
write16(spec_before, 0, scenario["direction_type"] or 0)
|
|
spec_value = (
|
|
scenario.get("spec_point", (4.0, 6.0, 3.0))
|
|
if scenario["direction_type"] == DIRECTION_POINT
|
|
else scenario.get("spec_vector", (0.6, 0.8, 0.0))
|
|
)
|
|
if scenario["direction_type"] in (DIRECTION_PROP, DIRECTION_OBJECT):
|
|
write32(
|
|
spec_before,
|
|
4,
|
|
prop_handle if scenario["direction_type"] == DIRECTION_PROP else object_handle,
|
|
)
|
|
else:
|
|
write_vector(spec_before, 4, tuple(spec_value))
|
|
|
|
direction_before = seeded_bytes(SMALL_SIZE, seed + 31)
|
|
prop_before = bytearray(seeded_bytes(SMALL_SIZE, seed + 43))
|
|
write32(prop_before, PROP_OWNER, 0xD8400000 | seed)
|
|
write_vector(prop_before, PROP_CENTER, tuple(scenario.get("prop_center", (4.0, 6.0, 3.0))))
|
|
write_vector(prop_before, PROP_HEAD, tuple(scenario.get("prop_head", (4.0, 6.0, 3.0))))
|
|
object_before = bytearray(seeded_bytes(SMALL_SIZE, seed + 59))
|
|
write16(object_before, OBJECT_TYPE, scenario.get("object_type", 0))
|
|
temporary_before = seeded_bytes(TEMPORARY_SIZE, seed + 71)
|
|
globals_before = bytes(machine.mem_read(GLOBALS, 0x1000))
|
|
actor_page_before = bytearray(seeded_bytes(PAGE_SIZE, seed + 101))
|
|
actor_page_before[:ACTOR_SIZE] = actor_before
|
|
spec_page_before = bytearray(seeded_bytes(PAGE_SIZE, seed + 103))
|
|
spec_page_before[:SMALL_SIZE] = spec_before
|
|
direction_page_before = bytearray(seeded_bytes(PAGE_SIZE, seed + 107))
|
|
direction_page_before[:SMALL_SIZE] = direction_before
|
|
prop_page_before = bytearray(seeded_bytes(PAGE_SIZE, seed + 109))
|
|
prop_page_before[:SMALL_SIZE] = prop_before
|
|
object_page_before = bytearray(seeded_bytes(PAGE_SIZE, seed + 113))
|
|
object_page_before[:SMALL_SIZE] = object_before
|
|
machine.mem_write(ACTOR, bytes(actor_page_before))
|
|
machine.mem_write(SPEC, bytes(spec_page_before))
|
|
machine.mem_write(DIRECTION, bytes(direction_page_before))
|
|
machine.mem_write(PROP, bytes(prop_page_before))
|
|
machine.mem_write(OBJECT, bytes(object_page_before))
|
|
machine.mem_write(TEMPORARY, temporary_before)
|
|
|
|
stack_before = bytearray(seeded_bytes(STACK_SIZE, seed + 89))
|
|
write32(stack_before, ENTRY_SP - STACK, STOP)
|
|
write32(stack_before, ENTRY_SP - STACK + 4, actor_handle)
|
|
caller_before = bytes(
|
|
stack_before[ENTRY_SP - STACK : ENTRY_SP - STACK + CALLER_FRAME_SIZE]
|
|
)
|
|
machine.mem_write(STACK, bytes(stack_before))
|
|
|
|
for name, register in NONVOLATILE.items():
|
|
machine.reg_write(register, SAVED[name])
|
|
machine.reg_write(UC_X86_REG_EAX, 0x89ABCDEF)
|
|
machine.reg_write(UC_X86_REG_ECX, 0x76543210)
|
|
machine.reg_write(UC_X86_REG_EDX, 0x0BADF00D)
|
|
machine.reg_write(UC_X86_REG_ESP, ENTRY_SP)
|
|
machine.reg_write(UC_X86_REG_EFLAGS, 0x202 | (0x400 if seed & 1 else 0))
|
|
machine.reg_write(UC_X86_REG_FPCW, fpcw)
|
|
machine.reg_write(UC_X86_REG_FPSW, 0)
|
|
machine.reg_write(UC_X86_REG_FPTAG, 0xFFFF)
|
|
initial_direction_flag = bool(machine.reg_read(UC_X86_REG_EFLAGS) & 0x400)
|
|
initial_fptag = machine.reg_read(UC_X86_REG_FPTAG) & 0xFFFF
|
|
if scenario["entry"] == "decoder":
|
|
machine.reg_write(UC_X86_REG_EBX, 0 if scenario["null_spec"] else SPEC)
|
|
machine.reg_write(UC_X86_REG_EDI, 0 if scenario["null_direction"] else DIRECTION)
|
|
|
|
api_calls: list[dict] = []
|
|
math_calls: list[dict] = []
|
|
decoder_entries: list[dict] = []
|
|
writes: list[dict] = []
|
|
invalid_accesses: list[dict] = []
|
|
terminated = False
|
|
try_index = 0
|
|
|
|
def argument(index: int) -> int:
|
|
return r32(machine, machine.reg_read(UC_X86_REG_ESP) + 4 + 4 * index)
|
|
|
|
def pointer_label(address: int) -> str:
|
|
regions = (
|
|
(ACTOR, PAGE_SIZE, "actor"),
|
|
(SPEC, PAGE_SIZE, "specification"),
|
|
(DIRECTION, PAGE_SIZE, "direction"),
|
|
(PROP, PAGE_SIZE, "prop"),
|
|
(OBJECT, PAGE_SIZE, "object"),
|
|
(TEMPORARY, TEMPORARY_SIZE, "temporary"),
|
|
(STACK, STACK_SIZE, "stack"),
|
|
(GLOBALS, 0x1000, "globals"),
|
|
(RDATA, RDATA_SIZE, "rdata"),
|
|
)
|
|
if address == 0:
|
|
return "null"
|
|
for base, size, label in regions:
|
|
if base <= address < base + size:
|
|
offset = address - base
|
|
return label if offset == 0 else f"{label}+0x{offset:x}"
|
|
return f"0x{address:08x}"
|
|
|
|
def cstring(address: int, limit: int = 4096) -> str | None:
|
|
if address == 0:
|
|
return None
|
|
raw = bytes(machine.mem_read(address, limit))
|
|
end = raw.find(b"\0")
|
|
if end < 0:
|
|
raise AssertionError((self.role, scenario["name"], "unterminated cstring", hex(address)))
|
|
return raw[:end].decode("ascii")
|
|
|
|
def return_from_stub(value: int = 0) -> None:
|
|
esp = machine.reg_read(UC_X86_REG_ESP)
|
|
return_address = r32(machine, esp)
|
|
machine.reg_write(UC_X86_REG_EAX, value & 0xFFFFFFFF)
|
|
machine.reg_write(UC_X86_REG_ESP, esp + 4)
|
|
machine.reg_write(UC_X86_REG_EIP, return_address)
|
|
|
|
def decode_format(format_address: int) -> tuple[str, list[dict], str]:
|
|
pattern = cstring(format_address)
|
|
assert pattern is not None
|
|
offset_words = 2
|
|
typed: list[dict] = []
|
|
rendered_parts: list[str] = []
|
|
cursor = 0
|
|
for match in FORMAT_RE.finditer(pattern):
|
|
rendered_parts.append(pattern[cursor : match.start()])
|
|
kind = match.group("kind")
|
|
width = int(match.group("width") or 0)
|
|
precision = int(match.group("precision") or 6)
|
|
if kind == "f":
|
|
low = argument(offset_words)
|
|
high = argument(offset_words + 1)
|
|
value = struct.unpack("<d", p32(low) + p32(high))[0]
|
|
typed.append(
|
|
{
|
|
"kind": "double",
|
|
"words": [low, high],
|
|
"value": float_token(value),
|
|
}
|
|
)
|
|
rendered_parts.append(format(value, f".{precision}f"))
|
|
offset_words += 2
|
|
elif kind == "s":
|
|
pointer = argument(offset_words)
|
|
text = cstring(pointer)
|
|
typed.append(
|
|
{
|
|
"kind": "string",
|
|
"pointer": pointer_label(pointer),
|
|
"value": text,
|
|
}
|
|
)
|
|
rendered_parts.append("(null)" if text is None else text)
|
|
offset_words += 1
|
|
elif kind == "d":
|
|
raw_value = argument(offset_words)
|
|
value = s32(raw_value)
|
|
typed.append({"kind": "signed", "raw": raw_value, "value": value})
|
|
rendered_parts.append(str(value))
|
|
offset_words += 1
|
|
else:
|
|
raw_value = argument(offset_words)
|
|
typed.append({"kind": "unsigned", "raw": raw_value, "value": raw_value})
|
|
text = format(raw_value, "X" if kind == "X" else "x")
|
|
if width:
|
|
fill = "0" if "0" in match.group("flags") else " "
|
|
text = text.rjust(width, fill)
|
|
rendered_parts.append(text)
|
|
offset_words += 1
|
|
cursor = match.end()
|
|
rendered_parts.append(pattern[cursor:])
|
|
return pattern, typed, "".join(rendered_parts)
|
|
|
|
stub_by_address = {address: name for name, address in STUB_ADDRESSES.items()}
|
|
helper_by_address = {CODE_ADDRESSES[name]: name for name in HELPERS}
|
|
|
|
def on_code(_machine, address, _size, _user):
|
|
nonlocal try_index, terminated
|
|
if address == CODE_ADDRESSES[DECODER]:
|
|
decoder_entries.append(
|
|
{
|
|
"ebx_specification": pointer_label(machine.reg_read(UC_X86_REG_EBX)),
|
|
"edi_direction": pointer_label(machine.reg_read(UC_X86_REG_EDI)),
|
|
"actor_index": argument(0),
|
|
}
|
|
)
|
|
return
|
|
helper_name = helper_by_address.get(address)
|
|
if helper_name is not None:
|
|
count = {"_normalize3d": 1, "_valid_real_normal3d": 1, "_vector_from_points3d": 3}.get(helper_name, 0)
|
|
args = [pointer_label(argument(index)) for index in range(count)]
|
|
math_calls.append({"name": helper_name, "args": args})
|
|
return
|
|
name = stub_by_address.get(address)
|
|
if name is None:
|
|
return
|
|
if name == "_datum_get":
|
|
data, handle = argument(0), argument(1)
|
|
if data == ACTOR_DATA_VALUE and handle == actor_handle:
|
|
result, data_label = ACTOR, "actor_data"
|
|
elif data == PROP_DATA_VALUE and handle == prop_handle:
|
|
result, data_label = PROP, "prop_data"
|
|
else:
|
|
raise AssertionError((self.role, scenario["name"], name, hex(data), hex(handle)))
|
|
api_calls.append({"name": name, "data": data_label, "handle": handle})
|
|
return_from_stub(result)
|
|
elif name == "_datum_try_and_get":
|
|
data, handle = argument(0), argument(1)
|
|
if data != PROP_DATA_VALUE or handle != prop_handle:
|
|
raise AssertionError((self.role, scenario["name"], name, hex(data), hex(handle)))
|
|
sequence = scenario.get("prop_try_sequence", ["prop"])
|
|
item = sequence[try_index] if try_index < len(sequence) else sequence[-1]
|
|
try_index += 1
|
|
result = PROP if item == "prop" else 0
|
|
api_calls.append(
|
|
{"name": name, "data": "prop_data", "handle": handle, "result": pointer_label(result)}
|
|
)
|
|
return_from_stub(result)
|
|
elif name == "_object_try_and_get_and_verify_type":
|
|
handle, mask = argument(0), argument(1)
|
|
if handle != object_handle or mask != 0xFFFFFFFF:
|
|
raise AssertionError((self.role, scenario["name"], name, hex(handle), hex(mask)))
|
|
result = OBJECT if scenario.get("object_present", True) else 0
|
|
api_calls.append({"name": name, "handle": handle, "mask": mask, "result": pointer_label(result)})
|
|
return_from_stub(result)
|
|
elif name in ("_unit_get_head_position", "_object_get_origin"):
|
|
handle, output = argument(0), argument(1)
|
|
if handle != object_handle or not (STACK <= output <= STACK + STACK_SIZE - 12):
|
|
raise AssertionError((self.role, scenario["name"], name, hex(handle), hex(output)))
|
|
value = tuple(scenario.get("object_point", (4.0, 6.0, 3.0)))
|
|
machine.mem_write(output, b"".join(struct.pack("<f", f32(component)) for component in value))
|
|
api_calls.append({"name": name, "handle": handle, "output": pointer_label(output), "value": list(value)})
|
|
return_from_stub(0)
|
|
elif name == "_actor_path_has_path":
|
|
handle = argument(0)
|
|
if handle != actor_handle:
|
|
raise AssertionError((self.role, scenario["name"], name, hex(handle)))
|
|
value = 1 if scenario.get("path_result", False) else 0
|
|
api_calls.append({"name": name, "actor_index": handle, "result": value})
|
|
return_from_stub(value)
|
|
elif name in ("_sprintf", "_csprintf"):
|
|
destination, format_address = argument(0), argument(1)
|
|
if destination != TEMPORARY:
|
|
raise AssertionError((self.role, scenario["name"], name, pointer_label(destination)))
|
|
pattern, typed, rendered = decode_format(format_address)
|
|
encoded = rendered.encode("ascii") + b"\0"
|
|
if len(encoded) > TEMPORARY_SIZE:
|
|
raise AssertionError((scenario["name"], "modeled format overflow", len(encoded)))
|
|
machine.mem_write(destination, encoded)
|
|
api_calls.append(
|
|
{
|
|
"name": name,
|
|
"destination": "temporary",
|
|
"format": pattern,
|
|
"typed_arguments": typed,
|
|
"modeled_rendering": rendered,
|
|
}
|
|
)
|
|
return_from_stub(destination if name == "_csprintf" else len(rendered))
|
|
elif name == "_display_assert":
|
|
info, file_address, line, fatal = (argument(index) for index in range(4))
|
|
api_calls.append(
|
|
{
|
|
"name": name,
|
|
"info_pointer": pointer_label(info),
|
|
"info": cstring(info),
|
|
"file": cstring(file_address),
|
|
"line": line,
|
|
"fatal": fatal,
|
|
}
|
|
)
|
|
return_from_stub(0)
|
|
elif name == "_system_exit":
|
|
status = s32(argument(0))
|
|
api_calls.append({"name": name, "status": status})
|
|
terminated = True
|
|
machine.reg_write(UC_X86_REG_EIP, STOP)
|
|
machine.emu_stop()
|
|
else:
|
|
raise AssertionError(name)
|
|
|
|
def on_write(_machine, _access, address, size, value, _user):
|
|
for base, bound, label in (
|
|
(ACTOR, PAGE_SIZE, "actor"),
|
|
(SPEC, PAGE_SIZE, "specification"),
|
|
(DIRECTION, PAGE_SIZE, "direction"),
|
|
(PROP, PAGE_SIZE, "prop"),
|
|
(OBJECT, PAGE_SIZE, "object"),
|
|
(TEMPORARY, TEMPORARY_SIZE, "temporary"),
|
|
(GLOBALS, 0x1000, "globals"),
|
|
):
|
|
if base <= address < base + bound:
|
|
writes.append(
|
|
{
|
|
"region": label,
|
|
"offset": address - base,
|
|
"size": size,
|
|
"value": value & ((1 << min(size * 8, 64)) - 1),
|
|
}
|
|
)
|
|
return
|
|
|
|
def on_invalid(_machine, access, address, size, value, _user):
|
|
invalid_accesses.append(
|
|
{"access": access, "address": address, "size": size, "value": value}
|
|
)
|
|
return False
|
|
|
|
machine.hook_add(u.UC_HOOK_CODE, on_code)
|
|
machine.hook_add(u.UC_HOOK_MEM_WRITE, on_write)
|
|
machine.hook_add(u.UC_HOOK_MEM_INVALID, on_invalid)
|
|
entry = CODE_ADDRESSES[DECODER if scenario["entry"] == "decoder" else AFFECT]
|
|
try:
|
|
machine.emu_start(entry, STOP, count=100000)
|
|
except u.UcError as error:
|
|
raise AssertionError(
|
|
(
|
|
self.role,
|
|
scenario["name"],
|
|
"emulation error",
|
|
repr(error),
|
|
hex(machine.reg_read(UC_X86_REG_EIP)),
|
|
invalid_accesses,
|
|
)
|
|
) from error
|
|
|
|
buffers_before = {
|
|
"actor": bytes(actor_page_before),
|
|
"specification": bytes(spec_page_before),
|
|
"direction": bytes(direction_page_before),
|
|
"prop": bytes(prop_page_before),
|
|
"object": bytes(object_page_before),
|
|
"temporary": temporary_before,
|
|
"globals": globals_before,
|
|
}
|
|
buffers_after = {
|
|
"actor": bytes(machine.mem_read(ACTOR, PAGE_SIZE)),
|
|
"specification": bytes(machine.mem_read(SPEC, PAGE_SIZE)),
|
|
"direction": bytes(machine.mem_read(DIRECTION, PAGE_SIZE)),
|
|
"prop": bytes(machine.mem_read(PROP, PAGE_SIZE)),
|
|
"object": bytes(machine.mem_read(OBJECT, PAGE_SIZE)),
|
|
"temporary": bytes(machine.mem_read(TEMPORARY, TEMPORARY_SIZE)),
|
|
"globals": bytes(machine.mem_read(GLOBALS, 0x1000)),
|
|
}
|
|
changes = {
|
|
name: changed_offsets(buffers_before[name], buffers_after[name])
|
|
for name in buffers_before
|
|
}
|
|
nonvolatile = {
|
|
name: machine.reg_read(register) & 0xFFFFFFFF
|
|
for name, register in NONVOLATILE.items()
|
|
}
|
|
fpsw = machine.reg_read(UC_X86_REG_FPSW) & 0xFFFF
|
|
fptag = machine.reg_read(UC_X86_REG_FPTAG) & 0xFFFF
|
|
final_direction_flag = bool(machine.reg_read(UC_X86_REG_EFLAGS) & 0x400)
|
|
result = {
|
|
"role": self.role,
|
|
"case": scenario["name"],
|
|
"entry": scenario["entry"],
|
|
"fpcw_requested": fpcw,
|
|
"fpcw_final": machine.reg_read(UC_X86_REG_FPCW) & 0xFFFF,
|
|
"fpsw_final": fpsw,
|
|
"fptag_initial": initial_fptag,
|
|
"fptag_final": fptag,
|
|
"x87_top": (fpsw >> 11) & 7,
|
|
"direction_flag_initial": initial_direction_flag,
|
|
"direction_flag_final": final_direction_flag,
|
|
"terminated_by_system_exit": terminated,
|
|
"eip": machine.reg_read(UC_X86_REG_EIP) & 0xFFFFFFFF,
|
|
"esp": machine.reg_read(UC_X86_REG_ESP) & 0xFFFFFFFF,
|
|
"al": machine.reg_read(UC_X86_REG_EAX) & 0xFF,
|
|
"nonvolatile": nonvolatile,
|
|
"caller_frame_exact": bytes(machine.mem_read(ENTRY_SP, CALLER_FRAME_SIZE)) == caller_before,
|
|
"api_calls": api_calls,
|
|
"math_calls": math_calls,
|
|
"decoder_entries": decoder_entries,
|
|
"writes": writes,
|
|
"invalid_accesses": invalid_accesses,
|
|
"changes": changes,
|
|
"buffer_hashes": {
|
|
name: {"before": sha256_bytes(buffers_before[name]), "after": sha256_bytes(buffers_after[name])}
|
|
for name in buffers_before
|
|
},
|
|
"direction_before_bits": [
|
|
struct.unpack_from("<I", buffers_before["direction"], index * 4)[0]
|
|
for index in range(3)
|
|
],
|
|
"direction_after_bits": [r32(machine, DIRECTION + index * 4) for index in range(3)],
|
|
"primary_priority": struct.unpack_from("<h", buffers_after["actor"], PRIMARY_PRIORITY)[0],
|
|
"forced_by_aiming": buffers_after["actor"][FORCED_BY_AIMING],
|
|
"forced_direction_bits": [
|
|
struct.unpack_from("<I", buffers_after["actor"], FORCED_DIRECTION + index * 4)[0]
|
|
for index in range(3)
|
|
],
|
|
}
|
|
if validate:
|
|
self._validate_run(result, scenario, actor_handle, fpcw)
|
|
return result
|
|
|
|
def _validate_run(self, result: dict, scenario: dict, actor_handle: int, fpcw: int) -> None:
|
|
api_names = [call["name"] for call in result["api_calls"]]
|
|
math_names = [call["name"] for call in result["math_calls"]]
|
|
displays = [
|
|
[
|
|
"temporary" if call["info_pointer"].startswith("temporary") else call["info"],
|
|
call["line"],
|
|
call["fatal"],
|
|
]
|
|
for call in result["api_calls"]
|
|
if call["name"] == "_display_assert"
|
|
]
|
|
expected_valid = (1 if self.role == "target" else 2) if scenario["decoder_result_true"] else 0
|
|
expected_math = Counter(
|
|
{
|
|
"_vector_from_points3d": scenario["vector_calls"],
|
|
"_normalize3d": scenario["normalize_calls"],
|
|
"_valid_real_normal3d": expected_valid,
|
|
}
|
|
)
|
|
expected_math += Counter()
|
|
actual_math = Counter(math_names)
|
|
checks = {
|
|
"api_order": api_names == scenario["expected_api"],
|
|
"display_diagnostics": displays == scenario["expected_display"],
|
|
"termination": result["terminated_by_system_exit"] == scenario["terminated"],
|
|
"fpcw_preserved": result["fpcw_final"] == fpcw,
|
|
"x87_top_balanced": result["x87_top"] == 0,
|
|
"x87_tag_restored": result["fptag_final"] == result["fptag_initial"],
|
|
"direction_flag_preserved": result["direction_flag_final"]
|
|
== result["direction_flag_initial"],
|
|
"no_unmapped_access": not result["invalid_accesses"],
|
|
"math_helper_counts": actual_math == expected_math,
|
|
"only_relevant_helpers_execute": set(math_names) <= EXECUTED_HELPERS,
|
|
"globals_unchanged": not result["changes"]["globals"],
|
|
"specification_unchanged": not result["changes"]["specification"],
|
|
"prop_unchanged": not result["changes"]["prop"],
|
|
"object_unchanged": not result["changes"]["object"],
|
|
"caller_frame_exact": result["caller_frame_exact"],
|
|
"direction_page_tail_unchanged": set(result["changes"]["direction"])
|
|
<= set(range(12)),
|
|
"temporary_page_unchanged_without_format_call": (
|
|
True
|
|
if any(name in ("_sprintf", "_csprintf") for name in api_names)
|
|
else not result["changes"]["temporary"]
|
|
),
|
|
}
|
|
if scenario["terminated"]:
|
|
system_calls = [call for call in result["api_calls"] if call["name"] == "_system_exit"]
|
|
checks["fatal_system_exit_minus_one"] = system_calls == [{"name": "_system_exit", "status": -1}]
|
|
checks["fatal_eip_stopped"] = result["eip"] == STOP
|
|
else:
|
|
checks["normal_eip_stop"] = result["eip"] == STOP
|
|
checks["cdecl_return_esp"] = result["esp"] == ENTRY_SP + 4
|
|
if scenario["entry"] == "decoder":
|
|
expected_spec = "null" if scenario["null_spec"] else "specification"
|
|
expected_direction = "null" if scenario["null_direction"] else "direction"
|
|
checks["private_entry_abi"] = result["decoder_entries"] == [
|
|
{
|
|
"ebx_specification": expected_spec,
|
|
"edi_direction": expected_direction,
|
|
"actor_index": actor_handle,
|
|
}
|
|
]
|
|
checks["decoder_actor_read_only"] = not result["changes"]["actor"]
|
|
checks["decoder_nonvolatile"] = (
|
|
result["nonvolatile"]["ebp"] == SAVED["ebp"]
|
|
and result["nonvolatile"]["esi"] == SAVED["esi"]
|
|
if not scenario["terminated"]
|
|
else True
|
|
)
|
|
checks["decoder_write_envelope"] = {
|
|
item["region"] for item in result["writes"]
|
|
} <= {"direction", "temporary"}
|
|
if not scenario["terminated"]:
|
|
checks["decoder_private_registers_preserved"] = (
|
|
result["nonvolatile"]["ebx"] == (0 if scenario["null_spec"] else SPEC)
|
|
and result["nonvolatile"]["edi"] == (0 if scenario["null_direction"] else DIRECTION)
|
|
)
|
|
checks["decoder_al"] = result["al"] == scenario["expected_al"]
|
|
else:
|
|
expected_entries = 1 if scenario["nested_decoder"] else 0
|
|
checks["public_real_decoder_edge"] = len(result["decoder_entries"]) == expected_entries
|
|
if expected_entries:
|
|
checks["public_private_abi"] = result["decoder_entries"] == [
|
|
{
|
|
"ebx_specification": "actor+0x3ec",
|
|
"edi_direction": "actor+0x524",
|
|
"actor_index": actor_handle,
|
|
}
|
|
]
|
|
checks["public_nonvolatile_preserved"] = result["nonvolatile"] == SAVED
|
|
checks["public_external_direction_unchanged"] = not result["changes"]["direction"]
|
|
checks["public_write_envelope"] = set(result["changes"]["actor"]) <= (
|
|
{PRIMARY_PRIORITY, PRIMARY_PRIORITY + 1, FORCED_BY_AIMING}
|
|
| set(range(FORCED_DIRECTION, FORCED_DIRECTION + 12))
|
|
)
|
|
checks["public_priority"] = result["primary_priority"] == scenario["expected_priority"]
|
|
checks["public_forced_flag"] = result["forced_by_aiming"] == scenario["expected_flag"]
|
|
|
|
failed = [name for name, passed in checks.items() if not passed]
|
|
result["checks"] = checks
|
|
if failed:
|
|
raise AssertionError(
|
|
(
|
|
self.role,
|
|
scenario["name"],
|
|
hex(fpcw),
|
|
"failed runtime checks",
|
|
failed,
|
|
{"api": api_names, "math": math_names, "display": displays},
|
|
)
|
|
)
|
|
|
|
if scenario["entry"] == "decoder":
|
|
expected = scenario["expected_direction"]
|
|
actual_bits = result["direction_after_bits"]
|
|
if expected == "unchanged":
|
|
passed = actual_bits == result["direction_before_bits"]
|
|
elif expected == "nonfinite":
|
|
passed = any(not math.isfinite(bits_f32(value)) for value in actual_bits)
|
|
else:
|
|
passed = all(
|
|
math.isclose(bits_f32(bits), f32(component), rel_tol=2e-6, abs_tol=1e-7)
|
|
for bits, component in zip(actual_bits, expected)
|
|
)
|
|
result["checks"]["direction_result"] = passed
|
|
if not passed:
|
|
raise AssertionError((self.role, scenario["name"], "direction result", actual_bits, expected))
|
|
else:
|
|
expected = scenario["expected_forced_direction"]
|
|
actual_bits = result["forced_direction_bits"]
|
|
initial_bits = [f32_bits(value) for value in (9.0, 8.0, 7.0)]
|
|
if expected == "unchanged":
|
|
passed = actual_bits == initial_bits
|
|
else:
|
|
passed = all(
|
|
math.isclose(bits_f32(bits), f32(component), rel_tol=2e-6, abs_tol=1e-7)
|
|
for bits, component in zip(actual_bits, expected)
|
|
)
|
|
result["checks"]["forced_direction_result"] = passed
|
|
if not passed:
|
|
raise AssertionError((self.role, scenario["name"], "forced direction", actual_bits, expected))
|
|
|
|
|
|
def typed_without_machine_words(value):
|
|
if isinstance(value, list):
|
|
return [typed_without_machine_words(item) for item in value]
|
|
if isinstance(value, dict):
|
|
return {
|
|
key: typed_without_machine_words(item)
|
|
for key, item in value.items()
|
|
if key not in ("words", "modeled_rendering", "output", "pointer")
|
|
}
|
|
return value
|
|
|
|
|
|
def semantic_signature(run: dict) -> dict:
|
|
after_hashes = {
|
|
name: hashes["after"]
|
|
for name, hashes in run["buffer_hashes"].items()
|
|
}
|
|
result = {
|
|
"entry": run["entry"],
|
|
"fpcw": run["fpcw_final"],
|
|
"fptag": run["fptag_final"],
|
|
"x87_top": run["x87_top"],
|
|
"direction_flag": run["direction_flag_final"],
|
|
"terminated": run["terminated_by_system_exit"],
|
|
"api_calls": typed_without_machine_words(run["api_calls"]),
|
|
"decoder_entries": run["decoder_entries"],
|
|
"after_hashes": after_hashes,
|
|
"changes": run["changes"],
|
|
"direction_bits": run["direction_after_bits"],
|
|
"priority": run["primary_priority"],
|
|
"forced_flag": run["forced_by_aiming"],
|
|
"forced_direction_bits": run["forced_direction_bits"],
|
|
}
|
|
if not run["terminated_by_system_exit"] and run["entry"] == "decoder":
|
|
result["al"] = run["al"]
|
|
return result
|
|
|
|
|
|
def exact_candidate_signature(run: dict) -> dict:
|
|
return {
|
|
key: value
|
|
for key, value in run.items()
|
|
if key not in ("role", "checks")
|
|
}
|
|
|
|
|
|
def validate_diagnostic_details(run: dict, scenario: dict, seed: int) -> dict:
|
|
display_calls = [call for call in run["api_calls"] if call["name"] == "_display_assert"]
|
|
checks = {
|
|
"source_path_all_displays": all(
|
|
call["file"] == "c:\\halo\\SOURCE\\ai\\actor_looking.c"
|
|
for call in display_calls
|
|
),
|
|
"expected_line_fatal_pairs": [
|
|
[call["line"], call["fatal"]] for call in display_calls
|
|
]
|
|
== [[item[1], item[2]] for item in scenario["expected_display"]],
|
|
}
|
|
if [[item[1], item[2]] for item in scenario["expected_display"]] == [[526, 0], [529, 1]]:
|
|
sprintf_calls = [call for call in run["api_calls"] if call["name"] == "_sprintf"]
|
|
csprintf_calls = [call for call in run["api_calls"] if call["name"] == "_csprintf"]
|
|
checks.update(
|
|
{
|
|
"one_primary_diagnostic_format": len(sprintf_calls) == 1,
|
|
"one_final_assert_format": len(csprintf_calls) == 1,
|
|
"final_assert_format": bool(csprintf_calls)
|
|
and csprintf_calls[0]["format"]
|
|
== "%s: assert_valid_real_normal3d(%f, %f, %f)",
|
|
"final_assert_typed_arity": bool(csprintf_calls)
|
|
and [item["kind"] for item in csprintf_calls[0]["typed_arguments"]]
|
|
== ["string", "double", "double", "double"],
|
|
"diagnostic_sequence_526_then_529": [call["line"] for call in display_calls]
|
|
== [526, 529],
|
|
}
|
|
)
|
|
if scenario["name"] == "movement_infinite_diagnostic":
|
|
call = next(call for call in run["api_calls"] if call["name"] == "_sprintf")
|
|
typed = call["typed_arguments"]
|
|
checks.update(
|
|
{
|
|
"movement_format_selected": "%smoving" in call["format"],
|
|
"movement_typed_arity": len(typed) == 8,
|
|
"movement_status_empty": typed[1]["kind"] == "string" and typed[1]["value"] == "",
|
|
"movement_point_x_z_z": [typed[index]["value"] for index in (2, 3, 4)]
|
|
== [float_token(11.0), float_token(33.0), float_token(33.0)],
|
|
}
|
|
)
|
|
if scenario["name"] == "prop_infinite_invalid_second_lookup":
|
|
call = next(call for call in run["api_calls"] if call["name"] == "_sprintf")
|
|
typed = call["typed_arguments"]
|
|
actor_handle = (0xA5100000 | seed) & 0xFFFFFFFF
|
|
checks.update(
|
|
{
|
|
"prop_format_selected": "prop 0x%08X%s" in call["format"],
|
|
"prop_typed_arity": len(typed) == 5,
|
|
"prop_invalid_status": typed[2]["kind"] == "string"
|
|
and typed[2]["value"] == " (invalid)",
|
|
"prop_null_owner_zero": typed[3]["kind"] == "unsigned"
|
|
and typed[3]["raw"] == 0,
|
|
"prop_actor_index_forwarded": typed[4]["kind"] == "unsigned"
|
|
and typed[4]["raw"] == actor_handle,
|
|
}
|
|
)
|
|
failed = [name for name, passed in checks.items() if not passed]
|
|
if failed:
|
|
raise AssertionError((run["role"], scenario["name"], "diagnostic checks", failed))
|
|
return checks
|
|
|
|
|
|
def compare_roles(target: dict, candidate: dict, scenario: dict) -> dict:
|
|
target_signature = semantic_signature(target)
|
|
candidate_signature = semantic_signature(candidate)
|
|
if target_signature != candidate_signature:
|
|
differing = sorted(
|
|
key
|
|
for key in set(target_signature) | set(candidate_signature)
|
|
if target_signature.get(key) != candidate_signature.get(key)
|
|
)
|
|
raise AssertionError(
|
|
(scenario["name"], hex(target["fpcw_requested"]), "semantic differential", differing)
|
|
)
|
|
target_math = Counter(call["name"] for call in target["math_calls"])
|
|
candidate_math = Counter(call["name"] for call in candidate["math_calls"])
|
|
return {
|
|
"semantic_signature_equal": True,
|
|
"target_fpsw": target["fpsw_final"],
|
|
"candidate_fpsw": candidate["fpsw_final"],
|
|
"fpsw_equal": target["fpsw_final"] == candidate["fpsw_final"],
|
|
"target_math_calls": dict(target_math),
|
|
"candidate_math_calls": dict(candidate_math),
|
|
"expected_candidate_extra_validity_call": (
|
|
candidate_math["_valid_real_normal3d"]
|
|
- target_math["_valid_real_normal3d"]
|
|
== (1 if scenario["decoder_result_true"] else 0)
|
|
),
|
|
}
|
|
|
|
|
|
def scenario_by_name(name: str) -> dict:
|
|
return next(item for item in cases() if item["name"] == name)
|
|
|
|
|
|
def mutated(raw: bytes, offset: int, before: bytes, after: bytes) -> bytes:
|
|
if raw[offset : offset + len(before)] != before:
|
|
raise AssertionError(("negative-control preimage", hex(offset), raw[offset : offset + len(before)].hex(), before.hex()))
|
|
if len(before) != len(after):
|
|
raise AssertionError("negative-control mutation must preserve length")
|
|
result = bytearray(raw)
|
|
result[offset : offset + len(after)] = after
|
|
return bytes(result)
|
|
|
|
|
|
def run_negative_controls(actual: RoleImage) -> dict:
|
|
results = {}
|
|
|
|
threshold_case = scenario_by_name("affect_vector_priority_3")
|
|
threshold_raw = actual.runtime_raw[AFFECT]
|
|
threshold_pattern = bytes.fromhex("66 83 be e8 03 00 00 03")
|
|
threshold_offset = threshold_raw.find(threshold_pattern)
|
|
if threshold_offset < 0 or threshold_raw.find(threshold_pattern, threshold_offset + 1) >= 0:
|
|
raise AssertionError(("threshold mutation pattern cardinality", threshold_offset))
|
|
baseline = actual.run(threshold_case, 0x037F, 0xD1)
|
|
changed = mutated(
|
|
threshold_raw,
|
|
threshold_offset + 7,
|
|
b"\x03",
|
|
b"\x04",
|
|
)
|
|
mutant = actual.run(
|
|
threshold_case,
|
|
0x037F,
|
|
0xD1,
|
|
overrides={AFFECT: changed},
|
|
validate=False,
|
|
)
|
|
detected = (
|
|
baseline["forced_by_aiming"] == 1
|
|
and mutant["forced_by_aiming"] == 0
|
|
and len(baseline["decoder_entries"]) == 1
|
|
and not mutant["decoder_entries"]
|
|
and semantic_signature(baseline) != semantic_signature(mutant)
|
|
)
|
|
results["priority_threshold_3_to_4"] = {
|
|
"function": AFFECT,
|
|
"offset": threshold_offset + 7,
|
|
"before": "03",
|
|
"after": "04",
|
|
"case": threshold_case["name"],
|
|
"detected": detected,
|
|
"observation": {
|
|
"baseline_flag": baseline["forced_by_aiming"],
|
|
"mutant_flag": mutant["forced_by_aiming"],
|
|
"baseline_decoder_entries": len(baseline["decoder_entries"]),
|
|
"mutant_decoder_entries": len(mutant["decoder_entries"]),
|
|
},
|
|
}
|
|
|
|
movement_case = scenario_by_name("movement_infinite_diagnostic")
|
|
decoder_raw = actual.runtime_raw[DECODER]
|
|
movement_offset = 0x2F7
|
|
before_instruction = bytes.fromhex("d9 86 14 05 00 00")
|
|
after_instruction = bytes.fromhex("d9 86 10 05 00 00")
|
|
baseline = actual.run(movement_case, 0x037F, 0xD2)
|
|
changed = mutated(decoder_raw, movement_offset, before_instruction, after_instruction)
|
|
mutant = actual.run(
|
|
movement_case,
|
|
0x037F,
|
|
0xD2,
|
|
overrides={DECODER: changed},
|
|
validate=False,
|
|
)
|
|
baseline_typed = next(
|
|
call["typed_arguments"] for call in baseline["api_calls"] if call["name"] == "_sprintf"
|
|
)
|
|
mutant_typed = next(
|
|
call["typed_arguments"] for call in mutant["api_calls"] if call["name"] == "_sprintf"
|
|
)
|
|
baseline_point = [baseline_typed[index]["value"] for index in (2, 3, 4)]
|
|
mutant_point = [mutant_typed[index]["value"] for index in (2, 3, 4)]
|
|
detected = baseline_point == [
|
|
float_token(11.0),
|
|
float_token(33.0),
|
|
float_token(33.0),
|
|
] and mutant_point == [
|
|
float_token(11.0),
|
|
float_token(22.0),
|
|
float_token(33.0),
|
|
]
|
|
results["movement_second_z_to_y"] = {
|
|
"function": DECODER,
|
|
"offset": movement_offset,
|
|
"before": before_instruction.hex(),
|
|
"after": after_instruction.hex(),
|
|
"case": movement_case["name"],
|
|
"detected": detected,
|
|
"observation": {"baseline_point": baseline_point, "mutant_point": mutant_point},
|
|
}
|
|
|
|
prop_case = scenario_by_name("prop_infinite_invalid_second_lookup")
|
|
owner_offset = 0x34C
|
|
baseline = actual.run(prop_case, 0x037F, 0xD3)
|
|
changed = mutated(decoder_raw, owner_offset, bytes.fromhex("33 c0"), bytes.fromhex("40 90"))
|
|
mutant = actual.run(
|
|
prop_case,
|
|
0x037F,
|
|
0xD3,
|
|
overrides={DECODER: changed},
|
|
validate=False,
|
|
)
|
|
baseline_typed = next(
|
|
call["typed_arguments"] for call in baseline["api_calls"] if call["name"] == "_sprintf"
|
|
)
|
|
mutant_typed = next(
|
|
call["typed_arguments"] for call in mutant["api_calls"] if call["name"] == "_sprintf"
|
|
)
|
|
baseline_owner = baseline_typed[3]["raw"]
|
|
mutant_owner = mutant_typed[3]["raw"]
|
|
detected = baseline_owner == 0 and mutant_owner == 1
|
|
results["null_prop_owner_zero_to_one"] = {
|
|
"function": DECODER,
|
|
"offset": owner_offset,
|
|
"before": "33c0",
|
|
"after": "4090",
|
|
"case": prop_case["name"],
|
|
"detected": detected,
|
|
"observation": {"baseline_owner": baseline_owner, "mutant_owner": mutant_owner},
|
|
}
|
|
|
|
if not all(item["detected"] for item in results.values()):
|
|
raise AssertionError(("negative controls", results))
|
|
return {
|
|
"pass": True,
|
|
"mutation_scope": "in-memory copies of relocated actual-candidate bytes only",
|
|
"controls": results,
|
|
}
|
|
|
|
|
|
def inventory_counts(path: Path) -> dict:
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
values = payload.values() if isinstance(payload, dict) else payload
|
|
counts = Counter(item["status"] for item in values)
|
|
return {"entries": len(payload), "statuses": dict(sorted(counts.items()))}
|
|
|
|
|
|
def main() -> None:
|
|
if not __debug__:
|
|
raise RuntimeError("refusing optimized Python: assertions are part of the oracle")
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--output", type=Path, default=OUTPUT_DEFAULT)
|
|
args = parser.parse_args()
|
|
output = args.output.resolve()
|
|
if output.exists():
|
|
raise ValueError(f"refusing to overwrite output: {output}")
|
|
if not output.is_relative_to((ROOT / "scratch").resolve()):
|
|
raise ValueError(f"output must remain under scratch: {output}")
|
|
|
|
pinned = {
|
|
"target": verify_hash(TARGET, PINNED_HASHES["target"], "target"),
|
|
"first": verify_hash(FIRST, PINNED_HASHES["first"], "first"),
|
|
"actual": verify_hash(ACTUAL, PINNED_HASHES["actual"], "actual"),
|
|
"before_manifest": verify_hash(BEFORE_MANIFEST, PINNED_HASHES["before_manifest"], "before_manifest"),
|
|
"first_manifest": verify_hash(FIRST_MANIFEST, PINNED_HASHES["first_manifest"], "first_manifest"),
|
|
"before_stable": verify_hash(BEFORE_STABLE, PINNED_HASHES["before_stable"], "before_stable"),
|
|
"first_stable": verify_hash(FIRST_STABLE, PINNED_HASHES["first_stable"], "first_stable"),
|
|
"cachebeta": verify_hash(CACHEBETA, PINNED_HASHES["cachebeta"], "cachebeta"),
|
|
"target_alert": verify_hash(TARGET_ALERT, PINNED_HASHES["target_alert"], "target_alert"),
|
|
"target_charge": verify_hash(TARGET_CHARGE, PINNED_HASHES["target_charge"], "target_charge"),
|
|
"target_combat": verify_hash(TARGET_COMBAT, PINNED_HASHES["target_combat"], "target_combat"),
|
|
"coff_compare": verify_hash(ROOT / "tools/coff_compare.py", PINNED_HASHES["coff_compare"], "coff_compare"),
|
|
"coff_tools": verify_hash(ROOT / "tools/campaign/_coff_tools.py", PINNED_HASHES["coff_tools"], "coff_tools"),
|
|
"regression_gate": verify_hash(ROOT / "tools/regression_gate.py", PINNED_HASHES["regression_gate"], "regression_gate"),
|
|
}
|
|
before_manifest = json.loads(BEFORE_MANIFEST.read_text(encoding="utf-8"))
|
|
first_manifest = json.loads(FIRST_MANIFEST.read_text(encoding="utf-8"))
|
|
manifest_checks = {
|
|
"before_frozen_target_hash": before_manifest["frozen_files"][
|
|
"build/split/source/ai/actor_looking.obj"
|
|
]["sha256"]
|
|
== PINNED_HASHES["target"],
|
|
"first_phase": first_manifest["phase"] == "first",
|
|
"first_head": first_manifest["head"] == "d2c0babb171a51de60fe0cb9ebba4bedab1c6f61",
|
|
"first_capture_sha256": first_manifest["capture_sha256"]
|
|
== "e2e301ff34d075482fdb0a305c0988eed978583e9548ab73c3baec9745625554",
|
|
"first_ninja_deps_sha256": first_manifest["ninja_deps_sha256"]
|
|
== "f3e21bdbea84da8f8edfa534e4ccdbd61077fc28d1a870ee16aaa97e79c132d1",
|
|
}
|
|
if not all(manifest_checks.values()):
|
|
raise AssertionError(("manifest guards", manifest_checks))
|
|
|
|
target = RoleImage("target", TARGET, PINNED_HASHES["target"])
|
|
first = RoleImage("first", FIRST, PINNED_HASHES["first"])
|
|
actual = RoleImage("actual", ACTUAL, PINNED_HASHES["actual"])
|
|
if any(first.section_raw[name] != actual.section_raw[name] for name in (DECODER, AFFECT) + HELPERS):
|
|
raise AssertionError("first/actual function or helper raw bytes differ")
|
|
if any(
|
|
not (target.section_raw[name] == first.section_raw[name] == actual.section_raw[name])
|
|
for name in HELPERS
|
|
):
|
|
raise AssertionError("target/first/actual helper raw bytes differ")
|
|
|
|
executions = []
|
|
fpsw_differences = []
|
|
for case_index, scenario in enumerate(cases()):
|
|
for control_index, fpcw in enumerate(FPCW_VALUES):
|
|
seed = case_index * len(FPCW_VALUES) + control_index + 1
|
|
target_run = target.run(scenario, fpcw, seed)
|
|
first_run = first.run(scenario, fpcw, seed)
|
|
actual_run = actual.run(scenario, fpcw, seed)
|
|
diagnostic_checks = {
|
|
role: validate_diagnostic_details(run, scenario, seed)
|
|
for role, run in (
|
|
("target", target_run),
|
|
("first", first_run),
|
|
("actual", actual_run),
|
|
)
|
|
}
|
|
target_first = compare_roles(target_run, first_run, scenario)
|
|
target_actual = compare_roles(target_run, actual_run, scenario)
|
|
if exact_candidate_signature(first_run) != exact_candidate_signature(actual_run):
|
|
raise AssertionError((scenario["name"], hex(fpcw), "first/actual observation differs"))
|
|
if target_first != target_actual:
|
|
raise AssertionError((scenario["name"], hex(fpcw), "target comparison differs for actual"))
|
|
if not target_first["fpsw_equal"]:
|
|
fpsw_differences.append(
|
|
{
|
|
"case": scenario["name"],
|
|
"fpcw": fpcw,
|
|
"target": target_run["fpsw_final"],
|
|
"candidate": first_run["fpsw_final"],
|
|
}
|
|
)
|
|
executions.append(
|
|
{
|
|
"case": scenario,
|
|
"fpcw": fpcw,
|
|
"target": target_run,
|
|
"first": first_run,
|
|
"actual": actual_run,
|
|
"diagnostic_checks": diagnostic_checks,
|
|
"comparison": target_first,
|
|
}
|
|
)
|
|
|
|
negative_controls = run_negative_controls(actual)
|
|
decoder_cases = [item for item in cases() if item["entry"] == "decoder"]
|
|
affect_cases = [item for item in cases() if item["entry"] == "affect"]
|
|
helper_hashes = {
|
|
name: {
|
|
"size": len(target.section_raw[name]),
|
|
"sha256": sha256_bytes(target.section_raw[name]),
|
|
"executed_by_corpus": name in EXECUTED_HELPERS,
|
|
"target_owner": target.function_owner[name],
|
|
"candidate_owner": first.function_owner[name],
|
|
"target_selection": 1,
|
|
"candidate_selection": 2,
|
|
}
|
|
for name in HELPERS
|
|
}
|
|
report = {
|
|
"pass": True,
|
|
"scope": (
|
|
"frozen January target, first natural candidate, and frozen ordinary-Ninja actual; "
|
|
"real private decoder and real public movement caller"
|
|
),
|
|
"build_or_production_mutation": False,
|
|
"script": {"path": str(SCRIPT), "sha256": sha256_path(SCRIPT)},
|
|
"pinned_inputs": pinned,
|
|
"manifest_checks": manifest_checks,
|
|
"stable_inventory": {
|
|
"before": inventory_counts(BEFORE_STABLE),
|
|
"first": inventory_counts(FIRST_STABLE),
|
|
"expected_preserved_exact_count": 6261,
|
|
},
|
|
"coff_guards": {
|
|
"target": target.guards(),
|
|
"first": first.guards(),
|
|
"actual": actual.guards(),
|
|
"first_actual_all_function_and_helper_raw_exact": True,
|
|
"target_first_actual_all_nine_helper_raw_exact": True,
|
|
"helpers": helper_hashes,
|
|
"physical_target_helper_owner_note": (
|
|
"January physical COMDAT owners are action_alert/action_charge/actor_combat; "
|
|
"real_math.obj is not asserted as the selected owner"
|
|
),
|
|
},
|
|
"coverage": {
|
|
"direct_decoder_cases": len(decoder_cases),
|
|
"public_affect_cases": len(affect_cases),
|
|
"case_count": len(cases()),
|
|
"fpcw_values": list(FPCW_VALUES),
|
|
"executions_per_role": len(executions),
|
|
"total_real_function_executions": len(executions) * 3,
|
|
"all_seven_direction_types": sorted(
|
|
{item["direction_type"] for item in decoder_cases if isinstance(item["direction_type"], int) and item["direction_type"] <= 6}
|
|
),
|
|
"false_and_zero_paths": True,
|
|
"priorities_2_3_4_and_path_short_circuits": True,
|
|
"object_unit_general_missing": True,
|
|
"diagnostic_lines": [349, 350, 449, 526, 529],
|
|
"adjacent_float32_validity_brackets": {
|
|
"lower_outside": "0x3f7fdf39",
|
|
"lower_inside": "0x3f7fdf3a",
|
|
"upper_inside": "0x3f801061",
|
|
"upper_outside": "0x3f801062",
|
|
"tested_divergence": False,
|
|
},
|
|
"movement_diagnostic_x_z_z": True,
|
|
"invalid_prop_owner_zero": True,
|
|
"real_public_caller_never_stubs_decoder": True,
|
|
"real_machine_helpers_executed": sorted(EXECUTED_HELPERS),
|
|
"loaded_but_not_reached_helpers": sorted(set(HELPERS) - EXECUTED_HELPERS),
|
|
"modeled_genuine_external_apis": list(APIS),
|
|
"guard_layout": "separate 4 KiB data maps with unmapped 4 KiB gaps; stack centered in bounded 128 KiB map",
|
|
},
|
|
"comparison_summary": {
|
|
"all_tested_semantic_signatures_equal": True,
|
|
"first_actual_all_tested_observations_exact": True,
|
|
"expected_candidate_extra_validity_call_checked": all(
|
|
item["comparison"]["expected_candidate_extra_validity_call"] for item in executions
|
|
),
|
|
"fpsw_difference_count": len(fpsw_differences),
|
|
"fpsw_differences": fpsw_differences,
|
|
"x87_top_tag_control_and_direction_flag_preserved_all_runs": True,
|
|
},
|
|
"negative_controls": negative_controls,
|
|
"limitations": [
|
|
"Finite authored corpus; not exhaustive input-space or floating-point proof.",
|
|
"The target inlines the first normal-vector validity check while candidates call the real helper. Four adjacent-float32 points bracketing both 0.001 magnitude-squared limits showed no divergence, but the finite corpus does not exclude every boundary case.",
|
|
"sprintf/csprintf text rendering is a stable Python model; machine stack words, parsed types, formats, and downstream display_assert arguments are the evidence, not host CRT byte identity.",
|
|
"Engine data/object/path/head/origin/assert/exit APIs are typed behavioral models; only the three reached inline math helpers execute genuine COFF bytes.",
|
|
"Masked x87 control words 0x027f and 0x037f are covered; unmasked exception modes and every FPSW flag combination are not.",
|
|
"Data buffers are bounded seeded envelopes and do not claim full engine allocation validity or exhaustive pointer aliasing.",
|
|
],
|
|
"executions": executions,
|
|
"conclusion": (
|
|
"PASS for the bounded corpus: target, first, and actual agree on external API semantics, "
|
|
"diagnostics, final memory, return behavior, private/public ABI observations, and guarded access. "
|
|
"The result is runtime evidence only: the different first-check inline/call placement caused no tested precision divergence."
|
|
),
|
|
}
|
|
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
print(f"PASS: {output}")
|
|
print(f"sha256={sha256_path(output)}")
|
|
print(
|
|
f"cases={len(cases())} executions_per_role={len(executions)} "
|
|
f"negative_controls={negative_controls['pass']} fpsw_differences={len(fpsw_differences)}"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|