#!/usr/bin/env python3 """Recover bounded qagame cvar change paths without executing any callees.""" import json from pathlib import Path from analyze_deferred_auth import partition_function, trace_slice from analyze_game_cvars import vm_string from analyze_qvm import decode, write_json from analyze_vm_extensions import verified def direct_registrations(ins, data, header): """Index adjacent trap-3 calls; immediate argument candidates are syntactic.""" result, entry = [], None for i in ins: at = i['index'] if i['opcode'] == 'ENTER': entry = at if i['opcode'] != 'CALL' or at == 0 or ins[at-1]['opcode'] != 'CONST' or ins[at-1]['operand'] != -4: continue start = max(entry, at-28) previous_calls = [j for j in range(start, at) if ins[j]['opcode'] == 'CALL'] if previous_calls: start = previous_calls[-1]+1 candidates = {} for j in range(start, at-1): if ins[j]['opcode'] == 'CONST' and ins[j+1]['opcode'] == 'ARG': arg, value = ins[j+1]['operand'], ins[j]['operand'] item = {'instruction': j, 'value': value} if arg in (12, 16) and 0 <= value < header['data_length']+header['literal_length']: item['string'] = vm_string(data, header, value) candidates[str(arg)] = item result.append({'function_entry': entry, 'call_instruction': at, 'immediate_argument_candidates': candidates, 'window': ins[start:at+2]}) return result def main(): source = json.loads(Path('docs/research/qvm/qagame.json').read_text()) data = verified(Path(source['source']['member']['output']).read_bytes(), source['sha256']) header, ins = decode(data) registry = json.loads(Path('docs/research/game-cvars.json').read_text()) if registry['sha256'] != source['sha256']: raise ValueError('registry provenance mismatch') rows = registry['registry']['rows'] for name, address in [('g_gametype', 34090788), ('g_stratTime', 34077764)]: if len([r for r in rows if r['name'] == name and r['mirror_address']+12 == address]) != 1: raise ValueError('strategy setting mirror identity changed') row, mirror = 0x10000, 0x20000 traces = {} # Each fixture follows actual instructions, recording rather than entering calls. traces['cache_changed'] = trace_slice(ins, 2974, 2997, {row: mirror, row+16: 7, mirror+4: 8}, {20: row}) traces['cache_unchanged'] = trace_slice(ins, 2974, 3834, {row: mirror, row+16: 8, mirror+4: 8}, {20: row}) traces['null_mirror'] = trace_slice(ins, 2962, 3834, {row: 0}, {20: row}) traces['update_call'] = trace_slice(ins, 2962, 2974, {row: mirror}, {20: row}) traces['notifications_disabled'] = trace_slice(ins, 2997, 3123, {row+20: 0}, {20: row}) notifications = [] for comparison, flags in [(0, 0), (1, 0), (1, 32), (1, 33)]: model = trace_slice(ins, 3018, 3123, {row: mirror, row+4: 0x30000, row+12: flags, mirror+8: 0x3f800000}, {20: row, 40: comparison}) notifications.append({'name_comparison_result': comparison, 'raw_flags': flags, 'trace': model}) word24 = [] for value in (0, 1, -1): for phase, start, stop, pointer_local, flag_local in [('register', 2764, 2774, 24, 32), ('update', 3123, 3133, 20, 28)]: model = trace_slice(ins, start, stop, {row+24: value}, {pointer_local: row, flag_local: 0}) word24.append({'phase': phase, 'word24': value, 'flag_local': flag_local, 'trace': model}) settings = [] for name, address, start, stop, values in [ ('g_bombPlantTime', 34080428, 3219, 3260, [-1, 0, 2, 3, 5]), ('g_funstuff', 34063556, 3585, 3630, [-1, 0, 1, 2]), ('g_inactivity', 34085460, 3648, 3668, [-1, 0, 1, 9, 10])]: match = [r for r in rows if r['name'] == name and r['mirror_address']+12 == address] if len(match) != 1: raise ValueError('setting mirror identity changed') for value in values: settings.append({'name': name, 'input': value, 'integer_address': address, 'trace': trace_slice(ins, start, stop, {address: value}, {})}) for gametype in (0, 4, 8): for value in (-1, 0, 1, 2, 10, 11): settings.append({'name': 'g_stratTime', 'input': value, 'gametype_raw': gametype, 'integer_address': 34077764, 'trace': trace_slice(ins, 3777, 3834, {34077764: value, 34090788: gametype}, {})}) strings = {str(at): vm_string(data, header, at) for at in [36187, 36145, 36111, 39360, 36382, 36358, 36345, 37632, 35998, 38919, 38546, 39625, 38431, 39367, 39400, 39982, 39731, 39293]} sites = direct_registrations(ins, data, header) constant_traces = [] for site in sites: at = site['call_instruction'] start = at-9 sequence = ins[start:at+2] expected = ['CONST', 'ARG']*4+['CONST', 'CALL', 'POP'] if [i['opcode'] for i in sequence] != expected or [sequence[j]['operand'] for j in (1, 3, 5, 7)] != [8, 12, 16, 20]: continue constant_traces.append({'call_instruction': at, 'trace': trace_slice(ins, start, at+2, {}, {})}) output = {'source': source['source'], 'sha256': source['sha256'], 'row_word20_nonzero_names': [r['name'] for r in rows if r['word20_initial']], 'row_word24_nonzero_names': [r['name'] for r in rows if r['word24_initial']], 'cache_traces': traces, 'notification_traces': notifications, 'word24_traces': word24, 'setting_traces': settings, 'strings': strings, 'direct_registration_sites': sites, 'constant_registration_traces': constant_traces, 'post_table_registration_trace': trace_slice(ins, 2791, 2813, {}, {}), 'registration_wrapper_trace': trace_slice(ins, 16095, 16124, {}, {32: 0x20000, 36: 0x30000, 40: 0x40000, 44: 80}), 'support_functions': {str(e): partition_function(ins, e, data, header) for e in [2589, 16094, 34364, 69386]}, 'word24_call_windows': {str(at): ins[at-5:at+3] for at in [2818, 3873]}, 'limitations': 'Static supplied qagame and bounded synthetic memory only. Calls are not entered; ' 'zero call results are placeholders, including formatted command pointers. Notification comparison ' 'results are explicit fixture inputs. Setting slices exclude surrounding name/change gates. Immediate ' 'registration argument candidates are syntactic, not proven CFG argument flow; computed calls excluded. ' 'Trap cvar-set calls do not mutate synthetic mirrors. No effective runtime/config/permissions claim.'} write_json(Path('docs/research/cvar-changes.json'), output) print(f'Recovered {len(output["direct_registration_sites"])} direct registration sites and {len(settings)} setting fixtures') if __name__ == '__main__': main()