"""Execute January and freshly rebuilt countdown-timer increment bodies.""" from contextlib import contextmanager import faulthandler import os from pathlib import Path import random import struct import subprocess import sys import pytest from tools import coff_compare as cc from tools.regression_gate import _section_aux_selection ROOT = Path(__file__).resolve().parents[1] UNIT = "source/networking/network_server_manager" SOURCE = ROOT / (UNIT + ".c") TARGET = ROOT / ("build/split/" + UNIT + ".obj") COMPILER = ROOT / "xbox/bin/vc7/CL.Exe" FUNCTION = "_countdown_timer_increment" SYSTEM_MILLISECONDS = "_system_milliseconds" DISPLAY_ASSERT = "_display_assert" SYSTEM_EXIT = "_system_exit" FILE_LITERAL = "??_C@_0DD@CFCGIJJL@c?3?2halo?2SOURCE?2networking?2networ@" ADJUSTMENT_LITERAL = "??_C@_0BA@LLGNDOEI@adjustment?5?$DO?$DN?50?$AA@" TIMER_LITERAL = "??_C@_0BL@DEKPBLAE@timer?9?$DOtime_remaining?5?$DO?$DN?50?$AA@" LITERALS = { FILE_LITERAL: b"c:\\halo\\SOURCE\\networking\\network_server_manager.c\0", ADJUSTMENT_LITERAL: b"adjustment >= 0\0", TIMER_LITERAL: b"timer->time_remaining >= 0\0", } TARGET_SPEC = ( 144, "0291de1fcc1f65baf92a784ad590c78925729c9c4ba0b9a1bbf33a6e8cdd7aa5", [ (0x05, 20, SYSTEM_MILLISECONDS), (0x39, 6, FILE_LITERAL), (0x3E, 6, ADJUSTMENT_LITERAL), (0x43, 20, DISPLAY_ASSERT), (0x4A, 20, SYSTEM_EXIT), (0x76, 6, FILE_LITERAL), (0x7B, 6, TIMER_LITERAL), (0x80, 20, DISPLAY_ASSERT), (0x87, 20, SYSTEM_EXIT), ], ) REBUILT_SPEC = ( 144, "0291de1fcc1f65baf92a784ad590c78925729c9c4ba0b9a1bbf33a6e8cdd7aa5", [ (0x05, 20, SYSTEM_MILLISECONDS), (0x39, 6, FILE_LITERAL), (0x3E, 6, ADJUSTMENT_LITERAL), (0x43, 20, DISPLAY_ASSERT), (0x4A, 20, SYSTEM_EXIT), (0x76, 6, FILE_LITERAL), (0x7B, 6, TIMER_LITERAL), (0x80, 20, DISPLAY_ASSERT), (0x87, 20, SYSTEM_EXIT), ], ) PAGE = 0x1000 CODE = 0x00100000 STOP = CODE + 0x0F00 API_PAGE = 0x00200000 APIS = { SYSTEM_MILLISECONDS: API_PAGE + 0x100, DISPLAY_ASSERT: API_PAGE + 0x200, SYSTEM_EXIT: API_PAGE + 0x300, } DATA_PAGE = 0x00300000 LITERAL_ADDRESSES = { FILE_LITERAL: DATA_PAGE + 0x100, ADJUSTMENT_LITERAL: DATA_PAGE + 0x200, TIMER_LITERAL: DATA_PAGE + 0x300, } TIMER_PAGE = 0x00400000 TIMER = TIMER_PAGE + 0x800 STACK_PAGE = 0x00500000 STACK_SIZE = 0x20000 ENTRY_ESP = STACK_PAGE + 0x10000 SAVED_REGISTERS = { "EBP": 0x1B1B1B1B, "EBX": 0x2B2B2B2B, "ESI": 0x3B3B3B3B, "EDI": 0x4B4B4B4B, } class RuntimeSemanticMismatch(AssertionError): """Raised only after two successfully executed bodies produce different results.""" @contextmanager def quiet_unicorn_windows_exceptions(): """Hide only Unicorn's internally handled Windows mapping probes.""" enabled = faulthandler.is_enabled() if enabled: faulthandler.disable() try: yield finally: if enabled: faulthandler.enable() def u32(value): return value & 0xFFFFFFFF def s32(value): value = u32(value) return value - 0x100000000 if value & 0x80000000 else value def relocation_name(relocation): target = relocation.get("symbolic_target", relocation["target"]) assert len(target) == 3 and target[0] == "symbol" and target[2] == 0, relocation return target[1] def verify_literal_contracts(obj): for name, payload in LITERALS.items(): owners = [symbol for symbol in obj["symbols"] if symbol["name"] == name and symbol["section"] > 0] assert len(owners) == 1, (name, owners) owner = owners[0] section = obj["sections"][owner["section"] - 1] assert (owner["value"], owner["type"], owner["storage"]) == (0, 0, 2) assert section["name"] == ".rdata" assert section["flags"] == 0x40301040 assert section["reloc_count"] == 0 assert _section_aux_selection(obj, section["index"]) == 2 assert bytes(cc._section_bytes(obj, section)) == payload def prepare_body(obj, spec): expected_size, expected_sha, expected_relocations = spec owner = cc.symbol(obj, FUNCTION) section = obj["sections"][owner["section"] - 1] info = cc.section_info(obj, FUNCTION) assert (owner["value"], owner["type"], owner["storage"]) == (0, 0x20, 2) assert section["name"] == ".text" and section["flags"] == 0x60501020 assert _section_aux_selection(obj, section["index"]) == 1 assert (info["size"], info["relocation_count"], info["normalized_sha256"]) == ( expected_size, len(expected_relocations), expected_sha) actual_relocations = [ (relocation["address"], relocation["type"], relocation_name(relocation)) for relocation in info["relocations"] ] assert actual_relocations == expected_relocations verify_literal_contracts(obj) raw = bytearray(cc._section_bytes(obj, section)) for offset, kind, name in actual_relocations: assert struct.unpack_from("time_remaining -= elapsed_time;") == 1 assert b"(unsigned long)" not in update_body increment_start = normalized_source.rindex(b"void countdown_timer_increment(") increment_end = normalized_source.index(b"void countdown_timer_decrement(", increment_start) increment_body = normalized_source[increment_start:increment_end] increment_tokens = b" ".join(increment_body.split()) assert increment_tokens.count( b"timer->time_remaining + adjustment < adjustment") == 1 assert b"(unsigned long)" not in increment_body output = tmp_path_factory.mktemp("countdown-timer-increment") / "rebuilt.obj" completed = subprocess.run( [sys.executable, "-B", "tools/campaign/gate.py", UNIT, "--fn", FUNCTION, "--out", str(output)], cwd=ROOT, env=dict(os.environ, HALO_CL=str(COMPILER), PYTHONDONTWRITEBYTECODE="1"), capture_output=True, text=True, ) assert completed.returncode == 0, completed.stdout + completed.stderr assert output.is_file() and output.stat().st_size > 0 assert SOURCE.read_bytes() == source_before, "canonical source changed during fixture" return { "target": prepare_body(cc.load(TARGET), TARGET_SPEC), "rebuilt": prepare_body(cc.load(output), REBUILT_SPEC), } def make_case(label, remaining, last, now, adjustment, maximum): return { "label": label, "remaining": u32(remaining), "last": u32(last), "now": u32(now), "adjustment": u32(adjustment), "maximum": u32(maximum), } def cases(): result = [ make_case("ordinary_no_clock_progress", 30_000, 100, 100, 5_000, 30_999), make_case("ordinary_forward_elapsed", 100, 100, 110, 5, 1_000), make_case("elapsed_exhausts_timer", 5, 100, 110, 5, 1_000), make_case("elapsed_equals_timer", 10, 100, 110, 0, 1_000), make_case("positive_to_negative_clock_sign_transition", 100, 0x7FFFFFFF, 0x80000000, 0, 1_000), make_case("negative_half_clock_forward", 100, 0x80000000, 0x80000001, 0, 1_000), make_case("tickcount_full_wrap", 100, 0xFFFFFFFF, 0, 0, 1_000), make_case("negative_elapsed_path", 100, 0x80000000, 0x7FFFFFFF, 0, 1_000), make_case("negative_elapsed_subtraction_wrap", 0x7FFFFFFF, 0x80000000, 0x7FFFFFFF, 0, 0x7FFFFFFF), make_case("increment_sign_overflow_clamps", 0x7FFFFFFF, 1, 1, 1, 30_999), make_case("increment_clamps_to_maximum", 29_000, 1, 1, 5_000, 30_999), make_case("largest_nonoverflow_increment", 0, 1, 1, 0x7FFFFFFF, 0x7FFFFFFF), make_case("negative_adjustment_asserts_after_update", 100, 100, 101, -1, 1_000), make_case("negative_maximum_final_assert", 0, 1, 1, 0, -1), make_case("negative_maximum_overflow_final_assert", 0x7FFFFFFF, 1, 1, 1, -1), make_case("initial_negative_remaining_wraps", -1, 1, 1, 1, 123), make_case("positive_clock_to_minus_one_skips", 100, 0, 0xFFFFFFFF, 0, 1_000), ] generator = random.Random(0x11B660) for index in range(256): result.append(make_case( f"random_valid_{index:03d}", generator.randrange(0x80000000), generator.randrange(0x100000000), generator.randrange(0x100000000), generator.randrange(0x80000000), generator.randrange(0x80000000), )) for index in range(32): result.append(make_case( f"random_negative_adjustment_{index:03d}", generator.randrange(0x80000000), generator.randrange(0x100000000), generator.randrange(0x100000000), generator.randrange(0x80000000, 0x100000000), generator.randrange(0x80000000), )) for index in range(32): result.append(make_case( f"random_negative_maximum_{index:03d}", generator.randrange(0x80000000), generator.randrange(0x100000000), generator.randrange(0x100000000), generator.randrange(0x80000000), generator.randrange(0x80000000, 0x100000000), )) assert len(result) == 337 return result def range_contains(base, length, address, size): return size > 0 and base <= address and address + size <= base + length def ranges_overlap(first_base, first_length, second_base, second_length): return first_base < second_base + second_length and second_base < first_base + first_length def execute(code, item): # Unicorn can raise and internally handle Windows mapping exceptions while # constructing the machine as well as while running it. with quiet_unicorn_windows_exceptions(): return _execute(code, item) def _execute(code, item): import unicorn as u from unicorn import x86_const as x cpu = u.Uc(u.UC_ARCH_X86, u.UC_MODE_32) for base, length in ( (CODE, PAGE), (API_PAGE, PAGE), (DATA_PAGE, PAGE), (TIMER_PAGE, PAGE), (STACK_PAGE, STACK_SIZE), ): cpu.mem_map(base, length) cpu.mem_write(CODE, code) for address in APIS.values(): cpu.mem_write(address, b"\xC3") for name, address in LITERAL_ADDRESSES.items(): cpu.mem_write(address, LITERALS[name]) timer_before = bytes([0xA5]) * PAGE cpu.mem_write(TIMER_PAGE, timer_before) cpu.mem_write(TIMER, struct.pack(" JMP: bypass the overflow clamp. changed = execute(bytes(mutant), item) with pytest.raises(RuntimeSemanticMismatch): require_same(reference, changed) def test_real_timer_update_branch_mutation_is_rejected(bodies): item = next(item for item in cases() if item["label"] == "ordinary_forward_elapsed") reference = execute(bodies["rebuilt"], item) require_same(execute(bodies["target"], item), reference) mutant = bytearray(bodies["rebuilt"]) assert mutant[0x11:0x13] == b"\x7E\x16" mutant[0x11] = 0xEB # JLE -> JMP: always bypass elapsed-time processing. changed = execute(bytes(mutant), item) with pytest.raises(RuntimeSemanticMismatch): require_same(reference, changed)