nmarshall commited on
Commit
66f51f1
·
1 Parent(s): 9ac939d

Recover selector parsing bounds and dispatcher entry routes

Browse files
docs/FINDINGS.md CHANGED
@@ -242,3 +242,9 @@ Bounded qagame paths recover bomb-plant/inactivity correction requests, direct f
242
  **F-051 — Observed query command policy/routes:** role checker 167097 requires mode-positive, a caller, nonempty command, state exactly 2 and roles 2–8. Friend/Member/Referee search their configured command strings as substrings; roles 5–8 reject exact public and accept other compared text. Two direct auth-whois routes forward client/tokenized-selector or zero/argv-1; only the visible client route invokes the role checker. Native entry/access controls and end-to-end permission behavior remain open. [QUERY-002](research/AUTH_QUERIES.md).
243
 
244
  **F-052 — Observed internal authentication mode:** gate 35017616 is separate from the registered auth mirror. Eight literal stores in entry 171 select zero/public/private/notoriety paths after engine, dedicated and feature prerequisites. Nonempty groups take precedence over notoriety. Address-resolution failure clears the gate and requests Off (no authserver) without a visible auth=0 request in that tail. Scheduling, alias writes and native configuration lifecycle remain open. [QUERY-003](research/AUTH_QUERIES.md).
 
 
 
 
 
 
 
242
  **F-051 — Observed query command policy/routes:** role checker 167097 requires mode-positive, a caller, nonempty command, state exactly 2 and roles 2–8. Friend/Member/Referee search their configured command strings as substrings; roles 5–8 reject exact public and accept other compared text. Two direct auth-whois routes forward client/tokenized-selector or zero/argv-1; only the visible client route invokes the role checker. Native entry/access controls and end-to-end permission behavior remain open. [QUERY-002](research/AUTH_QUERIES.md).
243
 
244
  **F-052 — Observed internal authentication mode:** gate 35017616 is separate from the registered auth mirror. Eight literal stores in entry 171 select zero/public/private/notoriety paths after engine, dedicated and feature prerequisites. Nonempty groups take precedence over notoriety. Address-resolution failure clears the gate and requests Off (no authserver) without a visible auth=0 request in that tail. Scheduling, alias writes and native configuration lifecycle remain open. [QUERY-003](research/AUTH_QUERIES.md).
245
+
246
+ **F-053 — Observed parser instructions and bounded wrap:** qagame integer helper 37159 skips signed bytes at most 32, consumes one sign, and accumulates digits until a nondigit without a visible overflow guard. Bounded 32-bit traces produce 0/3 from 4294967296/4294967299, and composed digit/parser/bounds slices accept those results with count four. Original interpreter/JIT behavior remains unverified. [BND-001](research/SELECTOR_BOUNDS.md).
247
+
248
+ **F-054 — Observed selector storage/count invariants:** name copy passes capacity 64, yielding trap copy count 63 and an explicit NUL at offset 63. Match pointers occupy the inferred 64-entry local region; a synthetic 65th insertion reaches local 352. QVM count 34992524 comes from sv_maxclients's integer mirror and record storage clears 64*6336 bytes. Both Linux ELF64 startup graphs request native correction into 1–64, but complete cvar/restart/mutation invariants remain open. [BND-002](research/SELECTOR_BOUNDS.md).
249
+
250
+ **F-055 — Observed dispatcher/chat mode separation:** native VM commands 0/6/9 connect to qagame initialization/client/console routes. The client dispatcher calls chat entry 187911 for say/say_team/me with modes 0/1/4; modes 1/4 bypass the later slash/auth dispatch. Native SV_GameCommand requires server state 2 before command nine. Full native filtering, console/rcon access and nonzero chat behavior remain open. [BND-003](research/SELECTOR_BOUNDS.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -33,7 +33,7 @@ For implementation items, the minimum done condition is a documented contract, a
33
 
34
  ### First research batch
35
 
36
- Progress: [RE-232–234 / TIME-001–003](research/FRAME_TIMING.md) now cover Linux ELF64 elapsed modification, event sources and pause predicates. The [RE-237–239 follow-up](research/EVENT_QUEUES.md) resolves the pause-field address/bot chain and adds queue/journal evidence. The [RE-241–243 announcement pass](research/AUTH_ANNOUNCEMENTS.md) recovers label tables and recipient paths; The [RE-245–247 cvar pass](research/GAME_CVARS.md) adds all 126 main registration rows and resolves auth_verbosity/callers. The [RE-249–251 change pass](research/CVAR_CHANGES.md) adds cache/notification behavior, four numeric correction branches and direct registration coverage. The [RE-253–255 query pass](research/AUTH_QUERIES.md) adds selector helpers, command-role routing and internal authentication modes. PROV-002 remains open for full output/reset coverage. PROV-001 remains open for complete scheduling and platform coverage.
37
 
38
  Start with the open questions raised by the current binary evidence. These tasks depend on the linked reports, not on a working rewrite.
39
 
 
33
 
34
  ### First research batch
35
 
36
+ Progress: [RE-232–234 / TIME-001–003](research/FRAME_TIMING.md) now cover Linux ELF64 elapsed modification, event sources and pause predicates. The [RE-237–239 follow-up](research/EVENT_QUEUES.md) resolves the pause-field address/bot chain and adds queue/journal evidence. The [RE-241–243 announcement pass](research/AUTH_ANNOUNCEMENTS.md) recovers label tables and recipient paths; The [RE-245–247 cvar pass](research/GAME_CVARS.md) adds all 126 main registration rows and resolves auth_verbosity/callers. The [RE-249–251 change pass](research/CVAR_CHANGES.md) adds cache/notification behavior, four numeric correction branches and direct registration coverage. The [RE-253–255 query pass](research/AUTH_QUERIES.md) adds selector helpers, command-role routing and internal authentication modes. The [RE-257–259 bounds/entry pass](research/SELECTOR_BOUNDS.md) adds integer parsing, selector storage/count evidence and native/QVM/chat route connections. PROV-002 remains open for full output/reset coverage. PROV-001 remains open for complete scheduling and platform coverage.
37
 
38
  Start with the open questions raised by the current binary evidence. These tasks depend on the linked reports, not on a working rewrite.
39
 
docs/RESEARCH_LEDGER.md CHANGED
@@ -117,3 +117,7 @@ Completed bounded milestones RE-249–251 with `analyze_cvar_changes.py`: cache
117
  ## 2026-10-11 — Authentication selectors, command policy and internal mode
118
 
119
  Completed bounded milestones RE-253–255 with `analyze_auth_queries.py`: numeric/name selector branches, actual cleanup/substring byte loops, role jump table/preconditions, two query dispatch routes, 24 mode references/eight literal writers and disabled/public/private/notoriety/address-failure tails. QUERY-001–003/F-050–052 distinguish substring policy from actual command recognition and internal mode from the auth cvar. Eight tests cover these boundaries plus malformed/missing byte reads and step limits. Extended the bounded evaluator with optional isolated byte memory, LOAD1/SEX8/NEGI and in-place byte writes while preserving existing report schemas when byte memory is absent. RE-248 remains open; RE-256 records parser/copy/match bounds, mode lifetime, trust and end-to-end runtime work. No original game/full QVM or recorded callee was executed.
 
 
 
 
 
117
  ## 2026-10-11 — Authentication selectors, command policy and internal mode
118
 
119
  Completed bounded milestones RE-253–255 with `analyze_auth_queries.py`: numeric/name selector branches, actual cleanup/substring byte loops, role jump table/preconditions, two query dispatch routes, 24 mode references/eight literal writers and disabled/public/private/notoriety/address-failure tails. QUERY-001–003/F-050–052 distinguish substring policy from actual command recognition and internal mode from the auth cvar. Eight tests cover these boundaries plus malformed/missing byte reads and step limits. Extended the bounded evaluator with optional isolated byte memory, LOAD1/SEX8/NEGI and in-place byte writes while preserving existing report schemas when byte memory is absent. RE-248 remains open; RE-256 records parser/copy/match bounds, mode lifetime, trust and end-to-end runtime work. No original game/full QVM or recorded callee was executed.
120
+
121
+ ## 2026-10-11 — Selector parsing/storage and native entry routes
122
+
123
+ Completed bounded milestones RE-257–259 with `analyze_selector_bounds.py`: 18 actual integer-helper traces, digit/parser/bounds compositions, name-copy calls and explicitly modeled truncation/cleanup compositions, match-list insertion boundaries, sv_maxclients initialization/109 literal references, QVM entry tables, chat argument/mode slices and ten native graphs. BND-001–003/F-053–055 distinguish evaluator arithmetic from interpreter/JIT conformance and normal count evidence from a complete safety invariant. Eight tests include independent modular arithmetic, byte/step/LEAVE bounds and cross-layer call evidence. The evaluator adds opt-in operand-stack capture and bounded LEAVE stopping without implementing VM frame unwinding. RE-256 remains open; RE-260 records arithmetic, count lifetime, filtering and chat-mode runtime work. No original process/full QVM/native libc call was executed.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -302,3 +302,8 @@ Purpose: turn the supplied installation into an evidence-backed specification of
302
  - [x] RE-254 Recover command-check role jump table/preconditions and both direct auth-whois dispatch routes with bounded argument traces ([QUERY-002](research/AUTH_QUERIES.md)).
303
  - [x] RE-255 Index internal authentication mode 35017616 literal references/writers and recover disabled/public/private/notoriety selection and address-failure tail ([QUERY-003](research/AUTH_QUERIES.md)).
304
  - [ ] RE-256 Recover selector integer conversion/overflow, name-copy/match-list bounds, full query/permission fixtures, mode scheduling/aliases and native console/network trust controls.
 
 
 
 
 
 
302
  - [x] RE-254 Recover command-check role jump table/preconditions and both direct auth-whois dispatch routes with bounded argument traces ([QUERY-002](research/AUTH_QUERIES.md)).
303
  - [x] RE-255 Index internal authentication mode 35017616 literal references/writers and recover disabled/public/private/notoriety selection and address-failure tail ([QUERY-003](research/AUTH_QUERIES.md)).
304
  - [ ] RE-256 Recover selector integer conversion/overflow, name-copy/match-list bounds, full query/permission fixtures, mode scheduling/aliases and native console/network trust controls.
305
+
306
+ - [x] RE-257 Recover selector decimal parser branches and bounded 32-bit overflow/slot-bound composition fixtures ([BND-001](research/SELECTOR_BOUNDS.md)).
307
+ - [x] RE-258 Recover selector name-copy capacity, match insertion layout, sv_maxclients count source and Linux ELF64 startup-bound evidence ([BND-002](research/SELECTOR_BOUNDS.md)).
308
+ - [x] RE-259 Connect native VM commands 0/6/9 to initialization/client/console dispatch and recover say/say_team/me argument and auth-path mode separation ([BND-003](research/SELECTOR_BOUNDS.md)).
309
+ - [ ] RE-260 Validate original interpreter/JIT arithmetic and full selector/copy behavior, every maxclients startup/restart/mutation invariant, native command admission/filtering and nonzero chat-mode formatting/recipients.
docs/research/AUTH_QUERIES.md CHANGED
@@ -62,3 +62,5 @@ One direct call to entry 171 was observed at instruction 5042 in function 3877;
62
  ## Completion and remaining work
63
 
64
  RE-253–255 cover the bounded selector/helper evidence, role table/query dispatch routes, and internal-mode writers/selection paths. RE-248 remains open for complete registrations/native cvar policy and complete selector/permission behavior. RE-256 tracks numeric conversion/overflow, name-copy and match-list bounds, end-to-end query/permission fixtures, mode scheduling/aliases and native console/network trust controls. No Rust runtime implementation or original-game conformance is claimed.
 
 
 
62
  ## Completion and remaining work
63
 
64
  RE-253–255 cover the bounded selector/helper evidence, role table/query dispatch routes, and internal-mode writers/selection paths. RE-248 remains open for complete registrations/native cvar policy and complete selector/permission behavior. RE-256 tracks numeric conversion/overflow, name-copy and match-list bounds, end-to-end query/permission fixtures, mode scheduling/aliases and native console/network trust controls. No Rust runtime implementation or original-game conformance is claimed.
65
+
66
+ Follow-up: [BND-001–003](SELECTOR_BOUNDS.md) recovers the integer helper and bounded wrap/slot compositions, name-copy/count/layout boundaries, and native/QVM dispatcher entry chain. It also establishes that only say mode zero reaches the chat handler's slash/auth path; say_team and me modes bypass it. Original-runtime arithmetic, complete copy/match behavior and native admission/access controls remain open.
docs/research/README.md CHANGED
@@ -34,10 +34,11 @@ python3 scripts/test_auth_announcements.py
34
  python3 scripts/test_game_cvars.py
35
  python3 scripts/test_cvar_changes.py
36
  python3 scripts/test_auth_queries.py
 
37
  python3 scripts/storage_policy.py check --require-setup
38
  ```
39
 
40
- 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`, `analyze_deferred_auth.py`, `analyze_engine_control.py`, `analyze_frame_timing.py`, `analyze_event_queues.py`, `analyze_auth_announcements.py`, `analyze_game_cvars.py`, `analyze_cvar_changes.py`, and `analyze_auth_queries.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.
41
 
42
  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.
43
 
@@ -73,3 +74,5 @@ The extension collector adds five focused tests for native tail calls, command-r
73
  [Cvar changes, limits and extra registrations](CVAR_CHANGES.md) adds RE-249–251: row-word/cache behavior, four setting correction paths, 28 direct registration sites and 23 exact constant argument traces. Reproduce with `python3 scripts/analyze_cvar_changes.py`; check with `python3 scripts/test_cvar_changes.py`.
74
 
75
  [Authentication selectors, command policy and internal mode](AUTH_QUERIES.md) adds RE-253–255: slot/name helper evidence, role routing and query dispatch, and internal-mode assignment/disable paths. Reproduce with `python3 scripts/analyze_auth_queries.py`; check with `python3 scripts/test_auth_queries.py`.
 
 
 
34
  python3 scripts/test_game_cvars.py
35
  python3 scripts/test_cvar_changes.py
36
  python3 scripts/test_auth_queries.py
37
+ python3 scripts/test_selector_bounds.py
38
  python3 scripts/storage_policy.py check --require-setup
39
  ```
40
 
41
+ 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`, `analyze_deferred_auth.py`, `analyze_engine_control.py`, `analyze_frame_timing.py`, `analyze_event_queues.py`, `analyze_auth_announcements.py`, `analyze_game_cvars.py`, `analyze_cvar_changes.py`, `analyze_auth_queries.py`, and `analyze_selector_bounds.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.
42
 
43
  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.
44
 
 
74
  [Cvar changes, limits and extra registrations](CVAR_CHANGES.md) adds RE-249–251: row-word/cache behavior, four setting correction paths, 28 direct registration sites and 23 exact constant argument traces. Reproduce with `python3 scripts/analyze_cvar_changes.py`; check with `python3 scripts/test_cvar_changes.py`.
75
 
76
  [Authentication selectors, command policy and internal mode](AUTH_QUERIES.md) adds RE-253–255: slot/name helper evidence, role routing and query dispatch, and internal-mode assignment/disable paths. Reproduce with `python3 scripts/analyze_auth_queries.py`; check with `python3 scripts/test_auth_queries.py`.
77
+
78
+ [Selector conversion, storage and entry routes](SELECTOR_BOUNDS.md) adds RE-257–259: integer overflow/slot compositions, copy/match/count bounds and native/QVM/chat dispatcher connections. Reproduce with `python3 scripts/analyze_selector_bounds.py`; check with `python3 scripts/test_selector_bounds.py`.
docs/research/SELECTOR_BOUNDS.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Selector integer conversion, storage bounds and dispatcher entry routes
2
+
3
+ Static pass, 2026-10-11. Reproduce with `python3 scripts/analyze_selector_bounds.py`; check with `python3 scripts/test_selector_bounds.py`. [Generated evidence](selector-bounds.json) retains verified QVM/native provenance, 18 integer instruction traces, composed digit/parser/slot-bound fixtures, name-copy call bounds, match insertion boundaries, dispatcher arguments and ten Linux ELF64 function graphs. This extends [authentication queries](AUTH_QUERIES.md). No original process or complete QVM runs; all CALLs remain recorded with zero placeholder results. The bounded evaluator can now stop explicitly at a helper's LEAVE instruction and capture its operand stack without implementing a real VM frame return.
4
+
5
+ ## BND-001: decimal parser and wrap behavior
6
+
7
+ Entry **37159** reads signed bytes through LOAD1/SEX8. It skips leading bytes whose signed value is at most **32**, returning zero if it encounters NUL. Consequently bytes 128–255 also satisfy its leading-skip comparison: the standalone parser fixture `ff 31 32` returns 12. This is separate from the selector's digit classifier, which sends such a string to name handling rather than numeric parsing.
8
+
9
+ After leading skipping, the parser consumes one optional `+` or `-`, initializes an accumulator to zero, and consumes digits until the first nondigit. Each digit performs **accumulator * 10 + digit** through MULI/ADD, and the result is multiplied by the sign. There is no visible range/overflow check, decimal-point handling or trailing-input validation.
10
+
11
+ | Input | Result in the bounded 32-bit evaluator |
12
+ | --- | --- |
13
+ | Empty / whitespace / `junk` | 0 |
14
+ | `01`, `+12`, `-12junk`, `12.5` | 1, 12, -12, 12 respectively |
15
+ | `2147483647` / `2147483648` | 2147483647 / -2147483648 |
16
+ | `4294967295` / `4294967296` / `4294967299` | -1 / 0 / 3 |
17
+ | `-2147483648` / `-2147483649` | -2147483648 / 2147483647 |
18
+
19
+ The tests also compare actual instruction-loop results against independently computed modulo-2^32 arithmetic at multiple boundaries. This establishes the bounded evaluator's interpretation of the supplied instructions, not original interpreter/JIT runtime conformance. Original execution modes and cross-platform arithmetic remain to be checked.
20
+
21
+ Composed fixtures feed digit-only strings through the selector's actual classifier, this parser and its actual signed slot-bounds slice with a supplied count of four. `4294967296` and `4294967299` then pass as slots zero and three; `4294967295` and `2147483648` fail as negative slots. These are separate slices whose results are explicitly connected, not execution of the whole selector or a native command. The Rust selector contract must decide this compatibility behavior based on an original-runtime fixture rather than assuming a checked integer parse behaves identically.
22
+
23
+ ## BND-002: name copy and match-list capacity
24
+
25
+ Selector instructions **253692–253703** call copy helper 69338 with local destination **24**, source `record + 504` and capacity **64**. The [previous copy-helper recovery](DEFERRED_AUTH.md) identifies game trap 102 / native strncpy. The helper's positive-capacity slice supplies native copy count **63** and explicitly writes NUL at destination offset **63**. Source-null and capacity-below-one branches invoke the existing error helper; they are not treated as returning safely here.
26
+
27
+ The report composes a **modeled** first-63-byte copy with the actual cleaning instructions. Native libc is not invoked and padding/overlap/error behavior is not newly verified. Under that copy contract, 63 `A` bytes, 64 `A` bytes and 63 `A` bytes followed by `X` yield the same cleaned name. Color escapes also consume space before cleanup: a 64-byte source beginning with `^1` has only its first 63 bytes available for cleaning. Complete selection of those colliding names still depends on exact-match precedence and the selector text. The report does not claim a whole-selector collision test has run.
28
+
29
+ The name selector stores partial-match pointers at **local 96 + match_count * 4** and increments match_count. Local 352 is the next recovered scalar temporary, so local offsets **96 through 348** form a **256-byte / 64-pointer** region. Bounded insertion fixtures show:
30
+
31
+ | Prior match count | Written local offset | New count |
32
+ | --- | --- | --- |
33
+ | 0 | 96 | 1 |
34
+ | 63 | 348 | 64 |
35
+ | 64 | 352 | 65 |
36
+
37
+ There is no explicit match-count bound in this insertion slice. The last fixture deliberately violates the intended count/layout invariant; it shows the 65th insertion crossing into local 352, not a demonstrated original-runtime overflow or a complete account of its effects. The surrounding name loop contributes at most one partial entry per visited slot and exits immediately on an exact match. A client count at most 64 therefore fits this region, subject to normal loop/state invariants.
38
+
39
+ The loop count at **34992524** is initialized at instruction **4220** from the integer mirror **34089604**, identified as `sv_maxclients` (main registry default `12`, raw flags 37). The same initialization region clears **405504 = 64 * 6336** bytes at VM address 101536 and sets the record-base global to that address. The QVM count-copy slice contains no clamp. All **109 literal references** to the count address are indexed; the collector does not claim alias/bulk-write coverage.
40
+
41
+ Both Linux ELF64 `SV_Startup` graphs show the normal native startup correction path: integer values at most zero request 1, values above **64** request 64, then the cvar integer is reloaded for allocation. `SV_BoundMaxClients` similarly accepts a lower-bound argument and uses upper comparison 64. Native Cvar_Set timing/merge semantics, alternate startup/restart paths, dynamic max-client changes and maliciously inconsistent VM/native state are not verified here. Thus native startup evidence supports the normal 64-client invariant but does not prove that every selector invocation has a safe count.
42
+
43
+ ## BND-003: native/QVM/chat entry chain
44
+
45
+ Initialized QVM entry table **3536** maps command 0 to instruction **21**, command 6 to **105**, and command 9 to **125**. Retained entry windows establish these routes:
46
+
47
+ - **Initialization:** case 0 forwards its three arguments to entry 3877. The native `SV_InitGameProgs` tail calls VM command zero after VM creation and time retrieval. This connects the count-copy initialization above and the previously recovered call to internal auth-mode setup at instruction 5042 to the native game initialization route; restart/re-entry scheduling is still open.
48
+ - **Client commands:** case 6 forwards its first argument to entry **204035**. The native `SV_ExecuteClientCommand` tail calls VM command six, deriving a native client slot from its client pointer. Entry 204035 uses that slot to address **34091368 + slot * 880**, checks object offset 520 for a nonzero record pointer and retrieves command argv 0. Complete native admission/flood/command filtering is retained in its graph but not fully specified by this pass.
49
+ - **Console commands:** case 9 calls **262012** and returns its result. Native `SV_GameCommand` returns zero unless native server state equals **2**; on that path it invokes VM command nine. This establishes a server-state gate and the console-dispatch entry, not native rcon authentication or shell/console access policy.
50
+
51
+ Entry 204035 compares command names against **say**, **say_team** and **me**, passing its object to chat handler **187911** with raw second arguments **0**, **1** and **4**, respectively, and a zero third argument. Three actual argument slices are retained.
52
+
53
+ Critically, the chat handler's entry branch sends **nonzero** second arguments to helper 180858 and jumps to its common send path at 189262. Only second argument **zero** enters the later single-slash/auth command path. Therefore the previously recovered client auth-whois route is connected to **say** processing; the existence of say_team/me calls to the same function does not prove those channels expose the same query dispatch. The nonzero-mode helper and eventual chat formatting/recipient semantics remain open.
54
+
55
+ Direct scanning finds exactly the five adjacent call sites linking these three dispatcher targets: two in QVM entry 0 and three in entry 204035. Computed calls and native-to-VM transfers are separate evidence and are not counted as those adjacent QVM calls.
56
+
57
+ ## Completion and remaining work
58
+
59
+ RE-257–259 cover parser/slot composition, selector storage/count-source boundaries and cross-layer dispatcher routes. RE-256 remains open for complete parser/copy/match/runtime conformance and mode/trust behavior. RE-260 tracks interpreter/JIT arithmetic comparison, complete maxclients startup/restart/mutation invariants, native client-command filtering and nonzero chat-mode behavior. The rewrite still needs safe ownership/limits and evidence-backed compatibility contracts before implementation.
docs/research/selector-bounds.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_binaries.py CHANGED
@@ -13,7 +13,7 @@ def main():
13
  'analyze_native_metadata.py', 'analyze_native_coverage.py', 'analyze_auth_drop.py',
14
  'analyze_deferred_auth.py', 'analyze_engine_control.py', 'analyze_frame_timing.py',
15
  'analyze_event_queues.py', 'analyze_auth_announcements.py', 'analyze_game_cvars.py',
16
- 'analyze_cvar_changes.py', 'analyze_auth_queries.py'):
17
  subprocess.run([sys.executable, str(Path('scripts')/script)], check=True)
18
 
19
 
 
13
  'analyze_native_metadata.py', 'analyze_native_coverage.py', 'analyze_auth_drop.py',
14
  'analyze_deferred_auth.py', 'analyze_engine_control.py', 'analyze_frame_timing.py',
15
  'analyze_event_queues.py', 'analyze_auth_announcements.py', 'analyze_game_cvars.py',
16
+ 'analyze_cvar_changes.py', 'analyze_auth_queries.py', 'analyze_selector_bounds.py'):
17
  subprocess.run([sys.executable, str(Path('scripts')/script)], check=True)
18
 
19
 
scripts/analyze_deferred_auth.py CHANGED
@@ -14,7 +14,8 @@ def signed32(value):
14
  return value-0x100000000 if value & 0x80000000 else value
15
 
16
 
17
- def trace_slice(instructions, start, stop, memory, locals_map, limit=256, byte_memory=None):
 
18
  """Evaluate only a selected straight/bounded slice against synthetic memory.
19
 
20
  Record calls rather than entering callees. Reject unknown reads/opcodes and
@@ -94,11 +95,15 @@ def trace_slice(instructions, start, stop, memory, locals_map, limit=256, byte_m
94
  stack.pop()
95
  elif op == 'JUMP':
96
  pc = stack.pop()
 
 
97
  else:
98
  raise ValueError(f'unsupported slice opcode {op}')
99
  result = {'trace': trace, 'calls': calls, 'memory': memory, 'byte_writes': byte_writes}
100
  if bytes_map is not None:
101
  result['byte_memory'] = bytes_map
 
 
102
  return result
103
 
104
 
 
14
  return value-0x100000000 if value & 0x80000000 else value
15
 
16
 
17
+ def trace_slice(instructions, start, stop, memory, locals_map, limit=256, byte_memory=None, capture_stack=False,
18
+ halt_on_leave=False):
19
  """Evaluate only a selected straight/bounded slice against synthetic memory.
20
 
21
  Record calls rather than entering callees. Reject unknown reads/opcodes and
 
95
  stack.pop()
96
  elif op == 'JUMP':
97
  pc = stack.pop()
98
+ elif op == 'LEAVE' and halt_on_leave:
99
+ break # bounded helper return boundary; no real VM frame is unwound
100
  else:
101
  raise ValueError(f'unsupported slice opcode {op}')
102
  result = {'trace': trace, 'calls': calls, 'memory': memory, 'byte_writes': byte_writes}
103
  if bytes_map is not None:
104
  result['byte_memory'] = bytes_map
105
+ if capture_stack:
106
+ result['stack'] = stack
107
  return result
108
 
109
 
scripts/analyze_selector_bounds.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Trace integer parsing, selector storage boundaries and dispatcher callers."""
3
+ import gzip
4
+ import json
5
+ from pathlib import Path
6
+ import struct
7
+
8
+ from analyze_auth_queries import byte_string, cleaned_name
9
+ from analyze_deferred_auth import partition_function, trace_slice
10
+ from analyze_engine_control import control_flow
11
+ from analyze_game_cvars import vm_string
12
+ from analyze_qvm import decode, write_json
13
+ from analyze_vm_extensions import verified, native_functions
14
+
15
+
16
+ def integer_trace(ins, value):
17
+ # Whitespace is signed SEX8 <= 32, including bytes 128..255. NUL ends it.
18
+ t = trace_slice(ins,37160,37277,{}, {44:0x10000},limit=10000,
19
+ byte_memory=byte_string(0x10000,value),capture_stack=True,halt_on_leave=True)
20
+ if len(t['stack']) != 1 or ins[t['trace'][-1]]['opcode'] != 'LEAVE':
21
+ raise ValueError('integer parser result stack changed')
22
+ return t
23
+
24
+
25
+ def main():
26
+ q=json.loads(Path('docs/research/qvm/qagame.json').read_text())
27
+ data=verified(Path(q['source']['member']['output']).read_bytes(),q['sha256'])
28
+ h,ins=decode(data)
29
+ registry=json.loads(Path('docs/research/game-cvars.json').read_text())
30
+ if registry['sha256']!=q['sha256']:
31
+ raise ValueError('registry provenance mismatch')
32
+ maxclients=next(r for r in registry['registry']['rows'] if r['name']=='sv_maxclients')
33
+ if maxclients['mirror_address']+12!=34089604:
34
+ raise ValueError('maxclients mirror changed')
35
+ inputs=[b'',b' ',b'\t\r\n',b'0',b'01',b'+12',b'-12junk',b'12.5',b'junk',b'\xff12',
36
+ b'2147483647',b'2147483648',b'4294967295',b'4294967296',b'4294967299',
37
+ b'-2147483648',b'-2147483649',b'99999999999999999999']
38
+ parsers=[{'input_hex':s.hex(),'trace':integer_trace(ins,s)} for s in inputs]
39
+ compositions=[]
40
+ for text in [b'4294967295',b'4294967296',b'4294967299',b'2147483648']:
41
+ p=integer_trace(ins,text);slot=p['stack'][0];valid=0<=slot<4
42
+ c=trace_slice(ins,253562,253605,{}, {376:0x10000},limit=2048,byte_memory=byte_string(0x10000,text))
43
+ b=trace_slice(ins,253612,253641 if valid else 253638,{34992524:4},{360:slot})
44
+ compositions.append({'input_hex':text.hex(),'parsed_slot':slot,'digit_trace':c,'bounds_trace':b})
45
+ copies=[]
46
+ for capacity in [1,64]:
47
+ t=trace_slice(ins,69361,69384,{}, {28:0x20000,32:0x30000,36:capacity},capture_stack=True)
48
+ copies.append({'capacity':capacity,'trace':t})
49
+ # This explicitly models the recovered strncpy contract; no native call executes.
50
+ names=[]
51
+ for raw in [b'Alpha',b'A'*63,b'A'*64,b'A'*63+b'X',b'^1'+b'A'*62]:
52
+ copied=raw[:63]
53
+ names.append({'source_hex':raw.hex(),'contract_copy_hex':copied.hex(),
54
+ 'cleaned':cleaned_name(ins,copied)})
55
+ insertions=[]
56
+ for count in [0,63,64]:
57
+ t=trace_slice(ins,253751,253771,{34992496:0x30000},{92:count,20:count})
58
+ insertions.append({'prior_match_count':count,'slot':count,'trace':t,
59
+ 'write_local_offset':96+count*4})
60
+ chat=[]
61
+ mode_branches=[]
62
+ for command,at,start,stop,mode in [('say',204128,204120,204130,0),
63
+ ('say_team',204154,204146,204156,1),('me',204180,204172,204182,4)]:
64
+ chat.append({'command':command,'call_instruction':at,'raw_second_arg':mode,
65
+ 'trace':trace_slice(ins,start,stop,{}, {1048:0x20000})})
66
+ mode_branches.append({'command':command,'mode':mode,
67
+ 'trace':trace_slice(ins,187929,187945 if mode==0 else 189262,{}, {3356:mode})})
68
+ entry_targets={str(n):struct.unpack_from('<I',data,h['data_offset']+3536+n*4)[0] for n in [0,6,9]}
69
+ caller_sites=[];count_refs=[];entry=None
70
+ for i in ins:
71
+ at=i['index']
72
+ if i['opcode']=='ENTER':entry=at
73
+ if i['opcode']=='CONST' and i['operand']==34992524:
74
+ count_refs.append({'instruction':at,'entry':entry,'window':ins[max(entry,at-4):at+6]})
75
+ if i['opcode']=='CALL' and at and ins[at-1]['opcode']=='CONST' and ins[at-1]['operand'] in [204035,187911,262012]:
76
+ caller_sites.append({'instruction':at,'entry':entry,'target':ins[at-1]['operand'],
77
+ 'window':ins[max(entry,at-12):at+4]})
78
+ output={'source':q['source'],'sha256':q['sha256'],'parser_traces':parsers,
79
+ 'digit_parser_bounds_compositions':compositions,'name_copy_call_traces':copies,
80
+ 'selector_name_copy_trace':trace_slice(ins,253692,253704,{}, {88:0x30000}),
81
+ 'name_contract_compositions':names,'match_insertion_traces':insertions,
82
+ 'maxclients_registry_row':maxclients,'count_init_trace':trace_slice(ins,4217,4221,{34089604:64},{}),
83
+ 'client_array_init_window':ins[4214:4233],'count_literal_references':count_refs,
84
+ 'chat_dispatch_traces':chat,'dispatcher_direct_callers':caller_sites,'qvm_entry_targets':entry_targets,
85
+ 'chat_mode_branch_traces':mode_branches,
86
+ 'client_entity_index_trace':trace_slice(ins,204036,204044,{}, {1264:3}),
87
+ 'entry_windows':{str(n):ins[at:at+16] for n,at in [(int(n),at) for n,at in entry_targets.items()]},
88
+ 'client_dispatch_window':ins[204035:204184],
89
+ 'functions':{str(e):partition_function(ins,e,data,h) for e in [37159,69338,5096]},
90
+ 'strings':{str(at):vm_string(data,h,at) for at in [69746,69737,69734,55676,55651]},'native':{},
91
+ 'limitations':'Static supplied QVM/Linux ELF64 plus bounded synthetic instruction traces. Calls are '
92
+ 'recorded, not entered, with zero placeholders. Parser results reflect the evaluator 32-bit arithmetic '
93
+ 'and require original interpreter/JIT confirmation. Name contract composition models strncpy truncation '
94
+ 'without executing libc. Match capacity relies on count/layout invariants; no end-to-end memory-safety '
95
+ 'or native configuration/access-control/runtime claim. Literal scans exclude alias/bulk paths.'}
96
+ for label in ['linux--Quake3-UrT-Ded.x86_64','linux--Quake3-UrT.x86_64']:
97
+ r=json.loads(Path(f'docs/research/native/{label}.json').read_text())
98
+ verified((Path('.cache/UrbanTerror43')/r['source']['path']).read_bytes(),r['source']['sha256'])
99
+ a=r['artifacts']['disassembly.txt']
100
+ f=native_functions(verified(gzip.open(a['path'],'rb').read(),a['uncompressed_sha256']).decode())
101
+ names=['SV_BoundMaxClients','SV_Startup','SV_GameCommand','SV_ExecuteClientCommand','SV_InitGameProgs']
102
+ output['native'][label]={'source':r['source'],'disassembly_sha256':a['uncompressed_sha256'],
103
+ 'graphs':{name:control_flow(f[name]) for name in names}}
104
+ write_json(Path('docs/research/selector-bounds.json'),output)
105
+ print(f'Recovered {len(parsers)} integer traces, selector boundaries and dispatcher entry routes')
106
+
107
+
108
+ if __name__=='__main__':
109
+ main()
scripts/test_selector_bounds.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Check selector parsing, bounded storage and cross-layer dispatcher evidence."""
3
+ import json
4
+ from pathlib import Path
5
+ import unittest
6
+
7
+ from analyze_auth_queries import byte_string
8
+ from analyze_deferred_auth import trace_slice
9
+ from analyze_qvm import decode
10
+ from analyze_selector_bounds import integer_trace
11
+ from analyze_vm_extensions import verified
12
+
13
+
14
+ class SelectorBoundsTests(unittest.TestCase):
15
+ @classmethod
16
+ def setUpClass(cls):
17
+ cls.r=json.loads(Path('docs/research/selector-bounds.json').read_text())
18
+ q=json.loads(Path('docs/research/qvm/qagame.json').read_text())
19
+ _,cls.ins=decode(verified(Path(q['source']['member']['output']).read_bytes(),q['sha256']))
20
+
21
+ def test_parser_sign_whitespace_prefix_and_wrap(self):
22
+ expected=[0,0,0,0,1,12,-12,12,0,12,2147483647,-2147483648,-1,0,3,-2147483648,2147483647,1661992959]
23
+ self.assertEqual([x['trace']['stack'][0] for x in self.r['parser_traces']],expected)
24
+ for x in self.r['parser_traces']:
25
+ self.assertEqual(x['trace']['calls'],[])
26
+ self.assertIn(x['trace']['trace'][-1],[37169,37276])
27
+
28
+ def test_decimal_result_matches_independent_modular_arithmetic(self):
29
+ # Actual byte/arithmetic loops, independently checked across many boundaries.
30
+ for value in [0,1,63,64,2**31-1,2**31,2**32-1,2**32,2**32+63,10**20-1]:
31
+ for sign in [1,-1]:
32
+ text=str(sign*value).encode()
33
+ result=(sign*value) % 2**32
34
+ if result>=2**31:result-=2**32
35
+ self.assertEqual(integer_trace(self.ins,text)['stack'],[result])
36
+
37
+ def test_digit_overflow_can_reach_in_range_slot_bounds(self):
38
+ traces=self.r['digit_parser_bounds_compositions']
39
+ self.assertEqual([x['parsed_slot'] for x in traces],[-1,0,3,-2147483648])
40
+ self.assertEqual([bool(x['bounds_trace']['calls']) for x in traces],[True,False,False,True])
41
+ for x in traces:self.assertEqual(x['digit_trace']['calls'],[])
42
+
43
+ def test_selector_name_copy_bound_and_contract_collision(self):
44
+ self.assertEqual(self.r['selector_name_copy_trace']['calls'],[{'instruction':253702,'target':69338,
45
+ 'args':{'8':0x40000000+24,'12':0x30000+504,'16':64}}])
46
+ t=self.r['name_copy_call_traces'][1]['trace']
47
+ self.assertEqual(t['calls'][0]['args'],{'8':0x20000,'12':0x30000,'16':63})
48
+ self.assertEqual(t['byte_writes'],[{'address':0x20000+63,'value':0}])
49
+ names=self.r['name_contract_compositions']
50
+ self.assertEqual(len(bytes.fromhex(names[2]['contract_copy_hex'])),63)
51
+ self.assertEqual(names[1]['cleaned']['output_hex'],names[2]['cleaned']['output_hex'])
52
+ self.assertEqual(names[2]['cleaned']['output_hex'],names[3]['cleaned']['output_hex'])
53
+
54
+ def test_match_list_boundary_and_count_source(self):
55
+ for x in self.r['match_insertion_traces']:
56
+ count=x['prior_match_count'];t=x['trace'];offset=96+count*4
57
+ self.assertEqual(t['memory'][str(0x40000000+offset)],0x30000+count*6336)
58
+ self.assertEqual(t['memory'][str(0x40000000+92)],count+1)
59
+ self.assertEqual(x['write_local_offset'],offset)
60
+ self.assertEqual([x['write_local_offset'] for x in self.r['match_insertion_traces']],[96,348,352])
61
+ self.assertEqual(self.r['count_init_trace']['memory']['34992524'],64)
62
+ self.assertEqual(self.r['maxclients_registry_row']['name'],'sv_maxclients')
63
+ self.assertEqual(len(self.r['count_literal_references']),109)
64
+
65
+ def test_qvm_entry_and_chat_dispatch_arguments(self):
66
+ self.assertEqual(self.r['qvm_entry_targets'],{'0':21,'6':105,'9':125})
67
+ self.assertEqual([(x['command'],x['trace']['calls'][0]['args']) for x in self.r['chat_dispatch_traces']],
68
+ [('say',{'8':0x20000,'12':0,'16':0}),('say_team',{'8':0x20000,'12':1,'16':0}),
69
+ ('me',{'8':0x20000,'12':4,'16':0})])
70
+ self.assertEqual([(x['entry'],x['instruction'],x['target']) for x in self.r['dispatcher_direct_callers']],
71
+ [(0,109,204035),(0,127,262012),(204035,204128,187911),
72
+ (204035,204154,187911),(204035,204180,187911)])
73
+ branches=self.r['chat_mode_branch_traces']
74
+ self.assertEqual([len(x['trace']['calls']) for x in branches],[0,1,1])
75
+ self.assertEqual([x['trace']['calls'][0]['target'] for x in branches[1:]],[180858,180858])
76
+ self.assertEqual(self.r['client_entity_index_trace']['memory'][str(0x40000000+1048)],34091368+3*880)
77
+
78
+ def test_linux_native_startup_bounds_and_vm_command_evidence(self):
79
+ for native in self.r['native'].values():
80
+ graphs=native['graphs']
81
+ startup='\n'.join(x['line'] for x in graphs['SV_Startup']['instructions'])
82
+ self.assertIn('$0x40',startup)
83
+ self.assertIn('$0x1, %esi',startup)
84
+ self.assertTrue({'Cvar_Get','Cvar_Set','Z_Malloc'} <= {c['label'] for c in graphs['SV_Startup']['calls']})
85
+ game='\n'.join(x['line'] for x in graphs['SV_GameCommand']['instructions'])
86
+ self.assertIn('$0x9, %esi',game)
87
+ self.assertIn('$0x2,',game)
88
+ client='\n'.join(x['line'] for x in graphs['SV_ExecuteClientCommand']['instructions'])
89
+ self.assertIn('$0x6, %esi',client)
90
+ self.assertIn('<VM_Call>',client)
91
+
92
+ def test_missing_terminator_step_limit_and_leave_opt_in(self):
93
+ with self.assertRaisesRegex(ValueError,'byte read'):
94
+ trace_slice(self.ins,37160,37277,{}, {44:0x10000},byte_memory={0x10000:ord('1')},halt_on_leave=True)
95
+ with self.assertRaisesRegex(ValueError,'escaped bound'):
96
+ trace_slice(self.ins,37160,37277,{}, {44:0x10000},limit=10,
97
+ byte_memory=byte_string(0x10000,b'1'),halt_on_leave=True)
98
+ with self.assertRaisesRegex(ValueError,'unsupported slice opcode LEAVE'):
99
+ trace_slice(self.ins,37160,37277,{}, {44:0x10000},byte_memory=byte_string(0x10000,b''))
100
+
101
+
102
+ if __name__=='__main__':
103
+ unittest.main()