Audited the command parser/dispatcher (process_command/process_cmdent) for memory-safety and unbounded recursion. Well-hardened: regular commands queue (not stack-recursed), the only direct stack-recursion paths (@assert/@break/ @lua /inline) are wizard-gated and LBUF-bounded, @train has its own guard, and function nesting is bounded by func_nest_lim. Working buffers are thread_local LBUF (not stack arrays); the space-compress off-by-one is bounded by NUL/input cap (verified max-length commands survive); arg parsing delegates to MAX_ARG- bounded helpers. No fix needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.1 KiB
Survey: command parser/dispatcher (command.cpp)
Audit of the command parser in mux/modules/engine/command.cpp — the second of
the three parsers the boolexp header names (functions = eval.cpp, commands =
command.cpp, locks = boolexp.cpp). It processes every line of player input:
process_command (decompose → match → dispatch) and process_cmdent (permission
- argument parsing + handler call). Methodology matches the boolexp/wild/JIT campaign. Result: CLEAN — no memory-safety or unbounded-recursion bug found.
Why it's well-hardened (unlike boolexp's parser)
Recursion is structurally bounded. The boolexp DoS (#839) was unbounded parser recursion reachable by any player. command.cpp has no equivalent:
- Regular commands in action lists are queued (
wait_que→ bounded by the command-queue cycle limits), not run on the C stack. - The only direct stack-recursion paths —
@assert/inline,@break/inline,@lua/inline(process_command/Eval called directly) — areCA_WIZARD(the/inlineswitch is wizard-only;@lua/inlinere-checksWizard), and even then bounded by the LBUF command-string length (~2048 levels of@break/inline 1=…; verified 500 survives, longer chains truncate at the LBUF input cap, rc=0, no crash). @trainhas its owntrain_nest_levguard.- Function/
[u()]nesting inside commands is bounded byfunc_nest_lim/func_invk_lim(reset per top-level command) and the AST evaluator'snStackNest/bStackLimitReachedspam guard (incremented in ast.cpp).
Buffers are bounded.
process_command's working buffers (preserve_cmd,SpaceCompressCommand,LowerCaseCommand) arethread_local UTF8[LBUF_SIZE]— not stack arrays, so deep call chains don't balloon the stack.- The space-compress loop and command-name extraction use the loose
q < buf + LBUF_SIZEguard (off-by-one vs-1, like boolexp's object-ref loop) but are not exploitable: command input is always NUL-terminated withinLBUF_SIZE-1, so the loop stops on the NUL beforeqreaches the end. Verified: a 32760-char single-token command and a max-length spaced command both survive (no OOB, rc=0). - Fixed buffers all use bounded fills:
switch_buff[200](mux_strncpy),pkg[SBUF_SIZE]inhandle_gmcp(nPkgclamped),result[8000]indo_lua(sizeof(result)passed to the Lua control),check2[UTF8_SIZE4+1],qbuff[I64BUF_SIZE].
Argument parsing delegates to bounded helpers. process_cmdent splits args
via parse_to / parse_arglist(..., args, MAX_ARG, ..., &nargs) — the shared,
MAX_ARG-bounded splitters — into alloc_lbuf buffers that are freed on every
path. CS_* calling-sequence handling is table-driven.
Surfaces examined
process_command (decompose, prefix/single-letter leadins, @icmd/hook checks,
switch stripping), process_cmdent (perms, switch loop, CS_NO_ARG/CS_ONE_ARG/
CS_TWO_ARG/CS_ARGV arg parsing, hooks), handle_gmcp (telnet GMCP → A_GMCP),
do_lua, do_assert/do_break/do_train (recursion paths), is_prefix_cmd,
cmdtest/zonecmdtest. All bounded or wizard-gated. No fix needed.