tinymux/docs/survey-command-parser.md

54 lines
3.1 KiB
Markdown
Raw Permalink Normal View History

# 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.