From 3b5345a7f92f56c88b85c5f4420a8658b933619c Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Tue, 30 Jun 2026 13:28:07 +0900 Subject: [PATCH] wolfterm: bound escBuf saves and clear esc state on OSC resume Harden the partial-sequence reassembly in wolfSSH_DoControlSeq and fix an escape-state leak in the wolfSSH_ConvertConsole OSC resume path (USE_WINDOWS_API console handling). - Tighten both escBuf partial-save branches to reject bufSz - *idx >= WOLFSSL_MAX_ESCBUF before the WMEMCPY and log via WLOG. escBuf is WOLFSSL_MAX_ESCBUF bytes, so a save of exactly that length fills it with no room for later state; rejecting the boundary keeps the two branches consistent and matches the resume check. - Add the capacity guard to the initial-parse CSI save branch, which previously copied into escBuf with no bound. In the default build getArgs caps the advance below WOLFSSL_MAX_ESCBUF so the copy was already safe; the guard prevents a custom WOLFSSH_MAX_CONSOLE_ARGS > WOLFSSL_MAX_ESCBUF build from overflowing escBuf. - Clear escState to WC_ESC_NONE when a resumed OSC sequence completes, not just escBufSz. Without this escState stayed WS_ESC_OSC and the next call re-entered OSC parsing on plain bytes instead of printing them. Extend test_wolfSSH_ConvertConsole with a CSI sequence split across three calls (ESC[ | args | command char), a trailing plain byte that must be printed once state is cleared, and a single buffer whose CSI args run to the end and complete on the next byte. --- src/wolfterm.c | 8 +++++++- tests/api.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/wolfterm.c b/src/wolfterm.c index dc377d6af..3c86c0ad3 100644 --- a/src/wolfterm.c +++ b/src/wolfterm.c @@ -475,7 +475,8 @@ static int wolfSSH_DoControlSeq(WOLFSSH* ssh, WOLFSSH_HANDLE handle, byte* buf, numArgs = getArgs(buf, bufSz, &i, args); if (i >= bufSz) { /* save left overs for next call */ - if (bufSz - *idx > WOLFSSL_MAX_ESCBUF) { + if (bufSz - *idx >= WOLFSSL_MAX_ESCBUF) { + WLOG(WS_LOG_ERROR, "escBuf state exceeds capacity"); return WS_FATAL_ERROR; } WMEMCPY(ssh->escBuf, buf + *idx, bufSz - *idx); @@ -491,6 +492,10 @@ static int wolfSSH_DoControlSeq(WOLFSSH* ssh, WOLFSSH_HANDLE handle, byte* buf, if (i >= bufSz) { /* save left overs for next call */ + if (bufSz - *idx >= WOLFSSL_MAX_ESCBUF) { + WLOG(WS_LOG_ERROR, "escBuf state exceeds capacity"); + return WS_FATAL_ERROR; + } WMEMCPY(ssh->escBuf, buf + *idx, bufSz - *idx); ssh->escBufSz = bufSz - *idx; return WS_WANT_READ; @@ -687,6 +692,7 @@ int wolfSSH_ConvertConsole(WOLFSSH* ssh, WOLFSSH_HANDLE handle, byte* buf, if (ret == WS_WANT_READ) { return ret; } + ssh->escState = WC_ESC_NONE; ssh->escBufSz = 0; break; diff --git a/tests/api.c b/tests/api.c index 9c3d6c850..bda465eef 100644 --- a/tests/api.c +++ b/tests/api.c @@ -3370,6 +3370,18 @@ static byte osc_trunc_str[] = { /* "ESC ] 0 ; title" with no BEL terminator */ static byte osc_full[] = { /* well formed "ESC ] 0 ; hi BEL" */ 0x1B, 0x5D, 0x30, 0x3B, 0x68, 0x69, 0x07 }; +static byte csi_open[] = { /* "ESC [" with no args yet, escBufSz left at 0 */ + 0x1B, 0x5B +}; +static byte csi_args[] = { /* pure args "12" with no command char */ + 0x31, 0x32 +}; +static byte csi_cmd[] = { /* command char 'm' completes the sequence */ + 0x6D +}; +static byte csi_inline[] = { /* "ESC [ 1 2" args run to end of one buffer */ + 0x1B, 0x5B, 0x31, 0x32 +}; #endif /* USE_WINDOWS_API */ @@ -3417,6 +3429,29 @@ static void test_wolfSSH_ConvertConsole(void) AssertIntEQ(wolfSSH_ConvertConsole(ssh, stdoutHandle, osc_full, sizeof(osc_full)), WS_SUCCESS); + /* a CSI sequence split so the first packet ends right after "ESC [" and + * the second carries only argument bytes must not read past the buffer + * while waiting for the command char */ + AssertIntEQ(wolfSSH_ConvertConsole(ssh, stdoutHandle, csi_open, + sizeof(csi_open)), WS_WANT_READ); + AssertIntEQ(wolfSSH_ConvertConsole(ssh, stdoutHandle, csi_args, + sizeof(csi_args)), WS_WANT_READ); + /* the trailing command char completes the reassembled sequence */ + AssertIntEQ(wolfSSH_ConvertConsole(ssh, stdoutHandle, csi_cmd, + sizeof(csi_cmd)), WS_SUCCESS); + /* after the split sequence completes the esc state must be cleared, so a + * following plain argument byte is printed rather than swallowed back into + * CSI parsing (which would return WS_WANT_READ) */ + AssertIntEQ(wolfSSH_ConvertConsole(ssh, stdoutHandle, csi_args, 1), + WS_SUCCESS); + + /* a single buffer whose CSI arguments run to the end with no command char + * must save the partial args and wait, then complete on the next byte */ + AssertIntEQ(wolfSSH_ConvertConsole(ssh, stdoutHandle, csi_inline, + sizeof(csi_inline)), WS_WANT_READ); + AssertIntEQ(wolfSSH_ConvertConsole(ssh, stdoutHandle, csi_cmd, + sizeof(csi_cmd)), WS_SUCCESS); + wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); #endif /* USE_WINDOWS_API */