nmarshall commited on
Commit
529b124
·
1 Parent(s): e678ca6

Trace deferred authentication drops and bounded message copy semantics

Browse files
docs/FINDINGS.md CHANGED
@@ -190,3 +190,8 @@ Exact native fork/source revision, runtime resource lookup, legacy launch enviro
190
  **F-032 — Observed: bounded native byte regions reconciled.** The SDL text bytes outside debug-map function ranges are 70 NOP runs totaling 524 bytes. Windows client resource bytes partition into 353,268 payload bytes and 520 tree-structure bytes, with no unreferenced gaps. This resolves the previously unclassified bytes in these two regions, not whole-binary semantics. [COV-001/002](research/native/COVERAGE.md).
191
 
192
  **F-033 — Observed/inferred: authentication drop boundary and deferred QVM branch.** Linux ELF64 trap 603 receives index/broadcast text/disconnect reason; valid completion and invalid-index guards both return zero. The QVM caller validates slot and states 1/2; state 1 calls a helper with record buffers and limits 128/256, then sets a flag, while state 2 invokes the native service. Deferred-consumer behavior, state names, pointer errors and other platforms remain open. [AUTH-001/002](research/AUTH_DROP.md).
 
 
 
 
 
 
190
  **F-032 — Observed: bounded native byte regions reconciled.** The SDL text bytes outside debug-map function ranges are 70 NOP runs totaling 524 bytes. Windows client resource bytes partition into 353,268 payload bytes and 520 tree-structure bytes, with no unreferenced gaps. This resolves the previously unclassified bytes in these two regions, not whole-binary semantics. [COV-001/002](research/native/COVERAGE.md).
191
 
192
  **F-033 — Observed/inferred: authentication drop boundary and deferred QVM branch.** Linux ELF64 trap 603 receives index/broadcast text/disconnect reason; valid completion and invalid-index guards both return zero. The QVM caller validates slot and states 1/2; state 1 calls a helper with record buffers and limits 128/256, then sets a flag, while state 2 invokes the native service. Deferred-consumer behavior, state names, pointer errors and other platforms remain open. [AUTH-001/002](research/AUTH_DROP.md).
193
+
194
+
195
+ **F-034 — Observed: bounded copy and error-service chain.** QVM helper 69338 calls trap 102 with capacity minus one and explicitly NUL-terminates the final destination byte. Null source/nonpositive capacity route through wrappers to game trap 1 and native Com_Error mode 1, not a print-only service. The native handler's full exit/escalation behavior remains open. [DEF-001](research/DEFERRED_AUTH.md).
196
+
197
+ **F-035 — Observed: deferred-drop sentinel/timestamp and strict delay.** Eight direct constant-offset references show field 3588 changing from sentinel 1 to current time in the begin callback, then being consumed by the frame callback only for state 2, positive pending value and signed elapsed > 100. Synthetic instruction-slice tests verify 100/101 boundaries and wrap; actual runtime timing, aliases, state names and reentrancy remain open. [DEF-002/003](research/DEFERRED_AUTH.md).
docs/RESEARCH_LEDGER.md CHANGED
@@ -83,3 +83,8 @@ Completed static milestones RE-214–216 with `analyze_native_metadata.py`: all
83
  ## 2026-10-11 — Byte reconciliation and authentication drop boundary
84
 
85
  Completed RE-218–220 with `analyze_native_coverage.py` and `analyze_auth_drop.py`: bounded byte partitions explain all SDL text gaps and resource-tree bytes; focused native/QVM evidence recovers broadcast/disconnect parameters, invalid-index zero returns and deferred versus immediate caller branches. F-032/033, COV-001/002 and AUTH-001/002 retain source hashes and limitations. Seven new tests verify interval accounting, malformed bounds, guard convergence, result-preserving epilogues, bounded literals and supplied fixtures. Static literal scanning excludes nonallocated ELF sections to avoid interpreting small numeric constants as offsets into compiler-comment sections. No original game code was executed.
 
 
 
 
 
 
83
  ## 2026-10-11 — Byte reconciliation and authentication drop boundary
84
 
85
  Completed RE-218–220 with `analyze_native_coverage.py` and `analyze_auth_drop.py`: bounded byte partitions explain all SDL text gaps and resource-tree bytes; focused native/QVM evidence recovers broadcast/disconnect parameters, invalid-index zero returns and deferred versus immediate caller branches. F-032/033, COV-001/002 and AUTH-001/002 retain source hashes and limitations. Seven new tests verify interval accounting, malformed bounds, guard convergence, result-preserving epilogues, bounded literals and supplied fixtures. Static literal scanning excludes nonallocated ELF sections to avoid interpreting small numeric constants as offsets into compiler-comment sections. No original game code was executed.
86
+
87
+
88
+ ## 2026-10-11 — Deferred authentication drops
89
+
90
+ Completed static milestones RE-222–224: copy-count/final-NUL behavior, invalid-input error-service chain, eight direct pending-field references and sentinel/timestamp transitions, strict signed elapsed > 100 consumer. `analyze_deferred_auth.py` emits source-hashed functions/native cases and bounded synthetic-memory traces; six tests check branch/copy boundaries and reject unsupported slices. Findings F-034/035 and DEF-001–003 distinguish these checks from runtime fixtures. An initial print-path interpretation of the diagnostic wrapper was corrected after tracing function 2319 through trap 1 to native Com_Error(1, ...). No game process or full QVM runtime was launched; tests only evaluate selected decoded instructions without entering callees.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -260,4 +260,9 @@ Purpose: turn the supplied installation into an evidence-backed specification of
260
  - [x] RE-218 Reconcile the 524 SDL text bytes outside paired debug-map functions and all Windows resource-region metadata/padding gaps; preserve evidence for every byte classification. **Evidence:** `analyze_native_coverage.py`, source-hashed segment partitions and COV-001/002; 70 SDL NOP runs account for all 524 bytes, Windows region accounts for 353,268 payload + 520 structure bytes with no gaps. Byte patterns do not establish runtime reachability.
261
  - [x] RE-219 Trace Linux ELF64 trap 603 argument meanings, both invalid-index continuations and common return epilogues. **Evidence:** `analyze_auth_drop.py`, `auth-drop.json`, AUTH-001; broadcast/disconnect formats and zero returns verified. Complete helper behavior/all-platform parity remain RE-206.
262
  - [x] RE-220 Recover the QVM trap-603 caller's slot/state guards, record stride and state-1 storage versus state-2 immediate service call. **Evidence:** AUTH-002 and full caller instructions; state names, copy-helper behavior and deferred consumer remain open.
263
- - [ ] RE-221 Trace the QVM record flag at offset 3588, helper 69338 and state transitions; specify deferred authentication drops, buffer limits, terminator behavior and reentrancy with runtime fixtures.
 
 
 
 
 
 
260
  - [x] RE-218 Reconcile the 524 SDL text bytes outside paired debug-map functions and all Windows resource-region metadata/padding gaps; preserve evidence for every byte classification. **Evidence:** `analyze_native_coverage.py`, source-hashed segment partitions and COV-001/002; 70 SDL NOP runs account for all 524 bytes, Windows region accounts for 353,268 payload + 520 structure bytes with no gaps. Byte patterns do not establish runtime reachability.
261
  - [x] RE-219 Trace Linux ELF64 trap 603 argument meanings, both invalid-index continuations and common return epilogues. **Evidence:** `analyze_auth_drop.py`, `auth-drop.json`, AUTH-001; broadcast/disconnect formats and zero returns verified. Complete helper behavior/all-platform parity remain RE-206.
262
  - [x] RE-220 Recover the QVM trap-603 caller's slot/state guards, record stride and state-1 storage versus state-2 immediate service call. **Evidence:** AUTH-002 and full caller instructions; state names, copy-helper behavior and deferred consumer remain open.
263
+ - [ ] RE-221 Trace the QVM record flag at offset 3588, helper 69338 and state transitions; specify deferred authentication drops, buffer limits, terminator behavior and reentrancy with runtime fixtures. **Progress 2026-10-11:** Eight constant-offset references, copy/error helpers and state-dependent sentinel/timestamp consumption recovered. Strict signed elapsed > 100 gate has synthetic instruction-slice checks; runtime fixtures, aliases and reentrancy remain open ([DEF-001–003](research/DEFERRED_AUTH.md)).
264
+ - [x] RE-222 Recover helper 69338 copy count, terminator write and invalid-input diagnostic chain through game trap 1/native Com_Error. **Evidence:** DEF-001, complete helper bodies and both ELF64 cases; capacity 1/2/128/256 and invalid-input slice tests pass.
265
+ - [x] RE-223 Collect all explicit constant-3588 references and associate sentinel initialization, connection reset, begin-time replacement and frame-time reads with source-hashed function partitions. **Evidence:** DEF-002; eight references across four functions; alias/computed-offset coverage remains open.
266
+ - [x] RE-224 Recover and verify the deferred drop consumer's strict > 100 signed 32-bit elapsed-time branch and argument forwarding. **Evidence:** DEF-003 and synthetic traces; delay boundaries, state gates, pending values and signed wrap tested without entering callees.
267
+ - [ ] RE-225 Trace native Com_Error mode 1 through ordinary, repeated and recursive error paths on every platform; define formatting, shutdown, nonlocal exit and Rust error-boundary behavior.
268
+ - [ ] RE-226 Establish game timestamp units and wrap/reset behavior; verify deferred-drop sentinel collisions, stored negative timestamps and connection resets with deterministic runtime traces.
docs/research/AUTH_DROP.md CHANGED
@@ -28,3 +28,5 @@ For state 2, it passes `AccountKick: %i - %s\n`, the index and broadcast-message
28
  - Add deterministic reference fixtures for invalid/inactive slots, deferred state, immediate state, empty broadcast text, disconnect reasons and repeated drops. No runtime conformance is claimed by static recognizer tests.
29
 
30
  RE-219/220 record completed static milestones. RE-206 remains open for full error/helper behavior and all-platform parity.
 
 
 
28
  - Add deterministic reference fixtures for invalid/inactive slots, deferred state, immediate state, empty broadcast text, disconnect reasons and repeated drops. No runtime conformance is claimed by static recognizer tests.
29
 
30
  RE-219/220 record completed static milestones. RE-206 remains open for full error/helper behavior and all-platform parity.
31
+
32
+ Follow-up: [DEF-001–003](DEFERRED_AUTH.md) resolves the copy helper, all eight direct pending-field references and the sentinel-to-timestamp consumer path. The strict elapsed > 100 branch has synthetic instruction-slice checks; runtime fixtures and reentrancy remain open.
docs/research/DEFERRED_AUTH.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deferred authentication drops and bounded message copying
2
+
3
+ Static pass, 2026-10-11. Reproduce with `python3 scripts/analyze_deferred_auth.py`; validate with `python3 scripts/test_deferred_auth.py`. [Generated evidence](deferred-auth.json) retains source hashes, complete selected QVM functions, direct-call candidates, every observed constant-3588 reference, native service cases and synthetic instruction-slice traces.
4
+
5
+ No original game process or complete QVM module was executed. The bounded test evaluator follows selected decoded instructions against synthetic memory, records calls without entering callees, and uses a placeholder return value. These checks establish local arithmetic/branch/argument behavior; they are not engine, networking or gameplay reference fixtures.
6
+
7
+ ## DEF-001: message-copy boundary
8
+
9
+ Function entry **69338** contains the diagnostic literals `Q_strncpyz: NULL src` and `Q_strncpyz: destsize < 1`. For a non-null source and positive capacity, it calls game trap **102** with `(destination, source, capacity - 1)`, then writes a zero byte at `destination + capacity - 1`. Both Linux ELF64 native dispatch cases route trap 102 to `strncpy`; their address-keyed cases are preserved in the report.
10
+
11
+ Capacity includes the terminator. A capacity of one requests a zero-length native copy and explicitly writes the sole destination byte to zero. Deferred broadcast/reason buffers pass capacities 128 and 256, producing maximum copy counts 127 and 255 and explicit terminal byte positions at those offsets. The native `strncpy` dependency still needs implementation/platform comparison for all padding, overlap and pointer-error behavior; slice tests record the call and do not perform a real native copy.
12
+
13
+ For null source or capacity below one, the helper invokes function **5096**. That wrapper formats through helper 37914, then calls function **2319** with `%s`. Function 2319 formats again and invokes trap **1** (`CONST -2`), whose Linux ELF64 dispatcher converts the string pointer and calls **`Com_Error(1, format, string)`**. This is the engine error path. The full native `Com_Error` functions contain shutdown, system-error and nonlocal-jump paths; exact selection, repeated-error escalation and termination semantics remain open. Do not infer a normal return or model this as a print-only diagnostic.
14
+
15
+ ## DEF-002: field 3588 is sentinel plus timestamp
16
+
17
+ Eight direct constant-offset references occur across four function partitions. Their complete contexts are recorded. This is coverage of explicit `CONST 3588` references, not proof that aliases or computed field addresses do not exist.
18
+
19
+ | Function entry | Observed role | Field/buffer behavior |
20
+ | --- | --- | --- |
21
+ | 165818 | Authentication drop request | State 1 invokes the copy helper into record offsets 3204/3332 and stores field 3588 = 1. State 2 clears the field and invokes native trap 603 immediately. |
22
+ | 173979 | Entry-command-2 connect callback candidate | Assigns state 1 at record offset 468. Under a nonzero incoming argument at local offset 1092, clears field 3588 and zeroes both 128/256-byte buffers using trap 100. Recovering the argument's authoritative name remains open. |
23
+ | 174399 | Entry-command-3 begin callback candidate | Assigns state 2. When global 35017616 is positive, replaces field value exactly 1 with current global timestamp 34992532. Zero and other values are preserved by this branch. |
24
+ | 15186 | Entry-command-8 frame callback candidate | Iterates client records, requires state 2 and field at least 1, then compares signed current-time-minus-field against 100. Eligible slots call 165818 with pointers to both stored message buffers. |
25
+
26
+ The frame function first replaces global timestamp 34992532 with its incoming argument from local offset 92. This supplies the source of the deferred deadline comparison. Timestamp units have not been independently established in this pass.
27
+
28
+ Field 3588 is not simply a Boolean flag. Value 1 initially acts as a sentinel and is later overwritten with a timestamp. At game time one, that stored timestamp also equals the sentinel, and zero/nonpositive timestamps interact with the pending-value gate. These edge cases need reference fixtures before choosing a Rust representation or normalizing behavior.
29
+
30
+ ## DEF-003: strictly greater than 100
31
+
32
+ At instructions 15896–15905 the frame path subtracts the stored field from global time with 32-bit signed semantics and skips processing when the result is **less than or equal to 100**. The drop occurs only above 100, after the state/pending gates. At instruction 15920 it calls 165818 with `(slot, record + 3204, record + 3332)`; that function's state-2 path clears the field and invokes the native drop service.
33
+
34
+ Synthetic slice checks verify elapsed 99 and 100 do not call the drop helper, while 101 does; inactive state, zero/negative pending values and negative elapsed time skip it. Subtraction wraps to 32 bits before the signed comparison. A crossing from positive stored time near `INT_MAX` to negative current time can therefore produce a positive elapsed result, but the separate pending-value gate rejects negative stored timestamps. This preserves observed arithmetic without promising desirable behavior after long uptime or clock changes.
35
+
36
+ The begin callback also uses record field 3200 as a one-shot gate for helper 165352 when no pending drop is present and authentication is enabled. Its surrounding admission/authentication behavior remains unreviewed; do not generalize the drop timing slice to the entire callback.
37
+
38
+ ## Verification and remaining work
39
+
40
+ Six tests cover strict delay boundaries, state/pending/wrap gates, sentinel replacement under the authentication-enabled gate, copy count/NUL positions, invalid-input diagnostic calls, native error targets, and fail-closed unknown reads/opcodes/control-flow bounds. The evaluator intentionally rejects full function prologues and arbitrary callees.
41
+
42
+ RE-222–224 complete these static deliverables. RE-221 remains open for runtime fixtures, reentrancy, authoritative field/state naming, alias coverage and complete state-transition behavior. Follow the formatting helper and native error handler, test buffer contents and repeated drops in an isolated reference harness, and compare Windows/macOS service behavior before implementing compatibility policy in Rust.
docs/research/README.md CHANGED
@@ -1,6 +1,6 @@
1
  # Binary reverse engineering: first static analysis pass
2
 
3
- Analysis date: **2026-10-11**. Reference: Urban Terror 4.3.4, with source hashes from [the installation snapshot](../inventory/snapshot.json) and [extraction manifests](../extraction/README.md). No original executable, library, QVM, or recovered native code was executed.
4
 
5
  This pass decodes every supplied QVM and inventories/disassembles all eight native images. It recovers entry-command jump tables and selected native syscall tables, while retaining unknown calls, ambiguous strings and unreviewed function bodies. It does not establish gameplay parity or a complete engine ABI.
6
 
@@ -12,6 +12,8 @@ The [native metadata follow-up](native/METADATA.md) recovers SDL/client debug-ma
12
 
13
  [Byte reconciliation](native/COVERAGE.md) now explains the SDL text gaps and entire Windows resource region; [authentication drop analysis](AUTH_DROP.md) traces parameter meanings, invalid-index returns and QVM state branches.
14
 
 
 
15
  ## Reproduce
16
 
17
  From the repository root, with the verified source installation and extracted assets present:
@@ -22,10 +24,11 @@ python3 scripts/test_binary_analysis.py
22
  python3 scripts/test_vm_extensions.py
23
  python3 scripts/test_native_metadata.py
24
  python3 scripts/test_native_followup.py
 
25
  python3 scripts/storage_policy.py check --require-setup
26
  ```
27
 
28
- The pipeline runs `fetch_analysis_references.py`, `analyze_qvm.py`, `analyze_native.py`, `analyze_vm_abi.py`, `analyze_native_dispatch.py`, `analyze_vm_extensions.py`, `analyze_native_metadata.py`, `analyze_native_coverage.py`, and `analyze_auth_drop.py` in that order. Python 3.9+ and **LLVM objdump** with x86 ELF/COFF/Mach-O support are required. This pass used Apple LLVM 17.0.0 on macOS arm64; it disassembled x86 images without running them. Upstream references are downloaded only if absent, checked against the committed SHA-256 manifest, and cached under `.cache/research/upstream/`. A clone also needs `git lfs pull` to obtain the derived artifacts/extracted members.
29
 
30
  Generated JSON/JSONL and compressed evidence are regenerated in place; keep prose notes in Markdown. Rerunning the four analysis stages reproduced every generated report and gzip artifact byte-for-byte on the same toolchain. gzip timestamps and stored filenames are fixed. Different objdump versions can produce different disassembly text, so tool version and uncompressed evidence hashes are recorded. Reports are UTF-8 normal Git text, with individual files below the storage policy's 2 MiB limit.
31
 
 
1
  # Binary reverse engineering: first static analysis pass
2
 
3
+ Analysis date: **2026-10-11**. Reference: Urban Terror 4.3.4, with source hashes from [the installation snapshot](../inventory/snapshot.json) and [extraction manifests](../extraction/README.md). No original executable, library or recovered native code was launched. No complete QVM module was run; the deferred-auth follow-up evaluates selected decoded instructions with synthetic memory and records calls without entering callees.
4
 
5
  This pass decodes every supplied QVM and inventories/disassembles all eight native images. It recovers entry-command jump tables and selected native syscall tables, while retaining unknown calls, ambiguous strings and unreviewed function bodies. It does not establish gameplay parity or a complete engine ABI.
6
 
 
12
 
13
  [Byte reconciliation](native/COVERAGE.md) now explains the SDL text gaps and entire Windows resource region; [authentication drop analysis](AUTH_DROP.md) traces parameter meanings, invalid-index returns and QVM state branches.
14
 
15
+ [Deferred authentication analysis](DEFERRED_AUTH.md) traces message-copy/error services and the sentinel/timestamp state branches, with bounded synthetic-memory instruction checks distinct from runtime conformance.
16
+
17
  ## Reproduce
18
 
19
  From the repository root, with the verified source installation and extracted assets present:
 
24
  python3 scripts/test_vm_extensions.py
25
  python3 scripts/test_native_metadata.py
26
  python3 scripts/test_native_followup.py
27
+ python3 scripts/test_deferred_auth.py
28
  python3 scripts/storage_policy.py check --require-setup
29
  ```
30
 
31
+ The pipeline runs `fetch_analysis_references.py`, `analyze_qvm.py`, `analyze_native.py`, `analyze_vm_abi.py`, `analyze_native_dispatch.py`, `analyze_vm_extensions.py`, `analyze_native_metadata.py`, `analyze_native_coverage.py`, `analyze_auth_drop.py`, and `analyze_deferred_auth.py` in that order. Python 3.9+ and **LLVM objdump** with x86 ELF/COFF/Mach-O support are required. This pass used Apple LLVM 17.0.0 on macOS arm64; it disassembled x86 images without running them. Upstream references are downloaded only if absent, checked against the committed SHA-256 manifest, and cached under `.cache/research/upstream/`. A clone also needs `git lfs pull` to obtain the derived artifacts/extracted members.
32
 
33
  Generated JSON/JSONL and compressed evidence are regenerated in place; keep prose notes in Markdown. Rerunning the four analysis stages reproduced every generated report and gzip artifact byte-for-byte on the same toolchain. gzip timestamps and stored filenames are fixed. Different objdump versions can produce different disassembly text, so tool version and uncompressed evidence hashes are recorded. Reports are UTF-8 normal Git text, with individual files below the storage policy's 2 MiB limit.
34
 
docs/research/deferred-auth.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_binaries.py CHANGED
@@ -10,7 +10,8 @@ def main():
10
  raise SystemExit('Run from the repository root')
11
  for script in ('fetch_analysis_references.py', 'analyze_qvm.py', 'analyze_native.py',
12
  'analyze_vm_abi.py', 'analyze_native_dispatch.py', 'analyze_vm_extensions.py',
13
- 'analyze_native_metadata.py', 'analyze_native_coverage.py', 'analyze_auth_drop.py'):
 
14
  subprocess.run([sys.executable, str(Path('scripts')/script)], check=True)
15
 
16
 
 
10
  raise SystemExit('Run from the repository root')
11
  for script in ('fetch_analysis_references.py', 'analyze_qvm.py', 'analyze_native.py',
12
  'analyze_vm_abi.py', 'analyze_native_dispatch.py', 'analyze_vm_extensions.py',
13
+ 'analyze_native_metadata.py', 'analyze_native_coverage.py', 'analyze_auth_drop.py',
14
+ 'analyze_deferred_auth.py'):
15
  subprocess.run([sys.executable, str(Path('scripts')/script)], check=True)
16
 
17
 
scripts/analyze_deferred_auth.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Static references and bounded synthetic-memory traces for deferred auth drops."""
3
+ import gzip
4
+ import json
5
+ from pathlib import Path
6
+ from analyze_qvm import decode, write_json
7
+ from analyze_vm_extensions import verified, native_functions
8
+ from analyze_auth_drop import ascii_literal
9
+ from analyze_native_dispatch import recover
10
+
11
+
12
+ def signed32(value):
13
+ value &= 0xffffffff
14
+ return value-0x100000000 if value & 0x80000000 else value
15
+
16
+
17
+ def trace_slice(instructions, start, stop, memory, locals_map, limit=256):
18
+ """Evaluate only a selected straight/bounded slice against synthetic memory.
19
+
20
+ Record calls rather than entering callees. Reject unknown reads/opcodes and
21
+ control flow outside the selected slice. This is not a QVM runtime.
22
+ """
23
+ stack, args, calls, trace, byte_writes = [], {}, [], [], []
24
+ memory = dict(memory)
25
+ local_base = 0x40000000
26
+ for offset, value in locals_map.items():
27
+ memory[local_base+offset] = value
28
+ pc = start
29
+ while pc != stop:
30
+ if not start <= pc < stop or len(trace) >= limit:
31
+ raise ValueError('slice control flow escaped bound')
32
+ ins = instructions[pc]
33
+ op, value = ins['opcode'], ins['operand']
34
+ trace.append(pc)
35
+ pc += 1
36
+ if op == 'CONST':
37
+ stack.append(value)
38
+ elif op == 'LOCAL':
39
+ stack.append(local_base+value)
40
+ elif op == 'LOAD4':
41
+ address = stack.pop()
42
+ if address not in memory:
43
+ raise ValueError('unknown synthetic memory read')
44
+ stack.append(memory[address])
45
+ elif op == 'STORE4':
46
+ value, address = stack.pop(), stack.pop()
47
+ memory[address] = signed32(value)
48
+ elif op == 'STORE1':
49
+ value, address = stack.pop(), stack.pop()
50
+ byte_writes.append({'address': address, 'value': value & 255})
51
+ elif op in ('ADD', 'SUB', 'MULI'):
52
+ right, left = stack.pop(), stack.pop()
53
+ stack.append(signed32(left+right if op == 'ADD' else left-right if op == 'SUB' else left*right))
54
+ elif op in ('EQ', 'NE', 'LTI', 'LEI', 'GTI', 'GEI'):
55
+ right, left = signed32(stack.pop()), signed32(stack.pop())
56
+ condition = {'EQ': left == right, 'NE': left != right, 'LTI': left < right,
57
+ 'LEI': left <= right, 'GTI': left > right, 'GEI': left >= right}[op]
58
+ if condition:
59
+ pc = value
60
+ elif op == 'ARG':
61
+ args[value] = stack.pop()
62
+ elif op == 'CALL':
63
+ calls.append({'instruction': ins['index'], 'target': stack.pop(), 'args': dict(args)})
64
+ args = {}
65
+ stack.append(0) # synthetic placeholder, not a recovered callee result
66
+ elif op == 'POP':
67
+ stack.pop()
68
+ elif op == 'JUMP':
69
+ pc = stack.pop()
70
+ else:
71
+ raise ValueError(f'unsupported slice opcode {op}')
72
+ return {'trace': trace, 'calls': calls, 'memory': memory, 'byte_writes': byte_writes}
73
+
74
+
75
+ def partition_function(instructions, entry, data, header):
76
+ if instructions[entry]['opcode'] != 'ENTER':
77
+ raise ValueError('expected function entry')
78
+ end = next(i for i in range(entry+1, len(instructions)) if instructions[i]['opcode'] == 'ENTER')
79
+ body = instructions[entry:end]
80
+ literals = []
81
+ for i in body:
82
+ if i['opcode'] == 'CONST' and instructions[i['index']+1]['opcode'] == 'ARG' and 0 <= i['operand'] < header['data_length']+header['literal_length']:
83
+ value = ascii_literal(data, header['data_offset']+i['operand'], header['data_offset']+header['data_length']+header['literal_length'])
84
+ if value:
85
+ literals.append({'instruction': i['index'], 'vm_address': i['operand'], 'text': value})
86
+ return {'entry': entry, 'linear_end_exclusive': end, 'instructions': body, 'literal_argument_candidates': literals}
87
+
88
+
89
+ def main():
90
+ report = json.loads(Path('docs/research/qvm/qagame.json').read_text())
91
+ data = verified(Path(report['source']['member']['output']).read_bytes(), report['sha256'])
92
+ header, instructions = decode(data)
93
+ entries = [2319, 5096, 69338, 15186, 165818, 173979, 174399]
94
+ functions = {str(e): partition_function(instructions, e, data, header) for e in entries}
95
+ refs = []
96
+ for i in instructions:
97
+ if i['opcode'] == 'CONST' and i['operand'] == 3588:
98
+ entry = next(j for j in range(i['index'], -1, -1) if instructions[j]['opcode'] == 'ENTER')
99
+ refs.append({'instruction': i['index'], 'containing_entry': entry,
100
+ 'window': instructions[max(entry, i['index']-4):i['index']+9]})
101
+ callers = {str(e): [i['index'] for i in instructions if i['opcode'] == 'CALL' and i['index'] and
102
+ instructions[i['index']-1]['opcode'] == 'CONST' and instructions[i['index']-1]['operand'] == e]
103
+ for e in entries}
104
+ synthetic = []
105
+ record = 0x10000
106
+ for state, pending, now in [(2, 1000, 1100), (2, 1000, 1101), (1, 1000, 1101),
107
+ (2, 0, 1101), (2, 1, 102), (2, 0x7ffffff0, -0x7fffff80)]:
108
+ model = trace_slice(instructions, 15878, 15922,
109
+ {record+468: state, record+3588: pending, 34992532: now}, {32: record, 28: 3})
110
+ synthetic.append({'state': state, 'pending': pending, 'now': now,
111
+ 'signed_elapsed': signed32(now-pending), 'trace': model['trace'], 'calls': model['calls']})
112
+ native = {}
113
+ for label in ('linux--Quake3-UrT-Ded.x86_64', 'linux--Quake3-UrT.x86_64'):
114
+ r = json.loads(Path(f'docs/research/native/{label}.json').read_text())
115
+ original = verified((Path('.cache/UrbanTerror43')/r['source']['path']).read_bytes(), r['source']['sha256'])
116
+ artifact = r['artifacts']['disassembly.txt']
117
+ text = verified(gzip.open(artifact['path'], 'rb').read(), artifact['uncompressed_sha256']).decode()
118
+ dispatch = recover(original, text, 'SV_GameSystemCalls')
119
+ native[label] = {'source': r['source'], 'disassembly_sha256': artifact['uncompressed_sha256'],
120
+ 'strncpy_case': next(c for c in dispatch['cases'] if 102 in c['numbers_sharing_target']),
121
+ 'error_case': next(c for c in dispatch['cases'] if 1 in c['numbers_sharing_target']),
122
+ 'error_function': native_functions(text)['Com_Error']}
123
+ write_json(Path('docs/research/deferred-auth.json'), {'source': report['source'], 'sha256': report['sha256'],
124
+ 'functions': functions, 'direct_call_candidates': callers, 'field_constant_references': refs,
125
+ 'synthetic_slice_traces': synthetic, 'native': native, 'limitations':
126
+ 'Constant-offset references do not cover aliases or dynamically computed offsets. '
127
+ 'Synthetic-memory traces evaluate selected QVM instructions without entering callees; '
128
+ 'these are static boundary checks, not real gameplay/time/network fixtures or a full QVM runtime.'})
129
+ print('Recovered eight field references, copy/error helpers and deferred timing boundary traces')
130
+
131
+
132
+ if __name__ == '__main__':
133
+ main()
scripts/test_deferred_auth.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Static instruction-slice boundary checks with synthetic memory, no callees."""
3
+ import json
4
+ from pathlib import Path
5
+ import unittest
6
+ from analyze_qvm import decode
7
+ from analyze_deferred_auth import trace_slice, signed32
8
+
9
+
10
+ class DeferredTests(unittest.TestCase):
11
+ @classmethod
12
+ def setUpClass(cls):
13
+ cls.report = json.loads(Path('docs/research/deferred-auth.json').read_text())
14
+ _, cls.instructions = decode(Path(cls.report['source']['member']['output']).read_bytes())
15
+ cls.record = 0x10000
16
+
17
+ def frame(self, state, pending, now):
18
+ return trace_slice(self.instructions, 15878, 15922,
19
+ {self.record+468: state, self.record+3588: pending, 34992532: now},
20
+ {32: self.record, 28: 3})
21
+
22
+ def test_strict_delay_boundary(self):
23
+ for elapsed, count in [(0, 0), (99, 0), (100, 0), (101, 1), (102, 1), (-1, 0)]:
24
+ with self.subTest(elapsed=elapsed):
25
+ calls = self.frame(2, 1000, 1000+elapsed)['calls']
26
+ self.assertEqual(len(calls), count)
27
+ if calls:
28
+ self.assertEqual(calls[0]['target'], 165818)
29
+ self.assertEqual(calls[0]['args'], {8: 3, 12: self.record+3204, 16: self.record+3332})
30
+
31
+ def test_state_pending_and_signed_wrap_gates(self):
32
+ self.assertEqual(self.frame(1, 1000, 2000)['calls'], [])
33
+ self.assertEqual(self.frame(2, 0, 2000)['calls'], [])
34
+ self.assertEqual(self.frame(2, -1, 2000)['calls'], [])
35
+ self.assertEqual(len(self.frame(2, 0x7ffffff0, -0x7fffff80)['calls']), 1)
36
+ self.assertEqual(signed32(0x80000000), -0x80000000)
37
+
38
+ def test_begin_replaces_only_sentinel_when_auth_enabled(self):
39
+ for enabled, pending, expected in [(1, 1, 1234), (1, 0, 0), (1, 100, 100), (0, 1, 1)]:
40
+ with self.subTest(enabled=enabled, pending=pending):
41
+ r = trace_slice(self.instructions, 174979, 175027,
42
+ {35017616: enabled, self.record+3588: pending, 34992532: 1234,
43
+ self.record+3200: 0}, {24: self.record})
44
+ self.assertEqual(r['memory'][self.record+3588], expected)
45
+
46
+ def test_copy_request_and_terminator_boundaries(self):
47
+ for capacity in [1, 2, 128, 256]:
48
+ with self.subTest(capacity=capacity):
49
+ r = trace_slice(self.instructions, 69339, 69384, {}, {28: 0x20000, 32: 0x30000, 36: capacity})
50
+ self.assertEqual(r['calls'], [{'instruction': 69373, 'target': -103,
51
+ 'args': {8: 0x20000, 12: 0x30000, 16: capacity-1}}])
52
+ self.assertEqual(r['byte_writes'], [{'address': 0x20000+capacity-1, 'value': 0}])
53
+
54
+ def test_bad_copy_inputs_reach_diagnostic_not_print_trap(self):
55
+ for source, capacity in [(0, 128), (0x30000, 0)]:
56
+ r = trace_slice(self.instructions, 69339, 69361, {}, {32: source, 36: capacity})
57
+ self.assertEqual(len(r['calls']), 1)
58
+ self.assertEqual(r['calls'][0]['target'], 5096)
59
+ for n in self.report['native'].values():
60
+ self.assertIn('Com_Error', n['error_case']['direct_call_or_jump_labels'])
61
+ self.assertIn('strncpy', [name.split('@')[0] for name in n['strncpy_case']['direct_call_or_jump_labels']])
62
+ body = self.report['functions']['2319']['instructions']
63
+ self.assertTrue(any(body[i-1]['operand'] == -2 and x['opcode'] == 'CALL' for i,x in enumerate(body) if i))
64
+
65
+ def test_unknown_reads_opcodes_and_escaped_branches_rejected(self):
66
+ with self.assertRaises(ValueError):
67
+ trace_slice(self.instructions, 15878, 15922, {}, {32: self.record})
68
+ with self.assertRaises(ValueError):
69
+ trace_slice(self.instructions, 69338, 69384, {}, {})
70
+ malformed = [{'index': 0, 'opcode': 'CONST', 'operand': 100}, {'index': 1, 'opcode': 'JUMP', 'operand': None}]
71
+ with self.assertRaises(ValueError):
72
+ trace_slice(malformed, 0, 2, {}, {})
73
+
74
+
75
+ if __name__ == '__main__':
76
+ unittest.main()