From 56dc745dfb45573d534579dfc0d5a7d1be973cb7 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Tue, 1 Sep 2026 13:16:07 -0700 Subject: [PATCH 1/2] tests: split the SFTP server and client argv wolfSSH_SftpTest() gives the echoserver thread and the SFTP client each their own argument array. The server thread keeps ser.argv pointing at its array for as long as it runs, so the two cannot share one. --- tests/sftp.c | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/sftp.c b/tests/sftp.c index 856753d82..681fc9c6f 100644 --- a/tests/sftp.c +++ b/tests/sftp.c @@ -469,7 +469,9 @@ int wolfSSH_SftpTest(int flag) int ret = 0; int argsCount; - const char* args[10]; + /* The server thread holds ser.argv, so the client cannot share it. */ + const char* argsServer[10]; + const char* argsClient[10]; #ifndef USE_WINDOWS_API char portNumber[8]; #endif @@ -496,16 +498,16 @@ int wolfSSH_SftpTest(int flag) #endif argsCount = 0; - args[argsCount++] = "."; - args[argsCount++] = "-1"; + argsServer[argsCount++] = "."; + argsServer[argsCount++] = "-1"; #ifndef USE_WINDOWS_API - args[argsCount++] = "-p"; - args[argsCount++] = "0"; + argsServer[argsCount++] = "-p"; + argsServer[argsCount++] = "0"; #endif if (flag) - args[argsCount++] = "-N"; + argsServer[argsCount++] = "-N"; - ser.argv = (char**)args; + ser.argv = (char**)argsServer; ser.argc = argsCount; ser.signal = &ready; InitTcpReady(ser.signal); @@ -513,23 +515,23 @@ int wolfSSH_SftpTest(int flag) WaitTcpReady(&ready); argsCount = 0; - args[argsCount++] = "."; - args[argsCount++] = "-u"; - args[argsCount++] = "jill"; - args[argsCount++] = "-P"; - args[argsCount++] = "upthehill"; + argsClient[argsCount++] = "."; + argsClient[argsCount++] = "-u"; + argsClient[argsCount++] = "jill"; + argsClient[argsCount++] = "-P"; + argsClient[argsCount++] = "upthehill"; #ifndef USE_WINDOWS_API /* use port that server has found */ - args[argsCount++] = "-p"; + argsClient[argsCount++] = "-p"; snprintf(portNumber, sizeof(portNumber), "%d", ready.port); - args[argsCount++] = portNumber; + argsClient[argsCount++] = portNumber; #endif if (flag) - args[argsCount++] = "-N"; + argsClient[argsCount++] = "-N"; - cli.argv = (char**)args; + cli.argv = (char**)argsClient; cli.argc = argsCount; cli.signal = &ready; cli.sftp_cb = commandCb; From 045d5d66b258dc2b6927898987a62ea7e0ea9e73 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Tue, 1 Sep 2026 13:16:07 -0700 Subject: [PATCH 2/2] ssh: add wolfSSH_ChannelIdPeek() wolfSSH_ChannelIdPeek() reports what is buffered on the channel named by channelId without consuming it or crediting the window. It carries wolfSSH_stream_peek()'s contract, except that wolfSSH_stream_peek() only ever peeks the first channel in the list, and this peeks during a rekey the way wolfSSH_ChannelIdRead() reads during one. - a NULL buf asks only for the count, still capped at bufSz - a drained channel reports WS_EOF, or WS_DISCONNECT once the session is gone - test_ChannelIdPeek() covers the count, the bytes left unconsumed, the rekey case, and both drained reports --- src/ssh.c | 41 +++++++++++++++++ tests/unit.c | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++ wolfssh/ssh.h | 6 +++ 3 files changed, 168 insertions(+) diff --git a/src/ssh.c b/src/ssh.c index 5cc84b99c..30b66c918 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -5195,6 +5195,47 @@ static int _ChannelReadExt(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz) } +int wolfSSH_ChannelIdPeek(WOLFSSH* ssh, word32 channelId, + byte* buf, word32 bufSz) +{ + WOLFSSH_CHANNEL* channel = NULL; + WOLFSSH_BUFFER* inputBuffer; + word32 avail; + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_ChannelIdPeek(), ID = %u", channelId); + + if (ssh == NULL) + return WS_BAD_ARGUMENT; + + channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF); + if (channel == NULL) + return WS_INVALID_CHANID; + + inputBuffer = &channel->inputBuffer; + avail = inputBuffer->length - inputBuffer->idx; + + /* Report the EOF only once the buffered data is drained. */ + if (avail == 0 && channel->eofRxd) { + ssh->error = WS_EOF; + return WS_ERROR; + } + + /* The EOF above outranks this: it names the channel. */ + if (avail == 0 && ssh->disconnected) { + ssh->error = WS_DISCONNECT; + return WS_FATAL_ERROR; + } + + bufSz = min(bufSz, avail); + if (buf != NULL) { + WMEMCPY(buf, inputBuffer->buffer + inputBuffer->idx, bufSz); + } + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_ChannelIdPeek(), rxd = %u", bufSz); + return (int)bufSz; +} + + int wolfSSH_ChannelIdRead(WOLFSSH* ssh, word32 channelId, byte* buf, word32 bufSz) { diff --git a/tests/unit.c b/tests/unit.c index a7d0c6bab..6aa588e84 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -10006,6 +10006,122 @@ static int test_stream_read_deferredWindowAdjust(void) return result; } +/* wolfSSH_ChannelIdPeek() reports a named channel without consuming it. */ +static int test_ChannelIdPeek(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + byte in[32]; + byte out[32]; + word32 i; + + for (i = 0; i < (word32)sizeof(in); i++) { + in[i] = (byte)(i + 1); + } + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -7050; + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -7051; goto done; } + + /* Bad args, before any channel exists. */ + if (wolfSSH_ChannelIdPeek(NULL, 0, out, (word32)sizeof(out)) + != WS_BAD_ARGUMENT) { + result = -7052; goto done; + } + if (wolfSSH_ChannelIdPeek(ssh, 99, out, (word32)sizeof(out)) + != WS_INVALID_CHANID) { + result = -7053; goto done; + } + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, + (word32)sizeof(in), DEFAULT_MAX_PACKET_SZ); + if (ch == NULL) { result = -7054; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -7055; + goto done; + } + ch->openConfirmed = 1; + + /* Open and empty: zero, not an error. */ + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, out, (word32)sizeof(out)); + if (ret != 0) { result = -7056; goto done; } + + if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) { + result = -7057; goto done; + } + + /* Whole payload reported and copied out. */ + WMEMSET(out, 0, sizeof(out)); + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, out, (word32)sizeof(out)); + if (ret != (int)sizeof(in)) { result = -7058; goto done; } + if (WMEMCMP(out, in, sizeof(in)) != 0) { result = -7059; goto done; } + + /* Consumed nothing, credited nothing. */ + if (ch->inputBuffer.length - ch->inputBuffer.idx != (word32)sizeof(in)) { + result = -7060; goto done; + } + if (ch->windowSz != 0) { result = -7061; goto done; } + + /* bufSz caps the count and the bytes written. */ + WMEMSET(out, 0, sizeof(out)); + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, out, 8); + if (ret != 8) { result = -7062; goto done; } + if (WMEMCMP(out, in, 8) != 0) { result = -7063; goto done; } + if (out[8] != 0) { result = -7064; goto done; } + + /* A NULL buffer asks only for the count. */ + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, NULL, (word32)sizeof(out)); + if (ret != (int)sizeof(in)) { result = -7065; goto done; } + + /* Still reported mid-rekey, where stream_peek returns WS_REKEYING. */ + ssh->isKeying = WOLFSSH_SELF_IS_KEYING; + if (wolfSSH_stream_peek(ssh, out, (word32)sizeof(out)) != WS_REKEYING) { + result = -7066; goto done; + } + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, out, (word32)sizeof(out)); + if (ret != (int)sizeof(in)) { result = -7067; goto done; } + ssh->isKeying = 0; + + /* EOF stays hidden until the buffered data is drained. */ + ch->eofRxd = 1; + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, out, (word32)sizeof(out)); + if (ret != (int)sizeof(in)) { result = -7068; goto done; } + + ret = wolfSSH_ChannelIdRead(ssh, ch->channel, out, (word32)sizeof(out)); + if (ret != (int)sizeof(in)) { result = -7069; goto done; } + + ssh->error = WS_SUCCESS; + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, out, (word32)sizeof(out)); + if (ret != WS_ERROR) { result = -7070; goto done; } + if (ssh->error != WS_EOF) { result = -7071; goto done; } + + /* EOF outranks a disconnect. */ + ssh->disconnected = 1; + ssh->error = WS_SUCCESS; + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, out, (word32)sizeof(out)); + if (ret != WS_ERROR) { result = -7072; goto done; } + if (ssh->error != WS_EOF) { result = -7073; goto done; } + + /* Without the EOF, the dead session is reported instead. */ + ch->eofRxd = 0; + ssh->error = WS_SUCCESS; + ret = wolfSSH_ChannelIdPeek(ssh, ch->channel, out, (word32)sizeof(out)); + if (ret != WS_FATAL_ERROR) { result = -7074; goto done; } + if (ssh->error != WS_DISCONNECT) { result = -7075; goto done; } + +done: + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + /* wolfSSH_ChannelIdRead() counterpart of * test_stream_read_deferredWindowAdjust(): callers break out on a non-positive * read, and this entry point has to retire the owed-flush status itself. */ @@ -23094,6 +23210,11 @@ int wolfSSH_UnitTest(int argc, char** argv) printf("ChannelIdRead_deferredWindowAdjust: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + + unitResult = test_ChannelIdPeek(); + printf("ChannelIdPeek: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; #endif /* NO_WOLFSSH_SERVER */ unitResult = test_BuildNameList_emptySrc(); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 938db9459..d1e2d10b0 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -979,6 +979,12 @@ WOLFSSH_API int wolfSSH_global_request(WOLFSSH* ssh, const unsigned char* data, * wolfSSH_ChannelRead() returns WS_REKEYING. */ WOLFSSH_API int wolfSSH_ChannelIdRead(WOLFSSH* ssh, word32 channelId, byte* buf, word32 bufSz); +/* Peeks the channel named by channelId, with wolfSSH_stream_peek()'s + * contract, except that it peeks during a rekey the way + * wolfSSH_ChannelIdRead() reads during one. A NULL buf asks only for the + * count, still capped at bufSz. */ +WOLFSSH_API int wolfSSH_ChannelIdPeek(WOLFSSH* ssh, word32 channelId, + byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_ChannelIdSend(WOLFSSH* ssh, word32 channelId, byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_ChannelIdReadExt(WOLFSSH* ssh, word32 channelId,