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

Recover authentication selectors command checks and mode paths

Browse files
docs/FINDINGS.md CHANGED
@@ -236,3 +236,9 @@ The main qagame update loop skips null mirrors/unchanged modification counts and
236
  ## F-049 — Setting corrections and additional registration sites
237
 
238
  Bounded qagame paths recover bomb-plant/inactivity correction requests, direct funstuff/strategy mirror edits and configstring indices 1007/1010. Requesting a cvar change does not prove an immediate mirror update. The direct trap-3 scan finds 28 sites in eight partitions; 23 constant-argument slices are evaluated exactly, including `sv_master1=master.urbanterror.info` and `g_modversion=4.3.4`. Dynamic/table/stack-mirror and native merge/flag policies remain incomplete ([CHG-002/003](research/CVAR_CHANGES.md)).
 
 
 
 
 
 
 
236
  ## F-049 — Setting corrections and additional registration sites
237
 
238
  Bounded qagame paths recover bomb-plant/inactivity correction requests, direct funstuff/strategy mirror edits and configstring indices 1007/1010. Requesting a cvar change does not prove an immediate mirror update. The direct trap-3 scan finds 28 sites in eight partitions; 23 constant-argument slices are evaluated exactly, including `sv_master1=master.urbanterror.info` and `g_modversion=4.3.4`. Dynamic/table/stack-mirror and native merge/flag policies remain incomplete ([CHG-002/003](research/CVAR_CHANGES.md)).
239
+
240
+ **F-050 — Observed selector/helper behavior:** qagame selector 253561 classifies digit-only strings for numeric parsing; signed slot bounds and state-zero rejection precede success slot assignment. Name scanning cleans copied names, prefers the first exact match over partial matches, and rejects ambiguous partial results. Bounded byte fixtures establish color/caret/control-byte cleanup and ASCII case-insensitive substring behavior; full selector/parser/copy limits remain open. [QUERY-001](research/AUTH_QUERIES.md).
241
+
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).
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. 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. 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
@@ -113,3 +113,7 @@ Completed static milestones RE-245–247 with `analyze_game_cvars.py`: 126 main
113
  ## 2026-10-11 — Cvar changes, corrections and additional registrations
114
 
115
  Completed bounded milestones RE-249–251 with `analyze_cvar_changes.py`: cache and row-word branches, notification format/bit-32 paths, the empty entry-2589 helper, 32 numeric setting fixtures, 28 direct registration sites and 23 constant argument traces. CHG-001–003/F-048/049 distinguish direct mirror edits from native set requests and retain exact string capitalization. Eight tests cover numeric boundaries, cached/NULL paths, notification selection, no-op helper evidence, registration coverage/wrapper metadata and 32-bit BAND semantics added to the bounded evaluator. RE-248 remains open; RE-252 records other settings/gear/configstring/runtime work. No original executable/full QVM or callee was executed.
 
 
 
 
 
113
  ## 2026-10-11 — Cvar changes, corrections and additional registrations
114
 
115
  Completed bounded milestones RE-249–251 with `analyze_cvar_changes.py`: cache and row-word branches, notification format/bit-32 paths, the empty entry-2589 helper, 32 numeric setting fixtures, 28 direct registration sites and 23 constant argument traces. CHG-001–003/F-048/049 distinguish direct mirror edits from native set requests and retain exact string capitalization. Eight tests cover numeric boundaries, cached/NULL paths, notification selection, no-op helper evidence, registration coverage/wrapper metadata and 32-bit BAND semantics added to the bounded evaluator. RE-248 remains open; RE-252 records other settings/gear/configstring/runtime work. No original executable/full QVM or callee was executed.
116
+
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.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -297,3 +297,8 @@ Purpose: turn the supplied installation into an evidence-backed specification of
297
  - [x] RE-250 Recover bounded bomb-plant, funstuff, inactivity and strategy-time numeric correction branches, direct mirror edits and configstring indices ([CHG-002](research/CVAR_CHANGES.md)).
298
  - [x] RE-251 Index all 28 adjacent qagame trap-3 registration sites and recover exact arguments at 23 constant sites, including two post-table metadata registrations ([CHG-003](research/CVAR_CHANGES.md)).
299
  - [ ] RE-252 Recover every remaining setting-specific cvar update branch, gear string/item tables and effects, configstring consumers, row-word mutation paths and original-runtime notification/correction timing fixtures.
 
 
 
 
 
 
297
  - [x] RE-250 Recover bounded bomb-plant, funstuff, inactivity and strategy-time numeric correction branches, direct mirror edits and configstring indices ([CHG-002](research/CVAR_CHANGES.md)).
298
  - [x] RE-251 Index all 28 adjacent qagame trap-3 registration sites and recover exact arguments at 23 constant sites, including two post-table metadata registrations ([CHG-003](research/CVAR_CHANGES.md)).
299
  - [ ] RE-252 Recover every remaining setting-specific cvar update branch, gear string/item tables and effects, configstring consumers, row-word mutation paths and original-runtime notification/correction timing fixtures.
300
+
301
+ - [x] RE-253 Recover bounded auth selector digit classification, slot/state branches, name scan/exact-match priority and byte cleanup/substring helpers ([QUERY-001](research/AUTH_QUERIES.md)).
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.
docs/research/AUTH_QUERIES.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Authentication selectors, command checks and internal mode
2
+
3
+ Static pass, 2026-10-11. Reproduce with `python3 scripts/analyze_auth_queries.py`; verify with `python3 scripts/test_auth_queries.py`. [Generated evidence](auth-queries.json) retains source hashes, function partitions, initialized jump-table rows, instruction windows and bounded byte/word-memory traces. No original executable or complete QVM is run. CALLs remain unexecuted with zero placeholder results; numeric-parser results, comparison results where applicable and substring length-call results are explicitly supplied fixture inputs.
4
+
5
+ ## QUERY-001: selector grammar and name helpers
6
+
7
+ Selector entry **253561** first scans bytes for ASCII digits `0` through `9`. A NUL after that scan selects numeric handling; any other byte selects name handling. Thus `01` selects numeric parsing, while `-1`, `+1`, whitespace and `1x` select name handling. Empty text also takes the numeric branch in this helper; the previously recovered auth-whois query wrapper rejects an empty selector before invoking it. Integer helper 37159 is not executed in this pass: decimal conversion, long-input overflow and its standalone empty-input result remain open.
8
+
9
+ After conversion, a signed slot below zero or at least the word at **34992524** prints `Bad client slot: %i\n` through diagnostic helper 5123 and returns zero. A valid slot addresses `load(34992496) + slot * 6336`. Record offset **468 == 0** prints the not-connected diagnostic through helper 2294 and returns zero; every nonzero value takes the success path. The success tail writes the derived slot index to record offset **140** and returns the record. The fixtures include a negative raw state to demonstrate this exact comparison, not to claim that such a state occurs normally.
10
+
11
+ Name handling scans slots below the same count. It skips records with state zero, copies the name from offset **504** into a 64-byte local buffer through helper 69338, then cleans that copy with **70241**. It compares the cleaned name against the selector using helper 69386 with limit 99999. An exact match replaces the match count with one and exits the scan immediately, superseding earlier partial matches. Otherwise substring helper **69723** contributes partial matches. No matches print the no-client diagnostic; multiple partial matches print a heading and slot/name list and return zero. A single match reaches the same offset-140 success tail. The selector itself is not passed through the name-cleaning helper in the visible path.
12
+
13
+ The actual byte loops in the two helpers are evaluated against synthetic buffers:
14
+
15
+ | Helper | Recovered behavior and fixtures |
16
+ | --- | --- |
17
+ | 70241 | In-place cleanup drops `^` followed by an ASCII digit, collapses adjacent carets by advancing over the first, keeps bytes 32–126 and **31**, drops other bytes, and writes a terminating NUL. `^1Alpha^7 → Alpha`, `A^^B → A^B`, `A^xB → A^xB`, and `A 01 1f 7f 80 B → A 1f B` in hexadecimal byte notation. Repeated carets can interact with a following color code: `^^^2A^ → A^`. |
18
+ | 69723 | Rejects zero lengths and a needle longer than its haystack, then searches every possible start position with ASCII uppercase-to-lowercase folding on both sides. `AlphaBravo` contains `BRAVO` and `phab`; it does not perform token or word-boundary checks. Length results are supplied inputs, while its comparison loops execute. |
19
+
20
+ The whole selector and its copy/comparison/parser callees are not composed into a runtime fixture yet. The 64-byte copy's truncation/error contract, actual client-count limits, match-list capacity, non-ASCII behavior beyond the retained byte instructions and slot identity lifetime still require recovery.
21
+
22
+ ## QUERY-002: role-dependent command check and query routes
23
+
24
+ Command-check entry **167097** requires internal mode 35017616 at least 1, a nonzero caller object, nonempty command text, caller-object offset 520 pointing to a record whose state at 468 equals **2**, and a role at record offset **3688** at least 2. Its initialized jump table at **29196 + role * 4**, guarded to roles 2 through 8, selects:
25
+
26
+ | Role value / previously recovered label | Target / check |
27
+ | --- | --- |
28
+ | 2 / Friend | 167174: substring helper 69723 against `auth_cmds_friend` mirror string at 35001200. |
29
+ | 3 / Member | 167200: the same check against `auth_cmds_member` string at 35000904. |
30
+ | 4 / Referee | 167226: the same check against `auth_cmds_referee` string at 35000608. |
31
+ | 5–8 / Admin through FrozenSandDeveloper | 167252: comparison helper 69386 with `public`, limit 99999; equal returns zero, unequal returns one. |
32
+
33
+ Other role values return zero. Substring results are converted to zero/one. This is a concrete helper policy, not a complete authentication or command-execution specification. In particular, the command lists are searched case-insensitively **as substrings**, not parsed as space-delimited permissions: the fixture `bigtext say` contains `ay`. Whether a text admitted by this helper is a recognized command still depends on later exact dispatch comparisons. This pass does not assert that arbitrary admitted substrings execute a command.
34
+
35
+ There are exactly **two adjacent CONST-1264/CALL** query callers:
36
+
37
+ - Entry **187911**, CALL **189081**: the retained prefix windows require a single initial slash, reject `//` and `/me`, then advance past the slash. The path checks argument count greater than one and internal mode greater than zero, invokes 167097 with the caller object and remaining command text, and proceeds only for a nonzero result. A later case-insensitive comparison selects exact `auth-whois`, forwarding the caller object and a tokenized selector to 1264. Its bounded dispatch tail proves forwarding, not the result of the earlier permission call.
38
+ - Entry **262012**, CALL **263053**: trap 9 (encoded target -10) argument retrieval fills command text from argv 0 and selector from argv 1. Exact `auth-whois` selects 1264 with caller-object argument **zero** and the argv-1 buffer. The visible route does not invoke 167097; it does not establish who can invoke this dispatcher or the native console/rcon access controls. The query wrapper still applies its internal-mode gate.
39
+
40
+ These routes connect the earlier [announcement mode-two output](AUTH_ANNOUNCEMENTS.md) and [query usage/error paths](GAME_CVARS.md) to actual command strings. Computed calls, tokenization grammar, selector escaping, upstream dispatcher entry points and complete permission/configuration lifecycle remain open.
41
+
42
+ ## QUERY-003: internal authentication mode 35017616
43
+
44
+ The previously unnamed positive gate is an **internal authentication mode**, distinct from the registered `auth` mirror. All **24 literal address references** are indexed. Eight adjacent constant-value/STORE4 writers occur in entry **171**; this literal scan does not establish indirect/bulk-write coverage or every lifetime transition.
45
+
46
+ Entry 171 rejects `auth_enable` integers other than exactly **1**, `sv_auth_engine` values other than **1**, and `dedicated` integers other than **2**. These branches directly set internal mode zero, request `auth=0`, and request `auth_status` respectively `off`, `off (bad engine)` or `off (local)`. The supplied `dedicated` registry name/default are preserved; no alternate g_dedicated cvar is inferred.
47
+
48
+ Next, at least one feature is required: `auth_cheaters == 1`, `auth_tags == 1`, `auth_verbosity >= 1`, `auth_notoriety >= 1`, a nonempty `auth_groups` string, or a nonempty `auth_owners` string. With none, mode zero and `off (no cvar)` are selected. The disabled-tail fixture begins after preceding feature checks; it does not execute those string-length calls.
49
+
50
+ Once prerequisites pass, the local mode selection is:
51
+
52
+ | Inputs / precedence | Internal mode | Requested cvar/status effects |
53
+ | --- | --- | --- |
54
+ | Nonempty `auth_groups` | 2 | `auth=2`, `auth_status=private`, and `auth_notoriety=0`. This branch wins even if notoriety is positive. |
55
+ | Empty groups, positive `auth_notoriety` | 3 | Formats the **negative** notoriety integer for `auth`, requests `auth_status=notoriety`. The formatter result remains a placeholder in fixtures. |
56
+ | Empty groups, notoriety zero/negative | 1 | `auth=1`, `auth_status=public`. |
57
+
58
+ The later visible address-resolution path uses the [previously recovered network trap 600](VM_EXTENSIONS.md). If address type at **35017620** remains zero, it prints diagnostics, resets internal mode to zero and requests `auth_status=Off (no authserver)`. This failure tail does not visibly also request `auth=0`, unlike the earlier disabled branches. A derived UI/status display must therefore not assume that the registered auth value always equals this internal gate. Network parsing and DNS are not executed here.
59
+
60
+ One direct call to entry 171 was observed at instruction 5042 in function 3877; its complete initialization scheduling/re-entry path is not specified by this pass. The recovered prerequisites do not establish current native server settings or historical authentication service availability.
61
+
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.
docs/research/GAME_CVARS.md CHANGED
@@ -64,3 +64,5 @@ The 11 literal integer-view references occur in entries 171, 165352, 166169 and
64
  Follow-up: [CHG-001–003](CVAR_CHANGES.md) recover the visible word-20/24 behavior, modification-cache gates, four numeric setting branches and 28 direct registration sites (23 exact constant-argument traces). Raw field values remain preserved; complete native policy and all row-word mutations are still open.
65
 
66
  RE-245–247 cover the main registry extraction, native mirror/verbosity connection and bounded direct caller argument/gate evidence. RE-230/244 remain open for full caller/object/global-update/reset and rendered-output behavior. RE-248 adds the remaining cvar registrations, native flag/merge/update policy, table word-20/24 semantics, positive-gate initialization and auth-whois selector/dispatch/permission recovery. The next implementation contract should use recovered names and fields rather than inferred global labels.
 
 
 
64
  Follow-up: [CHG-001–003](CVAR_CHANGES.md) recover the visible word-20/24 behavior, modification-cache gates, four numeric setting branches and 28 direct registration sites (23 exact constant-argument traces). Raw field values remain preserved; complete native policy and all row-word mutations are still open.
65
 
66
  RE-245–247 cover the main registry extraction, native mirror/verbosity connection and bounded direct caller argument/gate evidence. RE-230/244 remain open for full caller/object/global-update/reset and rendered-output behavior. RE-248 adds the remaining cvar registrations, native flag/merge/update policy, table word-20/24 semantics, positive-gate initialization and auth-whois selector/dispatch/permission recovery. The next implementation contract should use recovered names and fields rather than inferred global labels.
67
+
68
+ Further follow-up: [QUERY-001–003](AUTH_QUERIES.md) connects auth-whois to both direct dispatch routes, recovers its selector helpers and role-check policy, and assigns the positive gate a bounded internal authentication-mode role. Full selector/permission/configuration lifecycle remains open.
docs/research/README.md CHANGED
@@ -33,10 +33,11 @@ python3 scripts/test_event_queues.py
33
  python3 scripts/test_auth_announcements.py
34
  python3 scripts/test_game_cvars.py
35
  python3 scripts/test_cvar_changes.py
 
36
  python3 scripts/storage_policy.py check --require-setup
37
  ```
38
 
39
- 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`, and `analyze_cvar_changes.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.
40
 
41
  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.
42
 
@@ -70,3 +71,5 @@ The extension collector adds five focused tests for native tail calls, command-r
70
  [Main game cvars and announcement callers](GAME_CVARS.md) adds RE-245–247: all 126 main registry rows, native cvar mirror evidence and direct output-caller paths. Reproduce with `python3 scripts/analyze_game_cvars.py`; check with `python3 scripts/test_game_cvars.py`.
71
 
72
  [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`.
 
 
 
33
  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
 
 
71
  [Main game cvars and announcement callers](GAME_CVARS.md) adds RE-245–247: all 126 main registry rows, native cvar mirror evidence and direct output-caller paths. Reproduce with `python3 scripts/analyze_game_cvars.py`; check with `python3 scripts/test_game_cvars.py`.
72
 
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`.
docs/research/auth-queries.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_auth_queries.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Bounded authentication selector, command-role and internal-mode evidence."""
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from analyze_auth_announcements import recover_table
7
+ from analyze_deferred_auth import partition_function, trace_slice
8
+ from analyze_game_cvars import vm_string
9
+ from analyze_qvm import decode, write_json
10
+ from analyze_vm_extensions import verified
11
+
12
+
13
+ def byte_string(address, value):
14
+ return {address+i: b for i, b in enumerate(value+b'\0')}
15
+
16
+
17
+ def cleaned_name(ins, value):
18
+ address = 0x10000
19
+ t = trace_slice(ins, 70242, 70383, {}, {44: address}, limit=10000,
20
+ byte_memory=byte_string(address, value))
21
+ result = []
22
+ for at in range(address, address+len(value)+1):
23
+ b = t['byte_memory'][at]
24
+ if not b:
25
+ break
26
+ result.append(b)
27
+ return {'input_hex': value.hex(), 'output_hex': bytes(result).hex(), 'trace': t}
28
+
29
+
30
+ def substring_trace(ins, haystack, needle):
31
+ # The length-call results are supplied explicitly; the byte comparison runs.
32
+ empty = not haystack or not needle or len(needle)>len(haystack)
33
+ return trace_slice(ins, 69746, 69760 if empty else 69910, {},
34
+ {24: len(needle), 28: len(haystack), 72: 0x10000, 76: 0x20000},
35
+ limit=10000, byte_memory={**byte_string(0x10000, haystack), **byte_string(0x20000, needle)})
36
+
37
+
38
+ def main():
39
+ qvm = json.loads(Path('docs/research/qvm/qagame.json').read_text())
40
+ data = verified(Path(qvm['source']['member']['output']).read_bytes(), qvm['sha256'])
41
+ h, ins = decode(data)
42
+ rows = json.loads(Path('docs/research/game-cvars.json').read_text())
43
+ if rows['sha256'] != qvm['sha256']:
44
+ raise ValueError('registry provenance mismatch')
45
+ by_name = {r['name']: r for r in rows['registry']['rows']}
46
+ for name, address, offset in [('auth_enable',35003268,12),('dedicated',34090492,12),
47
+ ('auth_groups',35001792,16),('auth_notoriety',35002084,12),
48
+ ('auth_cmds_friend',35001200,16),('auth_cmds_member',35000904,16),
49
+ ('auth_cmds_referee',35000608,16)]:
50
+ if by_name[name]['mirror_address']+offset != address:
51
+ raise ValueError('cvar mirror identity mismatch')
52
+ classify = []
53
+ for text in [b'',b'0',b'01',b'63',b'123',b'-1',b'+1',b' 1',b'1 ',b'1x',b'Player']:
54
+ digit_only = all(48 <= x <= 57 for x in text)
55
+ trace = trace_slice(ins, 253562, 253605 if digit_only else 253669, {}, {376: 0x10000},
56
+ limit=2048, byte_memory=byte_string(0x10000,text))
57
+ classify.append({'input_hex':text.hex(),'digit_only':digit_only,'trace':trace})
58
+ slots = []
59
+ base = 0x30000
60
+ for slot in [-1,0,3,4]:
61
+ valid = 0 <= slot < 4
62
+ t = trace_slice(ins,253612,253641 if valid else 253638,{34992524:4},{360:slot})
63
+ slots.append({'slot':slot,'maxclients':4,'trace':t})
64
+ connection = []
65
+ for state in [-1,0,1,2]:
66
+ slot=3;record=base+slot*6336
67
+ t=trace_slice(ins,253641,253666 if state==0 else 253875,
68
+ {34992496:base,record+468:state},{352:slot})
69
+ returned = None if state==0 else trace_slice(ins,253875,253889,{34992496:base},{88:record})
70
+ connection.append({'state_raw':state,'slot':slot,'trace':t,'success_tail':returned})
71
+ substrings=[]
72
+ for haystack,needle in [(b'AlphaBravo',b'BRAVO'),(b'AlphaBravo',b'phab'),(b'Alpha',b'Gamma'),
73
+ (b'bigtext say',b'ay'),(b'bigtext say',b'say map'),(b'public',b'PUBLIC'),
74
+ (b'',b'a'),(b'abc',b''),(b'ab',b'abc')]:
75
+ substrings.append({'haystack_hex':haystack.hex(),'needle_hex':needle.hex(),
76
+ 'trace':substring_trace(ins,haystack,needle)})
77
+ table=recover_table(data,h,ins,29196,2,8,167097,167279)
78
+ table_memory={r['vm_address']:r['target_instruction'] for r in table['rows']}
79
+ role_targets={r['value']:r['target_instruction'] for r in table['rows']}
80
+ roles=[]
81
+ for role in [-1,0,1,2,3,4,5,6,7,8,9]:
82
+ target=role_targets.get(role,167275)
83
+ t=trace_slice(ins,167148,target,{**table_memory,0x20000+520:0x30000,0x30000+3688:role},{68:0x20000})
84
+ roles.append({'role':role,'target':target,'trace':t})
85
+ permission_gates=[]
86
+ for mode,entity,command,state,role in [(0,0x20000,b'whois',2,2),(1,0,b'whois',2,2),
87
+ (1,0x20000,b'',2,2),(1,0x20000,b'whois',1,2),(1,0x20000,b'whois',2,1),
88
+ (1,0x20000,b'whois',2,2),(3,0x20000,b'whois',2,8)]:
89
+ stop = 167103 if mode<1 else 167131 if not entity or not command or state!=2 else 167145 if role<2 else 167148
90
+ t=trace_slice(ins,167098,stop,{35017616:mode,entity+520:0x30000,0x30000+468:state,0x30000+3688:role},
91
+ {68:entity,72:0x40000},byte_memory=byte_string(0x40000,command))
92
+ permission_gates.append({'mode':mode,'entity':entity,'command_hex':command.hex(),'state_raw':state,
93
+ 'role':role,'reaches_role_switch':stop==167148,'trace':t})
94
+ routes=[]
95
+ for route,start,stop,locals_map in [('client',189074,189083,{3348:0x20000,3120:0x40000}),
96
+ ('console',263048,263055,{})]:
97
+ routes.append({'route':route,'trace':trace_slice(ins,start,stop,{},locals_map)})
98
+ modes=[]
99
+ for groups,notoriety in [(0,0),(1,0),(0,5),(1,5),(0,-1)]:
100
+ modes.append({'groups_length':groups,'notoriety':notoriety,
101
+ 'trace':trace_slice(ins,308,389,{35002084:notoriety},{80:groups})})
102
+ disabled=[]
103
+ for cause,start,memory,locals_map in [
104
+ ('enable_not_one',172,{35003268:0},{}),('bad_engine',201,{}, {68:0}),
105
+ ('not_dedicated_two',224,{34090492:1},{}),('no_features_tail',279,{}, {76:0}),
106
+ ('unresolved_address',469,{35017620:0},{})]:
107
+ disabled.append({'cause':cause,'trace':trace_slice(ins,start,838,memory,locals_map)})
108
+ refs=[];callers=[];entry=None
109
+ for i in ins:
110
+ at=i['index']
111
+ if i['opcode']=='ENTER':entry=at
112
+ if i['opcode']=='CONST' and i['operand']==35017616:
113
+ refs.append({'instruction':at,'entry':entry,'window':ins[max(entry,at-4):at+7]})
114
+ if i['opcode']=='CALL' and at and ins[at-1]['opcode']=='CONST' and ins[at-1]['operand']==1264:
115
+ callers.append({'instruction':at,'entry':entry,'window':ins[at-22:at+3]})
116
+ output={'source':qvm['source'],'sha256':qvm['sha256'],
117
+ 'functions':{str(e):partition_function(ins,e,data,h) for e in [171,253561,69723,70241,167097]},
118
+ 'digit_classification_traces':classify,'slot_bound_traces':slots,'slot_connection_traces':connection,
119
+ 'clean_name_traces':[cleaned_name(ins,s) for s in [b'^1Alpha^7',b'A^^B',b'A^xB',b'A\x01\x1f\x7f\x80B']],
120
+ 'substring_traces':substrings,'permission_role_table':table,'permission_role_traces':roles,
121
+ 'permission_gate_traces':permission_gates,
122
+ 'query_direct_callers':callers,'dispatch_traces':routes,
123
+ 'dispatch_windows':{'client_prefix':ins[188019:188059],
124
+ 'client_precheck':ins[188059:188105],'client_match':ins[189059:189085],
125
+ 'console_match':ins[263034:263059],'console_argv':ins[262012:262040]},
126
+ 'mode_selection_traces':modes,'disabled_mode_traces':disabled,'mode_literal_references':refs,
127
+ 'strings':{str(at):vm_string(data,h,at) for at in [77268,87076,38124,38397,38385,38139,38107,38095,
128
+ 38081,38073,38060,38053,37794,67226,91861,91825,91796,91759,91747]},
129
+ 'limitations':'Static supplied qagame plus bounded instruction slices, not a complete QVM/runtime. '
130
+ 'All CALLs are recorded and return zero placeholders. Integer parser and substring length-call results '
131
+ 'are supplied fixture inputs; selected slices exclude preceding conditions. No complete selector, '
132
+ 'permission lifecycle, engine registration/config timing or network/runtime conformance claim.'}
133
+ write_json(Path('docs/research/auth-queries.json'),output)
134
+ print(f'Recovered selector helpers, role table, {len(callers)} query callers and {len(refs)} internal-mode references')
135
+
136
+
137
+ if __name__=='__main__':
138
+ main()
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'):
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'):
17
  subprocess.run([sys.executable, str(Path('scripts')/script)], check=True)
18
 
19
 
scripts/analyze_deferred_auth.py CHANGED
@@ -14,7 +14,7 @@ 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):
18
  """Evaluate only a selected straight/bounded slice against synthetic memory.
19
 
20
  Record calls rather than entering callees. Reject unknown reads/opcodes and
@@ -22,6 +22,7 @@ def trace_slice(instructions, start, stop, memory, locals_map, limit=256):
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
@@ -42,12 +43,24 @@ def trace_slice(instructions, start, stop, memory, locals_map, limit=256):
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))
@@ -83,7 +96,10 @@ def trace_slice(instructions, start, stop, memory, locals_map, limit=256):
83
  pc = stack.pop()
84
  else:
85
  raise ValueError(f'unsupported slice opcode {op}')
86
- return {'trace': trace, 'calls': calls, 'memory': memory, 'byte_writes': byte_writes}
 
 
 
87
 
88
 
89
  def partition_function(instructions, entry, data, header):
 
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
 
22
  """
23
  stack, args, calls, trace, byte_writes = [], {}, [], [], []
24
  memory = dict(memory)
25
+ bytes_map = None if byte_memory is None else dict(byte_memory)
26
  local_base = 0x40000000
27
  for offset, value in locals_map.items():
28
  memory[local_base+offset] = value
 
43
  if address not in memory:
44
  raise ValueError('unknown synthetic memory read')
45
  stack.append(memory[address])
46
+ elif op == 'LOAD1':
47
+ address = stack.pop()
48
+ if bytes_map is None or address not in bytes_map:
49
+ raise ValueError('unknown synthetic byte read')
50
+ stack.append(bytes_map[address] & 255)
51
+ elif op == 'SEX8':
52
+ value = stack.pop() & 255
53
+ stack.append(value-256 if value & 128 else value)
54
+ elif op == 'NEGI':
55
+ stack.append(signed32(-stack.pop()))
56
  elif op == 'STORE4':
57
  value, address = stack.pop(), stack.pop()
58
  memory[address] = signed32(value)
59
  elif op == 'STORE1':
60
  value, address = stack.pop(), stack.pop()
61
  byte_writes.append({'address': address, 'value': value & 255})
62
+ if bytes_map is not None:
63
+ bytes_map[address] = value & 255
64
  elif op in ('ADD', 'SUB', 'MULI'):
65
  right, left = stack.pop(), stack.pop()
66
  stack.append(signed32(left+right if op == 'ADD' else left-right if op == 'SUB' else left*right))
 
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
 
105
  def partition_function(instructions, entry, data, header):
scripts/test_auth_queries.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Check authentication selector helpers, role routing and mode branches."""
3
+ import json
4
+ from pathlib import Path
5
+ import unittest
6
+
7
+ from analyze_auth_queries import cleaned_name, substring_trace
8
+ from analyze_deferred_auth import trace_slice
9
+ from analyze_qvm import decode
10
+ from analyze_vm_extensions import verified
11
+
12
+
13
+ class AuthQueryTests(unittest.TestCase):
14
+ @classmethod
15
+ def setUpClass(cls):
16
+ cls.r=json.loads(Path('docs/research/auth-queries.json').read_text())
17
+ q=json.loads(Path('docs/research/qvm/qagame.json').read_text())
18
+ _,cls.ins=decode(verified(Path(q['source']['member']['output']).read_bytes(),q['sha256']))
19
+
20
+ def test_digit_classifier_and_slot_bounds(self):
21
+ t=self.r['digit_classification_traces']
22
+ self.assertEqual([bytes.fromhex(x['input_hex']) for x in t if x['digit_only']],
23
+ [b'',b'0',b'01',b'63',b'123'])
24
+ for x in t:
25
+ self.assertIn(253604,x['trace']['trace'])
26
+ self.assertEqual(x['trace']['calls'],[])
27
+ for x in self.r['slot_bound_traces']:
28
+ calls=x['trace']['calls']
29
+ self.assertEqual(bool(calls),x['slot'] in (-1,4))
30
+ if calls:self.assertEqual(calls[0]['args'],{'8':91861,'12':x['slot']})
31
+
32
+ def test_connection_zero_rejects_nonzero_sets_slot_identity(self):
33
+ for x in self.r['slot_connection_traces']:
34
+ if x['state_raw']==0:
35
+ self.assertIsNone(x['success_tail'])
36
+ self.assertEqual(x['trace']['calls'][0]['args'],{'8':91825,'12':3})
37
+ else:
38
+ self.assertEqual(x['trace']['calls'],[])
39
+ self.assertEqual(x['success_tail']['memory'][str(0x30000+3*6336+140)],3)
40
+
41
+ def test_cleaning_and_case_insensitive_partial_match(self):
42
+ self.assertEqual([bytes.fromhex(x['output_hex']) for x in self.r['clean_name_traces']],
43
+ [b'Alpha',b'A^B',b'A^xB',b'A\x1fB'])
44
+ # Actual byte loops are evaluated; length calls are supplied fixture inputs.
45
+ for x,expected in zip(self.r['substring_traces'],[1,1,0,1,0,1,0,0,0]):
46
+ t=x['trace']
47
+ self.assertEqual(t['calls'],[])
48
+ self.assertEqual(t['memory'].get(str(0x40000000+20),0),expected)
49
+
50
+ def test_permission_role_table_and_preconditions(self):
51
+ self.assertEqual([(x['value'],x['target_instruction']) for x in self.r['permission_role_table']['rows']],
52
+ [(2,167174),(3,167200),(4,167226),(5,167252),(6,167252),(7,167252),(8,167252)])
53
+ for x in self.r['permission_role_traces']:
54
+ self.assertEqual(x['target'],167275 if x['role']<2 or x['role']>8 else
55
+ {2:167174,3:167200,4:167226}.get(x['role'],167252))
56
+ self.assertEqual([x['reaches_role_switch'] for x in self.r['permission_gate_traces']],
57
+ [False,False,False,False,False,True,True])
58
+
59
+ def test_two_dispatch_routes_and_exact_command(self):
60
+ self.assertEqual([(x['entry'],x['instruction']) for x in self.r['query_direct_callers']],
61
+ [(187911,189081),(262012,263053)])
62
+ self.assertEqual(self.r['strings']['77268'],'auth-whois')
63
+ self.assertEqual(self.r['strings']['87076'],'auth-whois')
64
+ self.assertEqual(self.r['dispatch_traces'][0]['trace']['calls'][0]['args'],{'8':0x20000,'12':0x40000})
65
+ self.assertEqual(self.r['dispatch_traces'][1]['trace']['calls'][0]['args'],
66
+ {'8':0,'12':0x40000000+1056})
67
+
68
+ def test_internal_modes_precedence_and_disabled_paths(self):
69
+ self.assertEqual([x['trace']['memory']['35017616'] for x in self.r['mode_selection_traces']], [1,2,3,2,1])
70
+ notoriety=self.r['mode_selection_traces'][2]['trace']['calls'][0]
71
+ self.assertEqual((notoriety['target'],notoriety['args']['12']),(70458,-5))
72
+ for x in self.r['disabled_mode_traces']:
73
+ self.assertEqual(x['trace']['memory']['35017616'],0)
74
+ refs=self.r['mode_literal_references']
75
+ self.assertEqual(len(refs),24)
76
+ # Every literal direct STORE is in entry 171; indirect/bulk writes remain outside this claim.
77
+ writers=[x for x in refs if x['window'][5]['opcode']=='CONST' and x['window'][6]['opcode']=='STORE4']
78
+ self.assertEqual(len(writers),8)
79
+ self.assertEqual({x['entry'] for x in writers},{171})
80
+
81
+ def test_byte_evaluator_fail_closed_and_signed_extension(self):
82
+ sequence=[{'index':i,'opcode':op,'operand':value} for i,(op,value) in enumerate(
83
+ [('CONST',0x10000),('LOAD1',None),('SEX8',None),('ARG',8),('CONST',1),('CALL',None)])]
84
+ with self.assertRaisesRegex(ValueError,'byte read'):
85
+ trace_slice(sequence,0,6,{}, {})
86
+ self.assertEqual(trace_slice(sequence,0,6,{}, {},byte_memory={0x10000:0x80})['calls'][0]['args'][8],-128)
87
+ with self.assertRaisesRegex(ValueError,'byte read'):
88
+ trace_slice(self.ins,253562,253605,{}, {376:0x10000},byte_memory={0x10000:ord('1')})
89
+ with self.assertRaisesRegex(ValueError,'escaped bound'):
90
+ trace_slice(self.ins,253562,253605,{}, {376:0x10000},limit=10,byte_memory={0x10000:0})
91
+
92
+ def test_in_place_cleanup_and_substring_input_is_unchanged(self):
93
+ t=cleaned_name(self.ins,b'^^^2A^')
94
+ self.assertEqual(bytes.fromhex(t['output_hex']),b'A^')
95
+ t=substring_trace(self.ins,b'BIGTEXT SAY',b'text')
96
+ self.assertEqual(t['memory'][0x40000000+20],1)
97
+ self.assertEqual(bytes(t['byte_memory'][0x10000+i] for i in range(12)),b'BIGTEXT SAY\0')
98
+
99
+
100
+ if __name__=='__main__':
101
+ unittest.main()