urt-rs / scripts /analyze_cvar_changes.py
nmarshall's picture
Recover cvar change behavior and setting correction paths
b1c5e8e
Raw History Blame Contribute Delete
7.22 kB
#!/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()