Bound CSI continuation parse and clear esc state on completion#1076
Bound CSI continuation parse and clear esc state on completion#1076yosuke-wolfssl wants to merge 1 commit into
Conversation
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1076
Scan targets checked: wolfssh-bugs, wolfssh-src
No new issues found in the changed files. ✅
aidangarske
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 1 total — 1 posted, 0 skipped
Posted findings
- [High] CSI capacity errors are swallowed on resume —
src/wolfterm.c:684-691
Review generated by Skoll.
| @@ -669,6 +685,9 @@ int wolfSSH_ConvertConsole(WOLFSSH* ssh, WOLFSSH_HANDLE handle, byte* buf, | |||
| if (ret == WS_WANT_READ) { | |||
There was a problem hiding this comment.
🟠 [High] CSI capacity errors are swallowed on resume
🚫 BLOCK bug
The PR adds WS_FATAL_ERROR returns in wolfSSH_DoControlSeq when saving a CSI continuation would exceed escBuf, but the resume path in wolfSSH_ConvertConsole treats every non-WS_WANT_READ result as a completed sequence. For example, after ESC [ leaves ssh->escState == WS_ESC_CSI and escBufSz == 0, a following buffer of WOLFSSL_MAX_ESCBUF argument bytes hits the new bufSz - *idx >= WOLFSSL_MAX_ESCBUF guard and returns WS_FATAL_ERROR. This caller then clears the escape state, falls through with i still 0, writes those bytes as plain output, and returns WS_SUCCESS. That means the new defensive rejection path introduced by the PR is not actually enforced for the resume case it is meant to harden.
Suggestion:
| if (ret == WS_WANT_READ) { | |
| ret = wolfSSH_DoControlSeq(ssh, handle, buf, bufSz, &i); | |
| if (ret == WS_WANT_READ) { | |
| return ret; | |
| } | |
| if (ret != WS_SUCCESS) { | |
| return ret; | |
| } | |
| ssh->escState = WC_ESC_NONE; | |
| ssh->escBufSz = 0; | |
| break; |
Overview
Fixes a one-byte heap out-of-bounds read in
wolfSSH_DoControlSeq(src/wolfterm.c) when a Linux-console CSI escape sequence is split across SSH channel-data packets such that a continuation buffer contains only argument bytes with no command terminator. While in this code, also hardens theescBufreassembly bounds and fixes a related escape-state leak in the resume path. Windows-only code (USE_WINDOWS_API).Addressed by f_5903.
The bug
wolfSSH_DoControlSeqis re-entered withssh->escState == WS_ESC_CSIandssh->escBufSz == 0when a prior channel-data buffer ends exactly afterESC [. On the next call, the outer-ifis taken but the inner-if(escBufSz > 0) is false, falling into an unguarded inner-else:When the continuation buffer holds only argument bytes (digits/semicolons) and no command char,
getArgsadvancesitobufSz, soc = buf[i]reads one byte past the allocation. The sibling outer-elsebranch already guarded this exact dereference withif (i >= bufSz); the inner-elsedid not.USE_WINDOWS_APIbuild, authenticated peer controlling SSH channel-data packet boundaries.Changes
Bound the CSI continuation read. Add the same
if (i >= bufSz)reassembly guard the sibling branch uses — stash the partial sequence inescBufand returnWS_WANT_READinstead of reading past the buffer.Explicit
escBufcapacity bound. BothescBufsaves now rejectbufSz - *idx >= WOLFSSL_MAX_ESCBUFbefore theWMEMCPY, using this function's existingWLOG+WS_FATAL_ERRORidiom and matching the boundary semantics of the resume check. Defense-in-depth: in the default buildgetArgscaps the advance atWOLFSSH_MAX_CONSOLE_ARGS(16) <WOLFSSL_MAX_ESCBUF(19), so the copy was already safe; this prevents a customWOLFSSH_MAX_CONSOLE_ARGS > WOLFSSL_MAX_ESCBUFbuild from overflowingescBuf.Clear escape state on resume completion. When a split CSI/OSC sequence completes via the
wolfSSH_ConvertConsoleresume handlers, resetescState = WC_ESC_NONE(not justescBufSz), matching the inline completion path. Without this,escStatestayedWS_ESC_CSIand the next call's leading plain bytes were fed back into CSI parsing instead of being printed.Testing
Extended
test_wolfSSH_ConvertConsoleintests/api.c:ESC [|12|m) — drives the previously-unguarded inner-elseand verifies reassembly completes.escStateis cleared (returnsWS_SUCCESS, not swallowed into a new CSI parse).ESC [ 1 2) — covers the initial-parse save branch with real content.Verified on native Windows (Visual Studio Build Tools 2022, static Debug x64):
Negative controls confirm each test catches its regression:
csi_argsreturnsWS_FATAL_ERRORinstead ofWS_WANT_READWS_WANT_READinstead ofWS_SUCCESS