Files
Cesarus85--mjolnir-vr/tools/audit/render_debug_three_runtime.py
T

792 lines
36 KiB
Python

"""Target-first runtime differential for three public RenderDebug functions.
The real public point, line, and box-outline machine code is executed. Box
outline also executes the real same-object polygon-edges and public-line code.
Deferred calls execute the real same-object variadic cache helper; only genuine
outside APIs (assert/exit, time/error, and the rasterizer line sink) are modeled.
The accepted domain is deliberately finite: valid non-NULL inputs with exactly
representable finite coordinates/sizes, the documented cache bounds, and seven
NULL assertion exits. This does not claim exhaustive floating-point proof for
the natural point residual. Synthetic outcome controls mutate captured results.
One separate execution control changes a verified target instruction byte only
in Unicorn memory; no on-disk object is modified.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import json
from pathlib import Path
import struct
import sys
import unicorn as u
from unicorn.x86_const import *
ROOT = next(parent for parent in Path(__file__).resolve().parents if (parent / "tools/coff_compare.py").is_file())
sys.path.insert(0, str(ROOT))
from tools import coff_compare as cc
from tools.campaign._coff_tools import function_sections
from tools.regression_gate import _section_aux_selection
PATHS = {
"target": ROOT / "scratch/render-debug-three-before-20260906/build/split/source/render/render_debug.obj",
"first": ROOT / "scratch/render-debug-three-first-20260906.obj",
"actual": ROOT / "scratch/render-debug-three-actual-20260906.obj",
"source": ROOT / "scratch/render-debug-three-source-final-20260906.c",
"cache_reference": ROOT / "scratch/render_debug_cache_behavior_20260905.py",
}
PINS = {
"target": "d3bbaa9484914bd9cf4695ef1775198a60c7891f466aff4fcefe87caf293fdd0",
"first": "c17a3543cac88b90690090da553f4e6b3d252354f122cf9b73ead08a3581909b",
"actual": "62a44f55c5a75662b70c068d6d4390b0dd6abddc1d699bd82e47bd0ae5c5bf90",
"source": "8a1d5cd0765136789b8c04fa5850acfa20a54f47274c90c0f766fc68cd41a79b",
"cache_reference": "6705d663d86f5185fcf9b7e71ce9e0549d9716bd3c86399a97bc2cecd8d28964",
}
POINT = "_render_debug_point"
LINE = "_render_debug_line"
BOX = "_render_debug_box_outline"
POLYGON = "_render_debug_polygon_edges"
CACHE_ENTRY = "_render_debug_add_cache_entry"
CACHE_STRING = "_render_debug_add_cache_string"
FUNCTIONS = (POINT, LINE, BOX, POLYGON, CACHE_ENTRY, CACHE_STRING)
CODE = 0x100000
STUB = 0x200000
STOP = 0x20F000
LITERAL = 0x210000
CACHE = 0x300100
INPUT = 0x400100
STACK = 0x500000
SP = 0x508000
CACHE_SIZE = 0x740C
POOL_SIZE = 1024
ENTRY_SIZE = 0x38
ENTRY_LIMIT = 512
TIME_OFFSET = 0x7400
COUNT_OFFSET = 0x7404
POOL_OFFSET = 0x7408
CACHE_WARNING_OFFSET = 0x740A
STRING_WARNING_OFFSET = 0x740B
FPCW_MODES = (0x027F, 0x037F)
SAVED = {
UC_X86_REG_EBP: 0x11223344,
UC_X86_REG_EBX: 0x22334455,
UC_X86_REG_ESI: 0x33445566,
UC_X86_REG_EDI: 0x44556677,
}
APIS = (
"_display_assert",
"_system_exit",
"_rasterizer_debug_line",
"_game_time_get",
"_error",
"_csstrcmp",
"_csstrncpy",
"_csstrlen",
)
STUBS = {name: STUB + index * 0x100 for index, name in enumerate(APIS)}
FUNCTION_ADDRESSES = {name: CODE + index * 0x1000 for index, name in enumerate(FUNCTIONS)}
SOURCE_TEXT = b"c:\\halo\\SOURCE\\render\\render_debug.c\0"
LITERALS = {
"??_C@_0CF@JKFPHJJE@c?3?2halo?2SOURCE?2render?2render_deb@": SOURCE_TEXT,
"??_C@_05BBEJFBFA@point?$AA@": b"point\0",
"??_C@_06LEDMCOFG@point0?$AA@": b"point0\0",
"??_C@_06KNCHBPBH@point1?$AA@": b"point1\0",
"??_C@_06GPGIDCJA@points?$AA@": b"points\0",
"??_C@_05PEENBMOG@color?$AA@": b"color\0",
"??_C@_06CGBPGGIE@bounds?$AA@": b"bounds\0",
"??_C@_0BN@JJPKCCPJ@render?5debug?5cache?5overflow?4?$AA@": b"render debug cache overflow.\0",
"??_C@_0CD@CLODFBGE@render?5debug?5cache?5string?5overfl@": b"render debug cache string overflow\0",
"__real@3f000000": struct.pack("<f", 0.5),
}
LITERAL_ADDRESSES = {
name: LITERAL + index * 0x400 for index, name in enumerate(sorted(LITERALS))
}
def digest(raw: bytes) -> str:
return hashlib.sha256(raw).hexdigest()
def p32(value: int) -> bytes:
return struct.pack("<I", value & 0xFFFFFFFF)
def p16(value: int) -> bytes:
return struct.pack("<H", value & 0xFFFF)
def f32(value: float) -> bytes:
return struct.pack("<f", value)
def f32_value(value: float) -> float:
return struct.unpack("<f", f32(value))[0]
def point_bytes(values) -> bytes:
return struct.pack("<3f", *values)
def color_bytes(values) -> bytes:
return struct.pack("<4f", *values)
def s16(raw: bytes | bytearray, offset: int) -> int:
return struct.unpack_from("<h", raw, offset)[0]
def contains(base: int, length: int, address: int, size: int) -> bool:
return base <= address and address + size <= base + length
def cases() -> list[dict]:
return [
{"name": "line_immediate", "function": LINE, "immediate": True},
{"name": "line_immediate_aliased_endpoints", "function": LINE, "immediate": True, "alias": True},
{"name": "line_deferred_same_tick", "function": LINE, "immediate": False, "count": 3},
{"name": "line_deferred_new_tick", "function": LINE, "immediate": False, "cache_time": 9,
"game_time": 10, "second_time": 11, "count": 12, "pool": b"old\0"},
{"name": "point_immediate_positive", "function": POINT, "immediate": True, "size": 4.0},
{"name": "point_immediate_zero", "function": POINT, "immediate": True, "size": 0.0},
{"name": "point_immediate_negative", "function": POINT, "immediate": True, "size": -2.0},
{"name": "point_deferred_same_tick", "function": POINT, "immediate": False,
"size": 2.25, "count": 7},
{"name": "point_deferred_new_tick", "function": POINT, "immediate": False,
"size": -2.0, "cache_time": 9, "game_time": 10, "second_time": 11,
"count": 511, "pool": b"stale\0"},
{"name": "box_immediate_ordinary", "function": BOX, "immediate": True},
{"name": "box_immediate_degenerate", "function": BOX, "immediate": True,
"bounds": (2.0, 2.0, -1.0, -1.0, 0.0, 0.0)},
{"name": "box_immediate_inverted", "function": BOX, "immediate": True,
"bounds": (4.0, -3.0, 6.0, -5.0, 8.0, -7.0)},
{"name": "box_deferred_same_tick", "function": BOX, "immediate": False, "count": 9},
{"name": "box_deferred_last_slot", "function": BOX, "immediate": False, "count": 511},
{"name": "line_deferred_full_cache_warns", "function": LINE, "immediate": False,
"count": 512, "cache_warning": 0},
{"name": "assert_point_null", "function": POINT, "immediate": True, "null": "point"},
{"name": "assert_point_color_null", "function": POINT, "immediate": True, "null": "color"},
{"name": "assert_line_point0_null", "function": LINE, "immediate": True, "null": "point0"},
{"name": "assert_line_point1_null", "function": LINE, "immediate": True, "null": "point1"},
{"name": "assert_line_color_null", "function": LINE, "immediate": True, "null": "color"},
{"name": "assert_box_bounds_null", "function": BOX, "immediate": True, "null": "bounds"},
{"name": "assert_box_color_null", "function": BOX, "immediate": True, "null": "color"},
]
def input_image(case: dict) -> tuple[bytes, dict[str, bytes], dict[str, int]]:
raw = bytearray((index * 23 + 11) & 0xFF for index in range(0x10000))
bounds = case.get("bounds", (-3.0, 4.0, -5.0, 6.0, -7.0, 8.0))
blocks = {
"point": point_bytes((1.25, -2.5, 3.75)),
"point0": point_bytes((1.25, -2.5, 3.75)),
"point1": point_bytes((-4.5, 5.25, -6.0)),
"color": color_bytes((0.75, 0.125, 0.5, 1.0)),
"bounds": struct.pack("<6f", *bounds),
}
locations = {name: INPUT + index * 0x100 for index, name in enumerate(blocks)}
if case.get("alias"):
locations["point1"] = locations["point0"]
blocks["point1"] = blocks["point0"]
for name, data in blocks.items():
at = locations[name] - INPUT
raw[at:at + len(data)] = data
return bytes(raw), blocks, locations
def public_arguments(case: dict, locations: dict[str, int]) -> bytes:
null = case.get("null")
address = lambda name: 0 if null == name else locations[name]
immediate = p32(int(case["immediate"]))
if case["function"] == POINT:
return immediate + p32(address("point")) + f32(case.get("size", 4.0)) + p32(address("color"))
if case["function"] == LINE:
return immediate + p32(address("point0")) + p32(address("point1")) + p32(address("color"))
assert case["function"] == BOX
return immediate + p32(address("bounds")) + p32(address("color"))
def initial_cache(case: dict) -> bytearray:
raw = bytearray((index * 37 + 19) & 0xFF for index in range(CACHE_SIZE))
pool = case.get("pool", b"")
assert len(pool) <= 1023
raw[:len(pool)] = pool
raw[len(pool)] = 0
raw[TIME_OFFSET:TIME_OFFSET + 2] = p16(case.get("cache_time", 10))
raw[COUNT_OFFSET:COUNT_OFFSET + 2] = p16(case.get("count", 0))
raw[POOL_OFFSET:POOL_OFFSET + 2] = p16(len(pool))
raw[CACHE_WARNING_OFFSET] = case.get("cache_warning", 0)
raw[STRING_WARNING_OFFSET] = case.get("string_warning", 0)
return raw
def cache_model(before: bytearray, case: dict, blocks: dict[str, bytes]) -> tuple[bytearray, list[dict], int | None]:
raw = bytearray(before)
events = [{"api": "game_time_get", "result": case.get("game_time", 10) & 0xFFFFFFFF}]
now = case.get("game_time", 10)
if s16(raw, TIME_OFFSET) != struct.unpack("<h", p16(now))[0]:
second = case.get("second_time", now)
events.append({"api": "game_time_get", "result": second & 0xFFFFFFFF})
raw[TIME_OFFSET:TIME_OFFSET + 2] = p16(second)
raw[POOL_OFFSET:POOL_OFFSET + 2] = p16(0)
raw[0] = 0
index = 0
else:
index = s16(raw, COUNT_OFFSET)
if index >= ENTRY_LIMIT:
if not raw[CACHE_WARNING_OFFSET]:
events.append({"api": "error", "priority": 2,
"message": "render debug cache overflow."})
raw[CACHE_WARNING_OFFSET] = 1
return raw, events, None
raw[COUNT_OFFSET:COUNT_OFFSET + 2] = p16(index + 1)
start = POOL_SIZE + index * ENTRY_SIZE
if case["function"] == POINT:
raw[start:start + 2] = p16(1)
raw[start + 4:start + 0x10] = blocks["point"]
raw[start + 0x10:start + 0x14] = f32(case.get("size", 4.0))
raw[start + 0x14:start + 0x24] = blocks["color"]
elif case["function"] == LINE:
raw[start:start + 2] = p16(2)
raw[start + 4:start + 0x10] = blocks["point0"]
raw[start + 0x10:start + 0x1C] = blocks["point1"]
raw[start + 0x1C:start + 0x2C] = blocks["color"]
else:
raw[start:start + 2] = p16(7)
raw[start + 4:start + 0x1C] = blocks["bounds"]
raw[start + 0x1C:start + 0x2C] = blocks["color"]
return raw, events, index
def point_axis_pairs(point: bytes, size: float) -> list[tuple[bytes, bytes]]:
x, y, z = struct.unpack("<3f", point)
half = f32_value(size) * 0.5
values = [
((x - half, y, z), (x + half, y, z)),
((x, y - half, z), (x, y + half, z)),
((x, y, z - half), (x, y, z + half)),
]
return [(point_bytes(a), point_bytes(b)) for a, b in values]
def box_points(bounds: bytes) -> list[bytes]:
x0, x1, y0, y1, z0, z1 = struct.unpack("<6f", bounds)
return [point_bytes(point) for point in (
(x0, y0, z0), (x1, y0, z0), (x1, y1, z0), (x0, y1, z0),
(x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1),
)]
def raster_event(point0: bytes, point1: bytes, color: bytes) -> dict:
return {"api": "rasterizer_debug_line", "point0": point0.hex(),
"point1": point1.hex(), "color": color.hex()}
def expected_success(case: dict, before: bytearray, blocks: dict[str, bytes]) -> tuple[bytearray, list[dict], list[str], int | None]:
if not case["immediate"]:
cache, events, index = cache_model(before, case, blocks)
return cache, events, [case["function"], CACHE_ENTRY], index
if case["function"] == LINE:
return before, [raster_event(blocks["point0"], blocks["point1"], blocks["color"])], [LINE], None
if case["function"] == POINT:
events = [raster_event(a, b, blocks["color"])
for a, b in point_axis_pairs(blocks["point"], case.get("size", 4.0))]
return before, events, [POINT], None
points = box_points(blocks["bounds"])
edges = [(3, 0), (0, 1), (1, 2), (2, 3),
(7, 4), (4, 5), (5, 6), (6, 7),
(0, 4), (1, 5), (2, 6), (3, 7)]
events = [raster_event(points[a], points[b], blocks["color"]) for a, b in edges]
trace = [BOX, POLYGON] + [LINE] * 4 + [POLYGON] + [LINE] * 8
return before, events, trace, None
ASSERTIONS = {
(POINT, "point"): ("point", 327),
(POINT, "color"): ("color", 328),
(LINE, "point0"): ("point0", 363),
(LINE, "point1"): ("point1", 364),
(LINE, "color"): ("color", 365),
(BOX, "bounds"): ("bounds", 812),
(BOX, "color"): ("color", 813),
}
class Loader:
def __init__(self, label: str, path: Path, code_patches: dict[str, list[tuple[int, int, int]]] | None = None):
self.label = label
self.path = path
self.obj = cc.load(path)
self.sections = function_sections(self.obj)
self.code_patches = code_patches or {}
self.uc = u.Uc(u.UC_ARCH_X86, u.UC_MODE_32)
for base, size in ((CODE, 0x10000), (STUB, 0x10000), (LITERAL, 0x10000),
(CACHE - 0x100, 0x8000), (INPUT - 0x100, 0x11000),
(STACK, 0x10000)):
self.uc.mem_map(base, size)
self.loaded: set[str] = set()
self.extents: dict[int, int] = {}
for address in STUBS.values():
self.uc.mem_write(address, b"\xC3")
self.uc.mem_write(STOP, b"\xCC")
for name, payload in LITERALS.items():
self.uc.mem_write(LITERAL_ADDRESSES[name], payload)
for name in (POINT, LINE, BOX):
self.function(name)
self.uc.mem_protect(CODE, 0x10000, u.UC_PROT_READ | u.UC_PROT_EXEC)
self.uc.mem_protect(STUB, 0x10000, u.UC_PROT_READ | u.UC_PROT_EXEC)
self.uc.mem_protect(LITERAL, 0x10000, u.UC_PROT_READ)
self.uc.mem_protect(CACHE - 0x100, 0x8000, u.UC_PROT_READ | u.UC_PROT_WRITE)
self.uc.mem_protect(INPUT - 0x100, 0x11000, u.UC_PROT_READ)
self.uc.mem_protect(STACK, 0x10000, u.UC_PROT_READ | u.UC_PROT_WRITE)
def function(self, name: str) -> int:
assert name in FUNCTION_ADDRESSES, (self.label, "unexpected internal function", name)
address = FUNCTION_ADDRESSES[name]
if name in self.loaded:
return address
self.loaded.add(name)
section = self.sections[name]
raw = bytearray(cc._section_bytes(self.obj, self.obj["sections"][section.section_number - 1]))
assert len(raw) <= 0x1000
for offset, expected, replacement in self.code_patches.get(name, []):
assert raw[offset] == expected, (self.label, name, offset, raw[offset], expected)
raw[offset] = replacement
self.extents[address] = len(raw)
for relocation in section.info["relocations"]:
target = relocation.get("symbolic_target", relocation["target"])
if target[0] == "internal":
destination = address + target[1]
else:
assert target[0] in ("symbol", "defined-noncode"), target
symbol, addend = target[1:] if target[0] == "symbol" else target[2:]
if symbol == "_render_debug_globals":
destination = CACHE
elif symbol in FUNCTION_ADDRESSES:
destination = self.function(symbol)
elif symbol in STUBS:
destination = STUBS[symbol]
else:
destination = self.constant(symbol)
destination += addend
if relocation["type"] == cc.IMAGE_REL_I386_REL32:
destination -= address + relocation["address"] + 4
else:
assert relocation["type"] == cc.IMAGE_REL_I386_DIR32, relocation
raw[relocation["address"]:relocation["address"] + 4] = p32(destination)
self.uc.mem_write(address, bytes(raw))
return address
def constant(self, name: str) -> int:
assert name in LITERALS, (self.label, "unexpected constant owner", name)
expected = LITERALS[name]
try:
symbol = cc.symbol(self.obj, name)
except cc.CoffError:
symbol = None
if symbol is not None:
section = self.obj["sections"][symbol["section"] - 1]
raw = bytes(cc._section_bytes(self.obj, section))[symbol["value"]:]
assert raw == expected, (self.label, name, raw.hex(), expected.hex())
return LITERAL_ADDRESSES[name]
def cstring(self, address: int) -> str:
if contains(LITERAL, 0x10000, address, 1):
limit = LITERAL + 0x10000
else:
raise AssertionError((self.label, "invalid cstring", hex(address)))
raw = bytes(self.uc.mem_read(address, limit - address))
assert b"\0" in raw
return raw.split(b"\0", 1)[0].decode("ascii")
@staticmethod
def pointer_region(address: int) -> list:
for label, base, size in (("input", INPUT, 0x10000), ("stack", STACK, 0x10000),
("cache", CACHE, CACHE_SIZE), ("literal", LITERAL, 0x10000)):
if contains(base, size, address, 1):
return [label, address - base]
return ["unknown", address]
def run(self, case: dict, fpcw: int) -> dict:
uc = self.uc
input_raw, blocks, locations = input_image(case)
cache_before = initial_cache(case)
expected_cache = bytearray(cache_before)
expected_events: list[dict]
expected_trace: list[str]
expected_index = None
assertion = case.get("null")
if assertion:
information, line = ASSERTIONS[(case["function"], assertion)]
expected_events = [
{"api": "display_assert", "information": information,
"file": SOURCE_TEXT[:-1].decode("ascii"), "line": line, "fatal": 1},
{"api": "system_exit", "code": 0xFFFFFFFF},
]
expected_trace = [case["function"]]
else:
expected_cache, expected_events, expected_trace, expected_index = expected_success(
case, cache_before, blocks)
uc.mem_write(CACHE - 64, b"\xA5" * 64 + bytes(cache_before) + b"\xA5" * 64)
uc.mem_write(INPUT - 64, b"\xA5" * 64 + input_raw + b"\xA5" * 64)
payload = public_arguments(case, locations)
stack = bytearray(b"\x5A" * 0x10000)
stack_at = SP - STACK
stack[stack_at:stack_at + 4 + len(payload)] = p32(STOP) + payload
uc.mem_write(STACK, bytes(stack))
state = {"events": [], "trace": [], "snapshots": [], "time_calls": 0,
"exited": False, "forbidden_access": None}
def args(count: int) -> tuple[int, ...]:
esp = uc.reg_read(UC_X86_REG_ESP)
return struct.unpack("<" + "I" * count, uc.mem_read(esp + 4, count * 4))
def code_hook(emulator, address, size, unused):
assert (address == STOP or address in STUBS.values()
or any(contains(base, length, address, size)
for base, length in self.extents.items())), (
self.label, "unexpected executable address", hex(address), size)
if address in FUNCTION_ADDRESSES.values():
state["trace"].append(next(name for name, value in FUNCTION_ADDRESSES.items()
if value == address))
name = next((name for name, value in STUBS.items() if value == address), None)
if name is None:
return
state["snapshots"].append({"api": name,
"cache_sha256": digest(bytes(uc.mem_read(CACHE, CACHE_SIZE)))})
result = 0x76543210
if name == "_rasterizer_debug_line":
p0, p1, color = args(3)
event = raster_event(bytes(uc.mem_read(p0, 12)), bytes(uc.mem_read(p1, 12)),
bytes(uc.mem_read(color, 16)))
event.update({"point0_pointer": self.pointer_region(p0),
"point1_pointer": self.pointer_region(p1),
"color_pointer": self.pointer_region(color)})
elif name == "_game_time_get":
key = "game_time" if state["time_calls"] == 0 else "second_time"
result = case.get(key, case.get("game_time", 10)) & 0xFFFFFFFF
state["time_calls"] += 1
event = {"api": "game_time_get", "result": result}
elif name == "_error":
priority, message = args(2)
event = {"api": "error", "priority": priority,
"message": self.cstring(message)}
elif name == "_display_assert":
information, file_name, line, fatal = args(4)
event = {"api": "display_assert", "information": self.cstring(information),
"file": self.cstring(file_name), "line": line, "fatal": fatal}
elif name == "_system_exit":
(code,) = args(1)
event = {"api": "system_exit", "code": code}
state["exited"] = True
state["events"].append(event)
emulator.emu_stop()
return
else:
raise AssertionError((self.label, "unexpected cache-string API", name))
state["events"].append(event)
uc.reg_write(UC_X86_REG_EAX, result & 0xFFFFFFFF)
uc.reg_write(UC_X86_REG_ECX, 0xA1B2C3D4)
uc.reg_write(UC_X86_REG_EDX, 0xB2C3D4E5)
def memory_hook(emulator, access, address, size, value, unused):
if access == u.UC_MEM_WRITE:
allowed = (contains(CACHE, CACHE_SIZE, address, size)
or contains(STACK + 64, SP - STACK - 64, address, size))
else:
allowed = (contains(CACHE, CACHE_SIZE, address, size)
or contains(INPUT, 0x10000, address, size)
or contains(STACK + 64, 0x10000 - 128, address, size)
or contains(LITERAL, 0x10000, address, size)
or contains(CODE, 0x10000, address, size))
if not allowed:
state["forbidden_access"] = [access, address, size, value]
raise AssertionError((self.label, "forbidden memory access", access,
hex(address), size, value))
code_handle = uc.hook_add(u.UC_HOOK_CODE, code_hook)
memory_handle = uc.hook_add(u.UC_HOOK_MEM_READ | u.UC_HOOK_MEM_WRITE, memory_hook)
initial_tag = None
try:
uc.reg_write(UC_X86_REG_ESP, SP)
uc.reg_write(UC_X86_REG_EFLAGS, 0x202)
uc.reg_write(UC_X86_REG_FPCW, fpcw)
uc.reg_write(UC_X86_REG_FPTAG, 0xFFFF)
uc.reg_write(UC_X86_REG_FPSW, 0)
initial_tag = uc.reg_read(UC_X86_REG_FPTAG)
for register, value in SAVED.items():
uc.reg_write(register, value)
uc.emu_start(FUNCTION_ADDRESSES[case["function"]], STOP, count=500000)
finally:
uc.hook_del(code_handle)
uc.hook_del(memory_handle)
returned = not assertion
esp_restored = (uc.reg_read(UC_X86_REG_ESP) == SP + 4) if returned else None
nonvolatiles_restored = (
all(uc.reg_read(register) == value for register, value in SAVED.items())
if returned else None)
fpcw_final = uc.reg_read(UC_X86_REG_FPCW)
fptag_final = uc.reg_read(UC_X86_REG_FPTAG)
fpsw_final = uc.reg_read(UC_X86_REG_FPSW)
assert state["exited"] == bool(assertion)
if returned:
assert uc.reg_read(UC_X86_REG_EIP) == STOP
assert esp_restored and nonvolatiles_restored
assert fpcw_final == fpcw
assert fptag_final == initial_tag, "x87 tag word changed"
assert (fpsw_final & 0x3800) == 0, "x87 TOP not restored"
assert not (uc.reg_read(UC_X86_REG_EFLAGS) & 0x400), "direction flag set"
guarded_cache = bytes(uc.mem_read(CACHE - 64, CACHE_SIZE + 128))
guarded_input = bytes(uc.mem_read(INPUT - 64, 0x10000 + 128))
after_stack = bytes(uc.mem_read(STACK, 0x10000))
cache_guard = guarded_cache[:64] == guarded_cache[-64:] == b"\xA5" * 64
input_guard = guarded_input[:64] == guarded_input[-64:] == b"\xA5" * 64
input_immutable = guarded_input[64:-64] == input_raw
stack_guard = (after_stack[:64] == bytes(stack[:64])
and after_stack[SP - STACK:] == bytes(stack[SP - STACK:]))
actual_cache = guarded_cache[64:-64]
assert cache_guard and input_guard and input_immutable and stack_guard
assert actual_cache == bytes(expected_cache), (
self.label, case["name"], "cache mismatch",
[index for index, pair in enumerate(zip(actual_cache, expected_cache))
if pair[0] != pair[1]][:20])
assert state["trace"] == expected_trace, (self.label, case["name"], state["trace"], expected_trace)
semantic_events = [{key: value for key, value in event.items() if not key.endswith("_pointer")}
for event in state["events"]]
assert semantic_events == expected_events, (
self.label, case["name"], semantic_events, expected_events)
entry_hex = None
if expected_index is not None:
start = POOL_SIZE + expected_index * ENTRY_SIZE
entry_hex = actual_cache[start:start + ENTRY_SIZE].hex()
return {
"case": case["name"],
"fpcw_initial": fpcw,
"events": state["events"],
"function_trace": state["trace"],
"api_cache_snapshots": state["snapshots"],
"cache_sha256": digest(actual_cache),
"cache_entry_hex": entry_hex,
"cache_count": s16(actual_cache, COUNT_OFFSET),
"cache_time": s16(actual_cache, TIME_OFFSET),
"cache_pool_offset": s16(actual_cache, POOL_OFFSET),
"cache_flags": list(actual_cache[CACHE_WARNING_OFFSET:STRING_WARNING_OFFSET + 1]),
"returned": returned,
"system_exit_nonreturn": state["exited"],
"abi": {
"esp_restored": esp_restored,
"nonvolatiles_restored": nonvolatiles_restored,
"direction_flag_clear": True,
"fpcw_final": fpcw_final,
"fptag_restored": fptag_final == initial_tag,
"x87_top_restored": (fpsw_final & 0x3800) == 0,
"cache_guard": cache_guard,
"input_guard": input_guard,
"input_immutable": input_immutable,
"stack_guard": stack_guard,
"forbidden_access": state["forbidden_access"],
},
}
def comparable(outcome: dict) -> dict:
return {key: value for key, value in outcome.items() if key not in ("case",)}
def captured_outcome_negative_controls(outcomes: dict[tuple[str, int], dict]) -> dict:
controls = {}
def changed(name: str, case_name: str, mutate):
key = (case_name, 0x037F)
original = outcomes[key]
modified = copy.deepcopy(original)
mutate(modified)
controls[name] = comparable(modified) != comparable(original)
assert controls[name], name
def wrong_axis(item):
events = [event for event in item["events"] if event["api"] == "rasterizer_debug_line"]
events[0]["point0"] = events[1]["point0"]
def wrong_sign(item):
event = next(event for event in item["events"] if event["api"] == "rasterizer_debug_line")
raw = bytearray.fromhex(event["point0"])
raw[:4] = bytearray.fromhex(item["events"][0]["point1"][:8])[:4]
event["point0"] = raw.hex()
def wrong_tag(item):
raw = bytearray.fromhex(item["cache_entry_hex"])
raw[:2] = p16(2)
item["cache_entry_hex"] = raw.hex()
changed("wrong_point_axis", "point_immediate_positive", wrong_axis)
changed("wrong_point_sign", "point_immediate_positive", wrong_sign)
changed("wrong_cache_tag", "point_deferred_same_tick", wrong_tag)
changed("wrong_box_call_order", "box_immediate_ordinary",
lambda item: item["events"].__setitem__(slice(0, 2), list(reversed(item["events"][:2]))))
changed("wrong_stack_result", "line_immediate",
lambda item: item["abi"].__setitem__("esp_restored", False))
changed("wrong_nonvolatile_result", "line_immediate",
lambda item: item["abi"].__setitem__("nonvolatiles_restored", False))
changed("wrong_guard_result", "box_immediate_ordinary",
lambda item: item["abi"].__setitem__("cache_guard", False))
changed("wrong_fpcw_result", "point_immediate_positive",
lambda item: item["abi"].__setitem__("fpcw_final", 0x027F))
return controls
def in_memory_target_byte_negative_control() -> dict:
# January point +0x77 is `d8 e1` (fsub st(1)); replace only its ModR/M byte
# with c1 (`d8 c1`, fadd st(1)). This reverses the first x-axis endpoint
# while leaving the function runnable. Loader.run must reject its modeled
# event. The pinned target object on disk remains untouched.
mutation = {POINT: [(0x78, 0xE1, 0xC1)]}
loader = Loader("target_in_memory_wrong_x_sign", PATHS["target"], mutation)
case = next(item for item in cases() if item["name"] == "point_immediate_positive")
try:
loader.run(case, 0x037F)
except AssertionError as error:
return {
"detected": True,
"kind": "transient Unicorn code-memory mutation; on-disk object unchanged",
"function": POINT,
"offset": 0x78,
"expected_byte": 0xE1,
"replacement_byte": 0xC1,
"instruction_change": "d8 e1 fsub st(1) -> d8 c1 fadd st(1)",
"case": case["name"],
"fpcw": 0x037F,
"rejection": repr(error),
}
raise AssertionError("in-memory target instruction mutation was not detected")
def object_metadata(loader: Loader) -> dict:
metadata = {}
for name in FUNCTIONS:
symbol = cc.symbol(loader.obj, name)
metadata[name] = {
"storage": symbol["storage"],
"type": symbol["type"],
"value": symbol["value"],
"selection": _section_aux_selection(loader.obj, symbol["section"]),
**loader.sections[name].info,
}
half = next((symbol for symbol in loader.obj["symbols"]
if symbol["name"] == "__real@3f000000" and symbol["section"] > 0), None)
metadata["half_literal"] = None if half is None else {
"size": loader.obj["sections"][half["section"] - 1]["size"],
"selection": _section_aux_selection(loader.obj, half["section"]),
"payload": bytes(cc._section_bytes(loader.obj,
loader.obj["sections"][half["section"] - 1])).hex(),
}
return metadata
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--output",
type=Path,
default=ROOT / "scratch/render-debug-three-runtime-report.json",
help="report path (default preserves the original scratch artifact name)",
)
args = parser.parse_args()
for label, path in PATHS.items():
actual = digest(path.read_bytes())
assert actual == PINS[label], (label, actual, PINS[label])
loaders = {label: Loader(label, PATHS[label]) for label in ("target", "first", "actual")}
results = []
failures = []
target_outcomes: dict[tuple[str, int], dict] = {}
for case in cases():
for fpcw in FPCW_MODES:
try:
outcomes = {label: loader.run(case, fpcw) for label, loader in loaders.items()}
target = outcomes["target"]
target_outcomes[(case["name"], fpcw)] = target
first_equal = comparable(outcomes["first"]) == comparable(target)
actual_equal = comparable(outcomes["actual"]) == comparable(target)
assert first_equal and actual_equal
results.append({"case": case["name"], "fpcw": fpcw,
"target": target, "first_equal": first_equal,
"actual_equal": actual_equal})
except Exception as error:
failures.append({"case": case["name"], "fpcw": fpcw, "error": repr(error)})
outcome_controls = captured_outcome_negative_controls(target_outcomes) if not failures else {}
byte_control = in_memory_target_byte_negative_control() if not failures else {}
accepted_execution_count = len(cases()) * len(FPCW_MODES) * len(loaders)
report = {
"scope": __doc__,
"harness_sha256": digest(Path(__file__).read_bytes()),
"unicorn_version": u.__version__,
"pins": {label: {"path": str(path), "sha256": PINS[label]}
for label, path in PATHS.items()},
"objects": {label: object_metadata(loader) for label, loader in loaders.items()},
"scenario_count": len(cases()),
"fpcw_modes": list(FPCW_MODES),
"accepted_execution_count": accepted_execution_count,
"in_memory_negative_execution_count": 1 if byte_control else 0,
"execution_count": accepted_execution_count + (1 if byte_control else 0),
"failures": failures,
"all_target_models_pass": not failures,
"first_differential_pass": not failures and all(item["first_equal"] for item in results),
"actual_differential_pass": not failures and all(item["actual_equal"] for item in results),
"captured_outcome_negative_controls": {
"kind": "synthetic captured-outcome mutations; object bytes are never modified",
"results": outcome_controls,
"all_detected": bool(outcome_controls) and all(outcome_controls.values()),
},
"in_memory_target_byte_negative_control": byte_control,
"results": results,
"limitations": [
"Finite exact-value scenarios only; no NaN, infinity, denormal, or arbitrary invalid-input claim.",
"The point residual differs by four independent integer-store schedules around x87 operations; this harness proves the listed outcomes, not exhaustive equivalence.",
"system_exit is modeled as a genuine nonreturn boundary, so post-exit callee restoration is intentionally not asserted.",
"Only genuine external APIs are modeled; polygon_edges, public line, and private cache helpers execute their real same-object bytes.",
],
}
output = args.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps({
"output": str(output),
"scenario_count": report["scenario_count"],
"fpcw_modes": report["fpcw_modes"],
"execution_count": report["execution_count"],
"failures": failures,
"first_differential_pass": report["first_differential_pass"],
"actual_differential_pass": report["actual_differential_pass"],
"captured_outcome_negative_controls": report["captured_outcome_negative_controls"],
"in_memory_target_byte_negative_control": report["in_memory_target_byte_negative_control"],
}))
if (failures or not report["captured_outcome_negative_controls"]["all_detected"]
or not report["in_memory_target_byte_negative_control"].get("detected")):
raise SystemExit(1)
if __name__ == "__main__":
main()