nmarshall commited on
Commit
0c343f8
·
1 Parent(s): 66138c3

Recover native address types event queues and journal layout

Browse files
docs/FINDINGS.md CHANGED
@@ -210,3 +210,9 @@ Exact native fork/source revision, runtime resource lookup, legacy launch enviro
210
  **F-040 — Observed event-source and dispatch bytes:** Sys_GetEvent emits a zero-type record with a host timestamp when no event is queued; journal modes can replace or record those events. Both seven-entry native switch tables map key/character/mouse/joystick/console/packet paths. Com_Frame derives raw elapsed from the event-loop return before applying its inlined modifier. Complete scheduling and runtime wrap behavior remain open. [TIME-002](research/FRAME_TIMING.md).
211
 
212
  **F-041 — Observed native pause predicates:** cl_paused gates a native record count using state > 1 and unsigned field 0x24ee4 >= 1; count <= 1 selects sv_paused=1, larger counts select zero. SV_Frame inlines the gate before simulation steps. The counted field’s semantic name, game timeout interactions and runtime/platform parity remain unknown. [TIME-003](research/FRAME_TIMING.md).
 
 
 
 
 
 
 
210
  **F-040 — Observed event-source and dispatch bytes:** Sys_GetEvent emits a zero-type record with a host timestamp when no event is queued; journal modes can replace or record those events. Both seven-entry native switch tables map key/character/mouse/joystick/console/packet paths. Com_Frame derives raw elapsed from the event-loop return before applying its inlined modifier. Complete scheduling and runtime wrap behavior remain open. [TIME-002](research/FRAME_TIMING.md).
211
 
212
  **F-041 — Observed native pause predicates:** cl_paused gates a native record count using state > 1 and unsigned field 0x24ee4 >= 1; count <= 1 selects sv_paused=1, larger counts select zero. SV_Frame inlines the gate before simulation steps. The counted field’s semantic name, game timeout interactions and runtime/platform parity remain unknown. [TIME-003](research/FRAME_TIMING.md).
213
+
214
+ ## Native queue and pause-field update — 2026-10-11
215
+
216
+ **F-042 — Observed Linux ELF64 address-type chain:** client field 0x24ee4 is netchannel address type, with bot zero, loopback two and an IPv4-format path at four. Bot allocation writes zero and drop cleanup uses the corresponding bot-free path. This resolves F-041’s unnamed count field: the pause predicate excludes bots. Complete lifecycle and runtime behavior remain open. [EVT-001](research/EVENT_QUEUES.md).
217
+
218
+ **F-043 — Observed native ring/clock paths, synthetic journal validation:** platform/common queues retain 256/1024 records of 32 bytes; ordinary full insertion frees the overwritten nonnull payload and advances tail. Common overflow warnings are throttled until a nonfull insertion. Com_Milliseconds preserves nonzero real events in the common queue before returning a zero-type event timestamp, including journal-derived time. A bounded Linux ELF64 journal inspector preserves pointer/padding metadata without dereferencing it. No original capture or runtime conformance is claimed. [EVT-002/003](research/EVENT_QUEUES.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. 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. 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
@@ -97,3 +97,7 @@ Completed static milestones RE-227–229. `analyze_engine_control.py` retains te
97
  ## 2026-10-11 — Frame elapsed, event sources and pause gates
98
 
99
  Completed bounded static milestones RE-232–234 with `analyze_frame_timing.py`: ten Linux ELF64 graphs, file-backed float constants and event tables, pause-assignment strings, inline frame windows and 64 decoded-native-instruction traces with synthetic cvar inputs. TIME-001–003 and F-039–041 record the clock distinctions, zero-timescale/camera behavior, elapsed caps, journal/event timestamp source and raw pause-count predicates. Seven tests check boundary outputs in both binaries, native table/constant identity, real trace addresses and rejection of unsupported domains. RE-231 stays open; new RE-235/236 retain layout and runtime questions. Original game code was not launched; the evaluator does not enter native callees.
 
 
 
 
 
97
  ## 2026-10-11 — Frame elapsed, event sources and pause gates
98
 
99
  Completed bounded static milestones RE-232–234 with `analyze_frame_timing.py`: ten Linux ELF64 graphs, file-backed float constants and event tables, pause-assignment strings, inline frame windows and 64 decoded-native-instruction traces with synthetic cvar inputs. TIME-001–003 and F-039–041 record the clock distinctions, zero-timescale/camera behavior, elapsed caps, journal/event timestamp source and raw pause-count predicates. Seven tests check boundary outputs in both binaries, native table/constant identity, real trace addresses and rejection of unsupported domains. RE-231 stays open; new RE-235/236 retain layout and runtime questions. Original game code was not launched; the evaluator does not enter native callees.
100
+
101
+ ## 2026-10-11 — Address-type semantics and event queue ownership
102
+
103
+ Completed static milestones RE-237–239 with `analyze_event_queues.py`: sixteen Linux ELF64 graphs, queue symbol sizes, file-backed address labels, caller chains and 108 direct field references per image. EVT-001–003/F-042/043 resolve the pause field to address type, document bot exclusion and ordinary queue overflow/warning/reset paths, and establish Com_Milliseconds event preservation. Added a bounded raw-journal inspector with eight tests covering provenance/layout evidence, mixed synthetic records, truncation, negative/oversized payloads, limits and ambiguous symbols. RE-235/236 remain open; RE-240 tracks full ownership/wrap/runtime/platform recovery. No original executable or journal capture was run/produced.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -275,5 +275,10 @@ Purpose: turn the supplied installation into an evidence-backed specification of
275
  - [x] RE-232 Recover Linux ELF64 frame modification constants, fixedtime/timescale/camera branches, upper bounds and hitch threshold with decoded-instruction traces ([TIME-001](research/FRAME_TIMING.md)).
276
  - [x] RE-233 Recover Linux ELF64 event timestamp sources, journal record paths and file-backed event dispatch tables; connect first-pump elapsed production to frame windows ([TIME-002](research/FRAME_TIMING.md)).
277
  - [x] RE-234 Recover Linux ELF64 native pause gate predicates, record stride, direct cvar strings and inline server-frame placement ([TIME-003](research/FRAME_TIMING.md)).
278
- - [ ] RE-235 Recover the semantic layout of native client field 0x24ee4; validate pause counting, stale pause-state handling and game timeout interactions in runtime fixtures.
279
  - [ ] RE-236 Validate journal timestamps/payload ownership, event queue overflow and full frame scheduling with original-runtime traces; cover unsupported floating-point domains and cvar constraints.
 
 
 
 
 
 
275
  - [x] RE-232 Recover Linux ELF64 frame modification constants, fixedtime/timescale/camera branches, upper bounds and hitch threshold with decoded-instruction traces ([TIME-001](research/FRAME_TIMING.md)).
276
  - [x] RE-233 Recover Linux ELF64 event timestamp sources, journal record paths and file-backed event dispatch tables; connect first-pump elapsed production to frame windows ([TIME-002](research/FRAME_TIMING.md)).
277
  - [x] RE-234 Recover Linux ELF64 native pause gate predicates, record stride, direct cvar strings and inline server-frame placement ([TIME-003](research/FRAME_TIMING.md)).
278
+ - [ ] RE-235 Complete native client/address layout recovery and validate pause counting, stale pause-state handling and game timeout interactions in runtime fixtures; the Linux ELF64 field 0x24ee4 address-type chain is recovered in RE-237.
279
  - [ ] RE-236 Validate journal timestamps/payload ownership, event queue overflow and full frame scheduling with original-runtime traces; cover unsupported floating-point domains and cvar constraints.
280
+
281
+ - [x] RE-237 Recover Linux ELF64 pause-field semantics through netchannel setup, address formatting, bot allocation and drop paths; index literal references ([EVT-001](research/EVENT_QUEUES.md)).
282
+ - [x] RE-238 Recover both Linux ELF64 event ring sizes, producer layouts, overflow/free paths and warning/reset behavior ([EVT-002](research/EVENT_QUEUES.md)).
283
+ - [x] RE-239 Recover Linux ELF64 Com_Milliseconds event-preservation side effects and add a bounded inspector for the observed journal layout with malformed-input fixtures ([EVT-003](research/EVENT_QUEUES.md)).
284
+ - [ ] RE-240 Recover every event producer/consumer/reset ownership path, counter wrap and concurrency semantics; validate burst/overflow traces and journal layouts across all supplied platforms.
docs/research/EVENT_QUEUES.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Native client address types, event rings and journals
2
+
3
+ Static pass, 2026-10-11. Reproduce with `python3 scripts/analyze_event_queues.py`; check with `python3 scripts/test_event_queues.py`. [Generated evidence](event-queues.json) contains sixteen Linux ELF64 instruction graphs, queue symbol addresses/sizes, file-backed address-format strings, focused caller windows and 108 literal field references per engine. Original/disassembly hashes are checked before collection. No original game process, native callee or complete QVM executes in this pass.
4
+
5
+ ## EVT-001: the counted pause field is an address type
6
+
7
+ The earlier [pause report](FRAME_TIMING.md) intentionally left field `client + 0x24ee4` unnamed. The new evidence connects it to the native network address through independent paths:
8
+
9
+ - `SV_DirectConnect` forms `client + 0x24edc` as the second argument of `Netchan_Setup`.
10
+ - `Netchan_Setup` stores the incoming address type at channel offset 8. Thus `0x24edc + 8 = 0x24ee4`.
11
+ - Address bytes are written at channel offsets 12–15, and a 16-bit port at offset 16. At the client level the address starts at `0x24ee4`, address bytes at `0x24ee8`, and port at `0x24eec`.
12
+ - `SV_UserinfoChanged` loads eight bytes from `0x24ee4` and four from `0x24eec` into the native address arguments used by `NET_IsLocalAddress`, `NET_AdrToString` and LAN checks. Packet matching similarly passes that region to address comparison.
13
+ - `SV_BotAllocateClient` writes zero at `0x24ee4`. `NET_AdrToString`'s type-zero branch uses the file-backed string `bot`; type two uses `loopback`, and type four uses `%i.%i.%i.%i:%hu`. `NET_IsLocalAddress` tests type two.
14
+ - `SV_DropClient`'s zero-type path reaches `SV_BotFreeClient` after its disconnect callback and command path.
15
+
16
+ These paths establish **bot = 0, loopback = 2, IPv4-format path = 4** for these binaries. They do not recover every address enum member, prove IPv6 support, or establish that every unknown value is rejected. Do not copy a different engine revision’s enum into the rewrite without checking the supplied fork.
17
+
18
+ Consequently the pause gate counts state words unsigned greater than one with a **nonzero address type**; zero-type bots are excluded. It is more precise to say nonbot clients than human players: connection state and address validity still constrain the result. With at most one counted client the pause helper returns one and selects `sv_paused=1`; with more it returns zero and clears that cvar if needed. The `cl_paused=0` early return still leaves prior `sv_paused` state untouched in this helper.
19
+
20
+ The report's 108 direct displacement references in each engine include allocation, drop, connection, pause, status, packet and send paths. This is a literal-offset index, not a complete inventory of aliases, computed pointers or writes. Full client/netchannel layout, reachable lifecycle states, game-mode timeouts and original-runtime pause scenarios remain open under RE-235.
21
+
22
+ ## EVT-002: two rings and overflow ownership
23
+
24
+ | Ring | Storage | Record size | Index selection | Observed full condition |
25
+ | --- | --- | --- | --- | --- |
26
+ | Platform `eventQue` | 8192 bytes / 256 records | 32 bytes | Low eight bits of head | Signed 32-bit head-minus-tail greater than 255 |
27
+ | Common `com_pushedEvents` | 32768 bytes / 1024 records | 32 bytes | Head masked with 0x3ff | Signed 32-bit head-minus-tail greater than 1023 |
28
+
29
+ The storage sizes come from retained ELF symbols, independently of the masks and overflow comparisons in instructions. `Sys_QueEvent` receives timestamp, type, two values, payload length and payload pointer. It stores those fields at offsets 0, 4, 8, 12, 16 and 24 respectively. The four bytes at offset 20 are alignment space and are not assigned by this function. A zero supplied timestamp is replaced by `Sys_Milliseconds()`; a nonzero one is retained.
30
+
31
+ At normal full capacity, both push functions select the head slot, which coincides with the oldest retained slot. On overflow they free that slot’s nonnull payload through `Z_Free`, advance tail, replace the event, and advance head. The producer copies/transfers the incoming payload pointer rather than cloning its bytes. Consumer/event-loop cleanup was captured in the earlier timing report. These observations imply ownership requirements for a Rust queue, but do not establish that all producers obey them or that arbitrary malformed pointers are safe.
32
+
33
+ `Sys_QueEvent` prints on each observed overflow path. `Com_PushEvent` uses a retained `printedWarning` flag: the first full insertion prints and sets it; subsequent full insertions suppress the warning. An insertion when not full clears the flag, enabling another warning on a later overflow. `Com_InitPushEvent` zeroes the full 32768-byte storage and resets head/tail; its body does not explicitly reset the warning flag. Do not interpret initialization as safe cleanup of previously owned live payloads without examining call sites.
34
+
35
+ The graph collector retains local branch edges and possible call continuation; it does not execute queue transitions. Full signed counter wrap, pathological head/tail differences, producer concurrency, allocation failures, reset-time ownership and real burst behavior need fixtures. The capacity statements concern normal queue invariants, rather than proving behavior for every 32-bit counter state.
36
+
37
+ ## EVT-003: clock queries preserve queued events; journal inspection
38
+
39
+ `Com_Milliseconds` calls `Com_GetRealEvent` repeatedly until a type-zero event arrives, then returns its timestamp. For each nonzero event it inserts the complete 32-byte record into the common pushed ring. The compiler inlines the same overflow, payload-free and warning-flag paths seen in `Com_PushEvent`; it does not call that function or directly call `Sys_Milliseconds` in the recovered body. The common warning flag is shared by these paths.
40
+
41
+ This clock service therefore has queue and ownership side effects. A Rust replacement that always reads a host clock would omit event preservation and could lose journal replay timing. The returned event time can come from a journal rather than a live host sample. Full caller expectations and event-loop reentrancy still need investigation.
42
+
43
+ The earlier `Com_GetRealEvent` graph reads/writes a raw 32-byte event header, then the indicated payload. The new inspector uses the observed **little-endian Linux ELF64** layout:
44
+
45
+ | Offset | Bytes | Interpretation |
46
+ | --- | --- | --- |
47
+ | 0 | 4 | Signed timestamp |
48
+ | 4 | 4 | Event type word |
49
+ | 8 | 4 | First value |
50
+ | 12 | 4 | Second value |
51
+ | 16 | 4 | Signed payload length |
52
+ | 20 | 4 | Preserved alignment bytes |
53
+ | 24 | 8 | Historical pointer value |
54
+ | 32 | Payload length | Payload bytes; next header follows immediately |
55
+
56
+ For a future captured journal, use:
57
+
58
+ ```sh
59
+ python3 scripts/analyze_event_queues.py --journal .cache/research/capture.journal --output docs/research/capture-journal.json
60
+ ```
61
+
62
+ No captured original journal is present or claimed in this pass. The inspector retains signed words, padding, historical pointer values, source hash, record offsets and payload hashes, and never dereferences a stored pointer. Synthetic fixtures exercise mixed records and payload boundaries. It rejects truncated headers/payloads, negative lengths and configured size/count limits. Those stricter bounds are **inspector policy**, not proof that the original parser rejects the same inputs. Types are preserved even when unknown. A zero-length payload consumes no bytes regardless of historical pointer value.
63
+
64
+ Raw pointer width and alignment make it unsafe to assume this journal representation is portable to ELF32, Windows or macOS. Recover those layouts separately and define a portable versioned research/replay format before using captures in the replacement. This journal is also distinct from network demo formats; do not apply this inspector to demos based on extension alone.
65
+
66
+ ## Completed slices and remaining work
67
+
68
+ RE-237–239 record the static address-type chain, queue storage/overflow paths, clock-query side effects and bounded journal inspector. RE-235/236 remain open for runtime validation and broader semantics. RE-240 adds complete producer/consumer/reset ownership, counter-wrap behavior and cross-platform journal-layout recovery. The scripts/tests validate collection, storage bounds and synthetic journal parsing; they do not constitute original-runtime queue or pause conformance tests.
docs/research/FRAME_TIMING.md CHANGED
@@ -63,3 +63,5 @@ With count at most one, the helper sets `sv_paused` to string `"1"` if needed an
63
  ## Completion and follow-up
64
 
65
  RE-232–234 cover these bounded static deliverables. RE-231 and PROV-001 remain open because complete catch-up, restart, callback ordering, runtime constraints and Windows/macOS/ELF32 behavior have not been recovered. Next recover the counted native field, validate actual configurations and journal fixtures, and compare the complete host-frame/server-frame paths across platforms. The Rust scheduling contract must preserve observed clock distinctions and explicitly address unsupported numeric domains after further evidence.
 
 
 
63
  ## Completion and follow-up
64
 
65
  RE-232–234 cover these bounded static deliverables. RE-231 and PROV-001 remain open because complete catch-up, restart, callback ordering, runtime constraints and Windows/macOS/ELF32 behavior have not been recovered. Next recover the counted native field, validate actual configurations and journal fixtures, and compare the complete host-frame/server-frame paths across platforms. The Rust scheduling contract must preserve observed clock distinctions and explicitly address unsupported numeric domains after further evidence.
66
+
67
+ Follow-up: [EVT-001](EVENT_QUEUES.md) resolves field 0x24ee4 to the native netchannel address type and identifies zero-type bots. The raw predicate and runtime limitations above remain applicable.
docs/research/README.md CHANGED
@@ -29,10 +29,11 @@ python3 scripts/test_native_followup.py
29
  python3 scripts/test_deferred_auth.py
30
  python3 scripts/test_engine_control.py
31
  python3 scripts/test_frame_timing.py
 
32
  python3 scripts/storage_policy.py check --require-setup
33
  ```
34
 
35
- 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`, and `analyze_frame_timing.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.
36
 
37
  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.
38
 
@@ -58,3 +59,5 @@ Next work should complete the extra game/UI entry command specifications and pac
58
  The extension collector adds five focused tests for native tail calls, command-register clobbers and intervening control transfers, fail-closed source identities/function boundaries, and shipped command/service connections. RE-211–213 record completed static milestones; RE-205/206 remain open for full behavior and platform coverage.
59
 
60
  [Frame elapsed, event sources and pause gates](FRAME_TIMING.md) adds RE-232–234, ten Linux ELF64 graphs and 64 bounded native instruction traces. Reproduce with `python3 scripts/analyze_frame_timing.py`; check with `python3 scripts/test_frame_timing.py`. Original-runtime and all-platform scheduling remain open.
 
 
 
29
  python3 scripts/test_deferred_auth.py
30
  python3 scripts/test_engine_control.py
31
  python3 scripts/test_frame_timing.py
32
+ python3 scripts/test_event_queues.py
33
  python3 scripts/storage_policy.py check --require-setup
34
  ```
35
 
36
+ 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`, and `analyze_event_queues.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.
37
 
38
  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.
39
 
 
59
  The extension collector adds five focused tests for native tail calls, command-register clobbers and intervening control transfers, fail-closed source identities/function boundaries, and shipped command/service connections. RE-211–213 record completed static milestones; RE-205/206 remain open for full behavior and platform coverage.
60
 
61
  [Frame elapsed, event sources and pause gates](FRAME_TIMING.md) adds RE-232–234, ten Linux ELF64 graphs and 64 bounded native instruction traces. Reproduce with `python3 scripts/analyze_frame_timing.py`; check with `python3 scripts/test_frame_timing.py`. Original-runtime and all-platform scheduling remain open.
62
+
63
+ [Native address types, event queues and journals](EVENT_QUEUES.md) adds RE-237–239: the pause-field address/bot chain, two queue policies, clock-query event preservation and a bounded Linux ELF64 journal inspector. Reproduce with `python3 scripts/analyze_event_queues.py`; check with `python3 scripts/test_event_queues.py`.
docs/research/event-queues.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_binaries.py CHANGED
@@ -11,7 +11,8 @@ def main():
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', 'analyze_engine_control.py', 'analyze_frame_timing.py'):
 
15
  subprocess.run([sys.executable, str(Path('scripts')/script)], check=True)
16
 
17
 
 
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', 'analyze_engine_control.py', 'analyze_frame_timing.py',
15
+ 'analyze_event_queues.py'):
16
  subprocess.run([sys.executable, str(Path('scripts')/script)], check=True)
17
 
18
 
scripts/analyze_event_queues.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Recover native event queue/address evidence; inspect bounded ELF64 journals."""
3
+ import argparse
4
+ import gzip
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ import re
9
+ import struct
10
+
11
+ from analyze_engine_control import control_flow, windows
12
+ from analyze_frame_timing import read_address
13
+ from analyze_native import elf
14
+ from analyze_qvm import write_json
15
+ from analyze_vm_extensions import native_functions, verified
16
+
17
+
18
+ EVENT = struct.Struct('<iiiii4sQ')
19
+
20
+
21
+ def parse_journal(data, max_events=100000, max_payload=16*1024*1024):
22
+ """Inspect this observed little-endian ELF64 layout, never dereference pointers.
23
+
24
+ Bounds are inspector policy, not recovered original-engine validation rules.
25
+ The historical pointer and padding are preserved as evidence, not interpreted
26
+ as a usable address or a portable file-format guarantee.
27
+ """
28
+ if max_events < 1 or max_payload < 0:
29
+ raise ValueError('invalid journal inspection limits')
30
+ records, offset = [], 0
31
+ while offset < len(data):
32
+ if len(records) >= max_events or len(data)-offset < EVENT.size:
33
+ raise ValueError('journal record limit or truncated header')
34
+ time, kind, value, value2, size, padding, pointer = EVENT.unpack_from(data, offset)
35
+ if size < 0 or size > max_payload or size > len(data)-offset-EVENT.size:
36
+ raise ValueError('negative, oversized or truncated payload')
37
+ start = offset + EVENT.size
38
+ payload = data[start:start+size]
39
+ records.append({'index': len(records), 'file_offset': offset, 'time_signed32': time,
40
+ 'type_signed32': kind, 'value_signed32': value, 'value2_signed32': value2,
41
+ 'payload_bytes': size, 'padding_hex': padding.hex(), 'historical_pointer': pointer,
42
+ 'payload_sha256': hashlib.sha256(payload).hexdigest()})
43
+ offset = start+size
44
+ return {'layout': 'observed Linux ELF64 little-endian 32-byte event header plus payload',
45
+ 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest(), 'records': records,
46
+ 'limitations': 'Inspector policy, not the original engine parser. Types and pointer values '
47
+ 'are retained without validation; zero-length payloads consume no bytes.'}
48
+
49
+
50
+ def immediate_strings(data, sections, graph):
51
+ result = []
52
+ for ins in graph['instructions']:
53
+ if ins['opcode'] != 'movl':
54
+ continue
55
+ match = re.match(r'\$0x([0-9a-f]+), %edx(?:\s|$)', ins['operands'])
56
+ if not match:
57
+ continue
58
+ address = int(match[1], 16)
59
+ raw = bytearray()
60
+ for at in range(address, address+256):
61
+ chunk, _ = read_address(data, sections, at, 1)
62
+ if chunk == b'\0':
63
+ break
64
+ raw.extend(chunk)
65
+ else:
66
+ raise ValueError('unterminated address-format string')
67
+ result.append({'instruction': ins['address'], 'address': address, 'text': raw.decode('ascii')})
68
+ return result
69
+
70
+
71
+ def symbol_record(symbols, name):
72
+ matches = {(s['address'], s['bytes']) for s in symbols if s['name'] == name and not s['undefined']}
73
+ if len(matches) != 1:
74
+ raise ValueError('missing or ambiguous queue symbol: ' + name)
75
+ address, size = matches.pop()
76
+ return {'name': name, 'address': address, 'bytes': size}
77
+
78
+
79
+ def main():
80
+ parser = argparse.ArgumentParser(description=__doc__)
81
+ parser.add_argument('--journal', type=Path, help='inspect a captured Linux ELF64 journal instead of native binaries')
82
+ parser.add_argument('--output', type=Path, help='JSON destination for journal inspection')
83
+ parser.add_argument('--max-payload', type=int, default=16*1024*1024)
84
+ args = parser.parse_args()
85
+ if args.journal:
86
+ if not args.output:
87
+ parser.error('--journal requires --output')
88
+ if args.output.resolve() == args.journal.resolve():
89
+ parser.error('journal output must not overwrite its input')
90
+ if args.journal.stat().st_size > 512*1024*1024:
91
+ raise ValueError('journal exceeds inspector file-size limit')
92
+ write_json(args.output, parse_journal(args.journal.read_bytes(), max_payload=args.max_payload))
93
+ return
94
+ if args.output:
95
+ parser.error('--output requires --journal')
96
+ output = {'native': {}, 'limitations': 'Static Linux ELF64 only. No original game process or native '
97
+ 'callee is executed. Literal offset references are not complete alias analysis; '
98
+ 'queue counter overflow and runtime journal compatibility remain unverified.'}
99
+ names = ['Com_PushEvent', 'Com_Milliseconds', 'Com_InitPushEvent', 'Sys_QueEvent',
100
+ 'NET_AdrToString', 'NET_IsLocalAddress', 'Netchan_Setup', 'SV_BotAllocateClient']
101
+ queue_names = ['eventQue', 'eventHead', 'eventTail', 'com_pushedEvents',
102
+ 'com_pushedEventsHead', 'com_pushedEventsTail']
103
+ for label in ('linux--Quake3-UrT-Ded.x86_64', 'linux--Quake3-UrT.x86_64'):
104
+ report = json.loads(Path(f'docs/research/native/{label}.json').read_text())
105
+ data = verified((Path('.cache/UrbanTerror43')/report['source']['path']).read_bytes(), report['source']['sha256'])
106
+ artifact = report['artifacts']['disassembly.txt']
107
+ functions = native_functions(verified(gzip.open(artifact['path'], 'rb').read(), artifact['uncompressed_sha256']).decode())
108
+ metadata, symbols = elf(data)
109
+ graphs = {name: control_flow(functions[name]) for name in names}
110
+ references = [{'function': name, 'function_address': fn['address'], 'line': line.strip()}
111
+ for name, fn in functions.items() for line in fn['lines']
112
+ if re.search(r'0x24ee4\(', line)]
113
+ focused = ['SV_DirectConnect', 'SV_UserinfoChanged', 'SV_PacketEvent', 'SV_DropClient']
114
+ output['native'][label] = {'source': report['source'], 'disassembly_sha256': artifact['uncompressed_sha256'],
115
+ 'graphs': graphs, 'queue_symbols': [symbol_record(symbols, n) for n in queue_names],
116
+ 'address_format_literals': immediate_strings(data, metadata['sections'], graphs['NET_AdrToString']),
117
+ 'client_address_type_literal_references': references,
118
+ 'client_address_windows': {name: windows(functions[name], [r'0x24ee4\(', r'<Netchan_Setup>', r'<SV_BotFreeClient>'], 15, 9) for name in focused},
119
+ 'journal_header_layout': {'bytes': EVENT.size, 'endianness': 'little', 'fields': {
120
+ 'time': [0, 4], 'type': [4, 4], 'value': [8, 4], 'value2': [12, 4],
121
+ 'payload_length': [16, 4], 'padding': [20, 4], 'historical_pointer': [24, 8]}}}
122
+ write_json(Path('docs/research/event-queues.json'), output)
123
+ print('Recovered sixteen native queue/address graphs and bounded ELF64 journal inspector')
124
+
125
+
126
+ if __name__ == '__main__':
127
+ main()
scripts/test_event_queues.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Native evidence checks and adversarial journal inspector fixtures."""
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+ import unittest
7
+
8
+ from analyze_event_queues import EVENT, parse_journal, symbol_record
9
+
10
+
11
+ class EventQueueTests(unittest.TestCase):
12
+ @classmethod
13
+ def setUpClass(cls):
14
+ cls.report = json.loads(Path('docs/research/event-queues.json').read_text())
15
+
16
+ def test_journal_preserves_metadata_and_payload_boundaries(self):
17
+ data = EVENT.pack(-1, 5, 12, -2, 4, b'abcd', 0xffffffffffffffff) + b'cmd\0'
18
+ data += EVENT.pack(42, 0, 0, 0, 0, b'\0'*4, 123)
19
+ result = parse_journal(data)
20
+ first, second = result['records']
21
+ self.assertEqual(first['time_signed32'], -1)
22
+ self.assertEqual(first['padding_hex'], '61626364')
23
+ self.assertEqual(first['historical_pointer'], 0xffffffffffffffff)
24
+ self.assertEqual(first['payload_sha256'], hashlib.sha256(b'cmd\0').hexdigest())
25
+ self.assertEqual(second['file_offset'], 36)
26
+ self.assertEqual(second['historical_pointer'], 123)
27
+ self.assertEqual(result['bytes'], 68)
28
+
29
+ def test_journal_rejects_truncation_negative_and_oversized_lengths(self):
30
+ for data in [b'x', EVENT.pack(0, 6, 0, 0, -1, b'\0'*4, 0),
31
+ EVENT.pack(0, 6, 0, 0, 5, b'\0'*4, 0)+b'1234']:
32
+ with self.assertRaises(ValueError):
33
+ parse_journal(data)
34
+ data = EVENT.pack(0, 5, 0, 0, 4, b'\0'*4, 0)+b'1234'
35
+ with self.assertRaises(ValueError):
36
+ parse_journal(data, max_payload=3)
37
+
38
+ def test_journal_limits_and_empty_stream(self):
39
+ data = EVENT.pack(0, 0, 0, 0, 0, b'\0'*4, 0)
40
+ with self.assertRaises(ValueError):
41
+ parse_journal(data*2, max_events=1)
42
+ with self.assertRaises(ValueError):
43
+ parse_journal(data, max_payload=-1)
44
+ self.assertEqual(parse_journal(b'')['records'], [])
45
+ self.assertEqual(parse_journal(data, max_payload=0)['records'][0]['payload_bytes'], 0)
46
+
47
+ def test_queue_storage_and_overflow_services(self):
48
+ for n in self.report['native'].values():
49
+ symbols = {s['name']: s for s in n['queue_symbols']}
50
+ self.assertEqual(symbols['eventQue']['bytes'], 256*32)
51
+ self.assertEqual(symbols['com_pushedEvents']['bytes'], 1024*32)
52
+ for name, threshold in [('Sys_QueEvent', '$0xff'), ('Com_PushEvent', '$0x3ff')]:
53
+ g = n['graphs'][name]
54
+ self.assertIn(threshold, '\n'.join(i['operands'] for i in g['instructions']))
55
+ self.assertTrue({'Com_Printf', 'Z_Free'} <= {c['label'] for c in g['calls']})
56
+ self.assertIn('Sys_Milliseconds', {c['label'] for c in n['graphs']['Sys_QueEvent']['calls']})
57
+
58
+ def test_clock_query_pushes_events_with_inline_overflow(self):
59
+ for n in self.report['native'].values():
60
+ g = n['graphs']['Com_Milliseconds']
61
+ calls = {c['label'] for c in g['calls']}
62
+ self.assertTrue({'Com_GetRealEvent', 'Z_Free', 'Com_Printf'} <= calls)
63
+ self.assertNotIn('Sys_Milliseconds', calls)
64
+ self.assertNotIn('Com_PushEvent', calls)
65
+ self.assertIn('$0x3ff', '\n'.join(i['operands'] for i in g['instructions']))
66
+
67
+ def test_client_field_address_and_bot_chain(self):
68
+ for n in self.report['native'].values():
69
+ self.assertEqual([s['text'] for s in n['address_format_literals']], ['bot', '%i.%i.%i.%i:%hu', 'loopback'])
70
+ connect = '\n'.join(n['client_address_windows']['SV_DirectConnect'])
71
+ self.assertIn('0x24edc(', connect)
72
+ self.assertIn('<Netchan_Setup>', connect)
73
+ setup = '\n'.join(i['line'] for i in n['graphs']['Netchan_Setup']['instructions'])
74
+ self.assertIn('0x8(%rbx)', setup)
75
+ self.assertEqual(0x24edc + 8, 0x24ee4)
76
+ bot = '\n'.join(i['line'] for i in n['graphs']['SV_BotAllocateClient']['instructions'])
77
+ self.assertRegex(bot, r'movl\s+\$0x0, 0x24ee4\(%rbx\)')
78
+ self.assertIn('<NET_AdrToString>', '\n'.join(n['client_address_windows']['SV_UserinfoChanged']))
79
+ self.assertIn('<SV_BotFreeClient>', '\n'.join(n['client_address_windows']['SV_DropClient']))
80
+
81
+ def test_all_local_graph_edges_resolve(self):
82
+ for n in self.report['native'].values():
83
+ for g in n['graphs'].values():
84
+ addresses = {i['address'] for i in g['instructions']}
85
+ for edge in g['edges']:
86
+ self.assertIn(edge['from'], addresses)
87
+ if edge['target_scope'] == 'local':
88
+ self.assertIn(edge['to'], addresses)
89
+
90
+ def test_missing_ambiguous_or_undefined_symbols_rejected(self):
91
+ for symbols in [[], [{'name': 'q', 'address': 1, 'bytes': 4, 'undefined': True}],
92
+ [{'name': 'q', 'address': a, 'bytes': 4, 'undefined': False} for a in [1, 2]]]:
93
+ with self.assertRaises(ValueError):
94
+ symbol_record(symbols, 'q')
95
+
96
+
97
+ if __name__ == '__main__':
98
+ unittest.main()