urt-rs / docs /research /CHAT_PATHS.md
nmarshall's picture
Recover bounded dead-chat policies and delivery formats
5dedc3d
|
Raw History Blame Contribute Delete
8 kB
# Native client command admission and bounded chat paths
Static pass, 2026-10-11. Reproduce with `python3 scripts/analyze_chat_paths.py`; check with `python3 scripts/test_chat_paths.py`. [Generated evidence](chat-paths.json) includes source/disassembly hashes, both Linux ELF64 native command tables/graphs, QVM chat helper partitions and bounded join/recipient traces. CALLs are recorded, not entered; token lengths and team-mode helper results are supplied fixture inputs. No original game or complete QVM runs.
## CHAT-001: native admission, sanitation and weighted limits
Both Linux ELF64 `ucmds` tables contain eight name/handler pairs followed by a null pair: `userinfo`, `disconnect`, `cp`, `vdr`, `download`, `nextdl`, `stopdl`, `donedl`. Their handlers are respectively SV_UpdateUserinfo_f, SV_Disconnect_f, SV_VerifyPaks_f, SV_ResetPureClient_f, SV_BeginDownload_f, SV_NextDownload_f, SV_StopDownload_f and SV_DoneDownload_f. The collector reads bounded, file-backed 16-byte rows and verifies string termination; it does not execute those handlers.
SV_ExecuteClientCommand tokenizes input and compares argv 0 with built-in names through **strcmp**. A match invokes its handler with the native client pointer before the subsequent forwarding decision. Its third native argument controls that decision: zero permits a handled return or unhandled debug diagnostic; nonzero reaches game forwarding only when no built-in matched. The unmatched forwarding path additionally requires native server state **2** and native client state **4**. Those values belong to native structures and must not be confused with QVM recipient state 2 below. The caller's third-argument production, flood throttling and complete native state lifecycle remain open.
Cmd_Args_Sanitize replaces **LF (10)** and **CR (13)** with space (32) in argument strings after argv 0. The file-backed sanitizer bytes are `0a 0d 00`; the symbol-address and graph evidence identify traversal beginning at `cmd_argv + 8`, i.e. argv 1 in these eight-byte-pointer images. The graph is retained; native mutation/tokenizer behavior is not executed in fixtures.
After sanitation, Q_stricmp selects `say`, `say_team` and `tell` for weighted budget **256**, and `ut_radio` for budget **122**. Other unmatched game commands bypass this particular limit scan. The scanner visits argv argc-1 down to 1, counts bytes and inter-argument separators, and adds **16 extra units per `$` byte**. Thus each dollar costs 17 units including its ordinary byte. More than **six** dollar bytes or a signed weighted total above the chosen budget takes the warning/log/server-command path instead of VM forwarding. Equality with a budget passes the visible comparison. This is instruction-graph recovery, not native execution or proof of downstream expansion/escaping semantics. The native command-6 tail still requires its preceding admission/filter path.
## CHAT-002: token concatenation capacity
Helper **180858** uses a global buffer at VM address **35737412**. It obtains argc, starts at its incoming token index, retrieves each token into a 1024-byte local buffer and gets its byte length. Before copying it checks signed `used + token_length < 1023`; otherwise it stops before that token. Accepted tokens are copied through trap 101 (encoded CALL -102), and a space is appended unless the token is the final argument. A terminating NUL is written at the resulting used offset.
Bounded slices supply token lengths rather than entering argv/length/memcpy callees. They show that used 1021 plus length 1 can copy and append a separator, reaching used **1023** with NUL at offset 1023. Used 1022 plus an empty nonfinal token does likewise. Used 1022 plus length 1 rejects the token, as does an initial length 1023. Empty nonfinal tokens can therefore still contribute a separator. The visible routine truncates by stopping at a whole-token boundary; it does not partially copy the rejected token.
This is a shared global buffer, not an owned return string. Reentrancy, caller lifetime, signed overflow with impossible/corrupt length inputs and native copy semantics remain unverified. Helper 187381 subsequently copies chat content with capacity **150** and calls 186435; the exact transformation and final effective text limits are still open.
## CHAT-003: recipient front gates and remaining send behavior
The delivery helper **186721** applies these recovered early gates:
- Sender-record offset **3128** greater than global time **34992532** rejects delivery; equality passes. This is a mute-deadline interpretation supported by the later `(MUTED)` log format, not a complete mute lifecycle or time-wrap contract.
- Registered **g_allowChat**, mirror integer **34077172**, supplied default `2`, must be nonzero. Channel mode zero additionally requires its value at least 2; modes 1/2/4 pass the observed gate when it is 1. Other numeric domains have not been constrained by runtime configuration.
- Recipient object must be nonzero, object offset **532** nonzero, and its record pointer at offset **520** nonzero. Offset 532's complete field identity is not assigned here.
- Recipient-record word **3780 + 4 * sender_slot** must be zero. The sender slot is derived from its record pointer, shared record base and stride 6336. Any nonzero ignore word rejects; complete ignore-table creation, bounds and lifetime remain open.
- Recipient-record state at **468** must equal **2**.
- For channel mode 1, the visible team path accepts matching record fields **3096** when helper 33715 reports nonzero. If that helper reports zero or the comparison fails, the same-object exception allows sender-to-self; otherwise delivery is rejected. Fixtures supply the helper result explicitly.
These are individually bounded gates, not a composed end-to-end recipient decision. The intervening g_allowChat==3/global-bit/state branch, later dead-chat policy, fields 3740/4324/4, round/pause state, and exact formatting remain open and are retained in the complete helper partition.
Send helper **187381** uses a mode jump table, builds a 64-byte name prefix, copies/transforms text, then either sends toward an explicit target with a possible sender echo or iterates slot objects `34091368 + slot * 880`, invoking 186721 per slot. The final delivery helper formats `me`, ordinary chat variants or `cchat` for private messages, then sends trap 17 to the recipient slot derived from the object address. Full table/format selection, mutations, escaping, reliable-command admission and rendering are not closed by this pass.
## Correction to the preceding batch
The previous BND-003 fixture incorrectly treated local **3356** as the chat channel mode. With ENTER frame 3340, locals 3348/3352/3356 hold the first/second/third incoming arguments; channel mode is **3352**. The early branch at 187929 reads the **third argument**. All observed say/say_team/me callers pass zero there and can proceed toward slash/auth checks. A nonzero third argument bypasses those checks through concatenation/common-send handling. The analyzer, generated evidence, tests, prior findings and BND-003 documentation are corrected together. This correction does not establish that every later permission or command check succeeds on every channel.
## Completion and remaining work
RE-261–263 cover native table/admission/filter evidence, bounded concatenation boundaries and recipient front gates. RE-264 tracks native caller/flood/tokenizer execution, complete chat/dead-chat/ignore/mute lifecycle and formatting/queue/rendered-output fixtures. RE-260 remains open for the broader native filtering and chat runtime contract. Original assets remain in ignored cache; only code/documentation and generated analysis evidence are added.
The [DEL-001–003 follow-up](CHAT_DELIVERY.md) recovers bounded dead-chat policy, formatter arguments and echo/fanout calls. It narrows the later-path questions above; complete lifecycle and runtime output remain open under RE-264/268.