tinymux/docs/survey-command-parser.md
Stephen Dennis 68d411be48 docs: record command.cpp parser audit — clean, no bug
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>
2026-06-14 17:05:45 -06:00

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) — are CA_WIZARD (the /inline switch is wizard-only; @lua/inline re-checks Wizard), 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).
  • @train has its own train_nest_lev guard.
  • Function/[u()] nesting inside commands is bounded by func_nest_lim / func_invk_lim (reset per top-level command) and the AST evaluator's nStackNest/bStackLimitReached spam guard (incremented in ast.cpp).

Buffers are bounded.

  • process_command's working buffers (preserve_cmd, SpaceCompressCommand, LowerCaseCommand) are thread_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_SIZE guard (off-by-one vs -1, like boolexp's object-ref loop) but are not exploitable: command input is always NUL-terminated within LBUF_SIZE-1, so the loop stops on the NUL before q reaches 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] in handle_gmcp (nPkg clamped), result[8000] in do_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.