From 454ad89d7a9b4c430ab60a58e6e37e1c60e1a109 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Wed, 2 Sep 2026 17:40:14 +0900 Subject: [PATCH 1/3] apps, examples: tolerate a worker want-write - The echoserver and Espressif shell loops and the Windows wolfsshd shell loop treat a WS_WANT_WRITE from wolfSSH_worker() as non-fatal. --- apps/wolfsshd/wolfsshd.c | 2 +- examples/echoserver/echoserver.c | 2 +- .../ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 6ab963265..513e9f83c 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -1361,7 +1361,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, * peer. Both want fixing where they can be tested. */ continue; } - else if (rc != WS_WANT_READ) { + else if (rc != WS_WANT_READ && rc != WS_WANT_WRITE) { break; } } diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 00320b7c8..118275344 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1205,7 +1205,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) * above, which has already run this pass. */ continue; } - else if (rc != WS_WANT_READ) { + else if (rc != WS_WANT_READ && rc != WS_WANT_WRITE) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", cnt_r, errno); diff --git a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c index 835cf670b..be688c628 100644 --- a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c +++ b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c @@ -1170,7 +1170,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) * above, which has already run this pass. */ continue; } - else if (rc != WS_WANT_READ) { + else if (rc != WS_WANT_READ && rc != WS_WANT_WRITE) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", cnt_r, errno); From 12c0d1a8c2cf17c52b310b087f7d44ea036b7664 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Wed, 2 Sep 2026 17:40:50 +0900 Subject: [PATCH 2/3] ssh: keep a successful rekey trigger out of ssh->error - wolfSSH_TriggerKeyExchange() writes ssh->error only when SendKexInit() fails. It runs from HighwaterCheck() inside wolfSSH_SendPacket(), so writing WS_SUCCESS there erased what the pass the mark fired on had already reported. - test_TriggerKeyExchangeKeepsError() seeds ssh->error and checks a rekey that starts cleanly leaves it alone. --- src/ssh.c | 7 +++++-- tests/unit.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/ssh.c b/src/ssh.c index c02609769..04f4cefdf 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1288,8 +1288,11 @@ int wolfSSH_TriggerKeyExchange(WOLFSSH* ssh) if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) ret = WS_FATAL_ERROR; - if (ret == WS_SUCCESS) - ret = ssh->error = SendKexInit(ssh); + if (ret == WS_SUCCESS) { + ret = SendKexInit(ssh); + if (ret != WS_SUCCESS) + ssh->error = ret; + } WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_TriggerKeyExchange(), ret = %d", ret); return ret; diff --git a/tests/unit.c b/tests/unit.c index 1007afcae..5ee92aedb 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -7167,6 +7167,46 @@ static int test_StreamReadHeadOpenFailed(void) } #endif /* NO_WOLFSSH_CLIENT */ +#ifndef NO_WOLFSSH_CLIENT +/* A rekey that starts cleanly leaves ssh->error alone. It runs from + * HighwaterCheck() inside wolfSSH_SendPacket(), so writing WS_SUCCESS there + * would erase what the pass the mark fired on had already reported. */ +static int test_TriggerKeyExchangeKeepsError(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + int result = 0; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) + return -1820; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1821; goto done; } + + /* The status the pass was carrying when the mark fired. */ + ssh->error = WS_CHANNEL_CLOSED; + + ret = wolfSSH_TriggerKeyExchange(ssh); + if (ret != WS_SUCCESS) { result = -1822; goto done; } + if (wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) { + result = -1823; + goto done; + } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} +#endif /* NO_WOLFSSH_CLIENT */ + /* A peer may half-close its channel before it makes its shell/exec/subsystem * request, RFC 4254 section 5.3. DoChannelEof() reports that as WS_EOF, and @@ -20027,6 +20067,13 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; +#ifndef NO_WOLFSSH_CLIENT + unitResult = test_TriggerKeyExchangeKeepsError(); + printf("TriggerKeyExchangeKeepsError: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + unitResult = test_StreamReadExtDataHeadChannel(); printf("StreamReadExtDataHeadChannel: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); From c9f9857ef06ed597fd4eef67d3f1b5df6ed37a26 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Wed, 2 Sep 2026 17:41:07 +0900 Subject: [PATCH 3/3] ssh, internal: always flush the worker's output - wolfSSH_worker() calls wolfSSH_SendPacket() whenever ssh->outputBuffer holds bytes and the session is not disconnected, in place of doing so only for WS_SUCCESS, WS_WANT_READ, WS_CHAN_RXD or WS_EOF. ssh->error keeps the receive's code when the receive failed, and the close's when a WS_CHANNEL_CLOSED pass hard-failed its flush; a function-scope sendRet also masks the WS_REKEYING report. Drops the second DoReceive(), its WS_WINDOW_FULL case, the WOLFSSH_TEST_BLOCK fork, and the separate WS_CHANNEL_CLOSED flush. - BundlePacket() resets ssh->outputBuffer.length to ssh->packetStartIdx when the framing fails, and wolfSSH_shutdown() reports WS_WANT_WRITE when its read for the peer's close leaves output queued. - SendPacketFlush() records its code in ssh->error on every transport failure path; wolfSSH_SendPacket() says so and what a later write to that field owes it. - wolfssh/ssh.h drops WS_WINDOW_FULL from wolfSSH_worker() and names the two exceptions to a status surviving a failed flush: a hard-failed WS_CHANNEL_CLOSED, and WS_REKEYING. Comments in ssh.c, unit.c, the echoserver, the Espressif copy and portfwd name the channel's own state or the failing call instead of restating a contract. - Sixteen unit tests and the extended TestWorkerReportsDisconnect cover what ret and ssh->error hold after a receive, send, buffer, callback or framing failure, against channel data, extended data, a half-close, a rekey, a close, and a teardown read. The #ifndef WOLFSSH_TEST_BLOCK guards around TestWorkerReadsWhenSendWouldBlock go with the send-first fork. --- examples/echoserver/echoserver.c | 9 +- examples/portfwd/portfwd.c | 7 +- .../wolfssh_echoserver/main/echoserver.c | 9 +- src/internal.c | 12 +- src/ssh.c | 85 +- tests/regress.c | 16 +- tests/unit.c | 1284 +++++++++++++++-- wolfssh/ssh.h | 23 +- 8 files changed, 1243 insertions(+), 202 deletions(-) diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 118275344..77a7906a6 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1016,12 +1016,9 @@ static int ssh_worker(thread_ctx_t* threadCtx) rc = wolfSSH_get_error(ssh); /* The peer is done sending: hand back the backlog and answer - * its EOF, or a client that half-closed waits on a server - * that never finishes -- the library no longer answers for - * us. Off the channel's own state, not the WS_EOF status: the - * flush inside wolfSSH_worker() can supersede that, and it is - * raised once. Echo mode only; a shell child on a pty is - * still producing, so its EOF waits for the child to exit. */ + * its EOF, since the library no longer answers for us. Off + * the channel's own state, not the once-only WS_EOF status. + * Echo mode only; a shell child on a pty still produces. */ if (!eofAnswered && echoOnly) { WOLFSSH_CHANNEL* eofChannel; diff --git a/examples/portfwd/portfwd.c b/examples/portfwd/portfwd.c index e406368dc..de2c270f3 100644 --- a/examples/portfwd/portfwd.c +++ b/examples/portfwd/portfwd.c @@ -781,10 +781,9 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) /* Relay the half-close so a local reader waiting on end-of-input * returns; nothing else relays it. Driven off the latched channel - * state, not the WS_EOF status: the flush inside wolfSSH_worker() - * can supersede that, and it is raised only once. Only the channel - * appFd is wired to, since half-closing the wrong socket truncates - * a live transfer. */ + * state, not the once-only WS_EOF status. Only the channel appFd + * is wired to: half-closing the wrong socket truncates a live + * transfer. */ if (appFdSet && fwdChannel != NULL && !appFdHalfClosed && wolfSSH_ChannelGetEof(fwdChannel)) { int drained; diff --git a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c index be688c628..f55b0212f 100644 --- a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c +++ b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c @@ -1000,12 +1000,9 @@ static int ssh_worker(thread_ctx_t* threadCtx) rc = wolfSSH_get_error(ssh); /* The peer is done sending: hand back the backlog and answer - * its EOF, or a client that half-closed waits on a server - * that never finishes -- the library no longer answers for - * us. Off the channel's own state, not the WS_EOF status: the - * flush inside wolfSSH_worker() can supersede that, and it is - * raised once. Echo mode only; a shell child on a pty is - * still producing, so its EOF waits for the child to exit. */ + * its EOF, since the library no longer answers for us. Off + * the channel's own state, not the once-only WS_EOF status. + * Echo mode only; a shell child on a pty still produces. */ if (!eofAnswered && echoOnly) { WOLFSSH_CHANNEL* eofChannel; diff --git a/src/internal.c b/src/internal.c index 81e41ff9a..216a8b7e2 100644 --- a/src/internal.c +++ b/src/internal.c @@ -5142,6 +5142,7 @@ static int SendPacketFlush(WOLFSSH* ssh) if (ssh->ctx->ioSendCb == NULL) { WLOG(WS_LOG_DEBUG, "Your IO Send callback is null, please set"); + ssh->error = WS_SOCKET_ERROR_E; return WS_SOCKET_ERROR_E; } @@ -5152,6 +5153,7 @@ static int SendPacketFlush(WOLFSSH* ssh) if (ssh->outputBuffer.length > ssh->outputBuffer.bufferSz || ssh->outputBuffer.length < ssh->outputBuffer.idx) { WLOG(WS_LOG_ERROR, "Bad buffer state"); + ssh->error = WS_BUFFER_E; return WS_BUFFER_E; } @@ -5190,11 +5192,13 @@ static int SendPacketFlush(WOLFSSH* ssh) ssh->outputBuffer.plainSz = 0; ShrinkBuffer(&ssh->outputBuffer, 1); } + ssh->error = WS_SOCKET_ERROR_E; return WS_SOCKET_ERROR_E; } if ((word32)sent > ssh->outputBuffer.length) { WLOG(WS_LOG_DEBUG, "wolfSSH_SendPacket() out of bounds read"); + ssh->error = WS_SEND_OOB_READ_E; return WS_SEND_OOB_READ_E; } @@ -5219,7 +5223,9 @@ static int SendPacketFlush(WOLFSSH* ssh) } -/* returns WS_SUCCESS on success */ +/* returns WS_SUCCESS on success. Transport failures record their code in + * ssh->error, so a later write to that field on the same pass has to be + * conditional on this having succeeded, or it hides the dead transport. */ int wolfSSH_SendPacket(WOLFSSH* ssh) { int ret; @@ -14364,6 +14370,10 @@ static int BundlePacket(WOLFSSH* ssh) } else { WLOG(WS_LOG_DEBUG, "BP: failed to encrypt buffer"); + if (ssh != NULL) { + /* Drop the aborted packet */ + ssh->outputBuffer.length = ssh->packetStartIdx; + } } return ret; diff --git a/src/ssh.c b/src/ssh.c index 04f4cefdf..eacf9f186 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1252,6 +1252,9 @@ int wolfSSH_shutdown(WOLFSSH* ssh) /* received response */ ret = WS_SUCCESS; } + /* A reply queued during that read has not gone out yet. */ + if (ret == WS_SUCCESS && wolfSSH_OutputPending(ssh)) + ret = WS_WANT_WRITE; } if (ssh != NULL && ssh->channelList == NULL) { @@ -3734,6 +3737,7 @@ const char* wolfSSH_GetSessionCommand(const WOLFSSH* ssh) int wolfSSH_worker(WOLFSSH* ssh, word32* channelId) { int ret = WS_SUCCESS; + int sendRet = WS_SUCCESS; WLOG(WS_LOG_DEBUG, "Entering wolfSSH_worker()"); @@ -3750,59 +3754,30 @@ int wolfSSH_worker(WOLFSSH* ssh, word32* channelId) return WS_FATAL_ERROR; } -#ifdef WOLFSSH_TEST_BLOCK - /* In forced non-blocking test mode, keep legacy ordering (send before - * receive) to match the harness expectations and avoid synthetic spins. */ - if (ret == WS_SUCCESS) { - if (ssh->outputBuffer.length != 0) - ret = wolfSSH_SendPacket(ssh); - } - if (ret == WS_SUCCESS) - ret = DoReceive(ssh); -#else /* Always service inbound data first so window updates can unblock sends. */ if (ret == WS_SUCCESS) { ret = DoReceive(ssh); } - /* If receive only wanted read or delivered channel data, still try to - * flush any pending outbound packets. */ - if (ret == WS_SUCCESS || ret == WS_WANT_READ || ret == WS_CHAN_RXD - || ret == WS_EOF) { - int sendRet = WS_SUCCESS; - - if (ssh->outputBuffer.length != 0) - sendRet = wolfSSH_SendPacket(ssh); - - /* If send is back-pressured, immediately try another receive to pick - * up potential window-adjusts and then return the send status. The - * send status wins; a peer EOF stays latched on the channel. */ - if (sendRet == WS_WANT_WRITE || sendRet == WS_WINDOW_FULL) { - int recv2 = DoReceive(ssh); - if (recv2 == WS_SUCCESS || recv2 == WS_WANT_READ || recv2 == WS_CHAN_RXD - || recv2 == WS_EOF) - ret = sendRet; - else - ret = recv2; - } - else { - /* Preserve meaningful receive status when send succeeded. */ - if (sendRet != WS_SUCCESS) + /* Flush queued output whatever DoReceive() made of the socket, since an + * idle receive reports WS_FATAL_ERROR. !ssh->disconnected gates it. */ + if (ssh != NULL && !ssh->disconnected && ssh->outputBuffer.length != 0) { + int rxErr = ssh->error; + + sendRet = wolfSSH_SendPacket(ssh); + if (sendRet != WS_SUCCESS) { + if (ret == WS_SUCCESS) { ret = sendRet; - /* else leave ret as prior receive result (SUCCESS/WANT_READ/CHAN_RXD). */ + } + else if ((ret == WS_CHANNEL_CLOSED && sendRet != WS_WANT_WRITE) + || (ret == WS_FATAL_ERROR && rxErr != WS_WANT_READ)) { + /* A failed receive outranks the flush, and so does a close + * whose flush hard-failed: callers route teardown on it. + * Every other status keeps the code the send set. */ + ssh->error = rxErr; + } } } -#endif /* WOLFSSH_TEST_BLOCK */ - - /* DoChannelClose() bundles the reply inside DoReceive(), and callers - * treat the close as terminal, so flush it here. The close stays the - * return value; a short flush leaves WS_WANT_WRITE latched. */ - if (ret == WS_CHANNEL_CLOSED && ssh->outputBuffer.length != 0) { - int closeErr = ssh->error; - - if (wolfSSH_SendPacket(ssh) == WS_SUCCESS) - ssh->error = closeErr; - } /* WS_EXTDATA and WS_EOF report the channel too, so a multi-channel caller * can route the drain, or see which channel half-closed. */ @@ -3812,12 +3787,10 @@ int wolfSSH_worker(WOLFSSH* ssh, word32* channelId) *channelId = ssh->lastRxId; } - /* WS_EXTDATA and WS_EOF are raised once, on arrival; masking either - * strands the event, and the stderr window credit with it. A - * disconnect cannot be seen here: the gate at the top returns before - * this, and the DISCONNECT that sets the flag mid-pass leaves ret - * fatal. */ - if (ssh->isKeying && ret != WS_EXTDATA && ret != WS_EOF) { + /* Report the rekey, unless it would hide a once-only WS_EXTDATA + * or WS_EOF, or the error from a flush that failed. */ + if (ssh->isKeying && ret != WS_EXTDATA && ret != WS_EOF + && sendRet == WS_SUCCESS) { ssh->error = WS_REKEYING; return WS_REKEYING; } @@ -4255,8 +4228,9 @@ static int _ChannelRead(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz) } } else { - /* SendPacket() records only WS_WANT_WRITE, so a hard failure has to - * be recorded here; rewriting WS_WANT_WRITE is deliberate. */ + /* The adjust can fail before it reaches the transport, so the code + * is recorded here; the log skips a WS_WANT_WRITE, which only asks + * for a retry. */ ssh->error = updateResult; if (updateResult != WS_WANT_WRITE) { WLOG(WS_LOG_ERROR, @@ -4316,8 +4290,9 @@ static int _ChannelReadExt(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz) ssh->error = savedError; } else { - /* SendPacket() sets ssh->error only for WS_WANT_WRITE, so hard - * failures must be recorded here or they stay hidden. */ + /* The adjust can fail before it reaches the transport, so the + * code is recorded here; the log skips a WS_WANT_WRITE, which + * only asks for a retry. */ ssh->error = adjustResult; if (adjustResult != WS_WANT_WRITE) { WLOG(WS_LOG_ERROR, diff --git a/tests/regress.c b/tests/regress.c index 9f6cd451a..d2be5c64b 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -8277,11 +8277,19 @@ static void TestWorkerReportsDisconnect(void) wolfSSH_SetIOReadCtx(ssh, &io); wolfSSH_SetIOWriteCtx(ssh, &io); + /* Queued output, so the flush on this pass has something to push. */ + ssh->outputBuffer.length = 1; + ssh->outputBuffer.idx = 0; + ssh->outputBuffer.buffer[0] = 0; + AssertIntEQ(wolfSSH_worker(ssh, NULL), WS_FATAL_ERROR); AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); AssertTrue(ssh->disconnected); AssertTrue(ssh->isKeying != 0); - io.outSz = 0; + + /* The queued byte stays put: the session ended on this very pass. */ + AssertIntEQ(io.outSz, 0); + AssertTrue(wolfSSH_OutputPending(ssh)); /* The message behind it is still queued, and every further pass reports * the disconnect rather than the WS_SUCCESS of a skipped dispatch or the @@ -8954,7 +8962,6 @@ static void TestPasswordEofNoCrash(void) * still needs to service Receive() so window-adjusts can arrive and * unblock the flow control. Verify the receive callback is invoked even * when the first send attempt would block. */ -#ifndef WOLFSSH_TEST_BLOCK static int recvCallCount; static int WantWriteSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) @@ -8970,7 +8977,6 @@ static int WantReadRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) return WS_CBIO_ERR_WANT_READ; } -#ifndef WOLFSSH_TEST_BLOCK static void TestWorkerReadsWhenSendWouldBlock(void) { WOLFSSH_CTX* ctx; @@ -9005,8 +9011,6 @@ static void TestWorkerReadsWhenSendWouldBlock(void) wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } -#endif /* !WOLFSSH_TEST_BLOCK */ -#endif #ifdef WOLFSSH_SFTP @@ -13303,9 +13307,7 @@ int main(int argc, char** argv) TestClientBuffersIdempotent(); #endif TestPasswordEofNoCrash(); -#ifndef WOLFSSH_TEST_BLOCK TestWorkerReadsWhenSendWouldBlock(); -#endif #ifdef KEXDH_REPLY_REGRESS_KEX_ALGO #ifndef WOLFSSH_NO_RSA_SHA2_256 diff --git a/tests/unit.c b/tests/unit.c index 5ee92aedb..0d873dc6f 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -4373,6 +4373,22 @@ static WS_MAYBE_UNUSED int WantWriteIoSend(WOLFSSH* ssh, void* buf, word32 sz, return WS_CBIO_ERR_WANT_WRITE; } +/* A socket the peer reset under the send. */ +static WS_MAYBE_UNUSED int ConnResetIoSend(WOLFSSH* ssh, void* buf, word32 sz, + void* ctx) +{ + (void)ssh; (void)buf; (void)sz; (void)ctx; + return WS_CBIO_ERR_CONN_RST; +} + +/* A send callback that claims more bytes than it was handed. */ +static WS_MAYBE_UNUSED int OobIoSend(WOLFSSH* ssh, void* buf, word32 sz, + void* ctx) +{ + (void)ssh; (void)buf; (void)ctx; + return (int)sz + 1; +} + static int test_DoChannelExtendedData_overflow(void) { WOLFSSH_CTX* ctx = NULL; @@ -6105,38 +6121,932 @@ static int test_StreamReadExtDataOtherChannel(void) goto done; } - ch1 = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); - if (ch1 == NULL) { result = -1364; goto done; } - if (ChannelAppend(ssh, ch1) != WS_SUCCESS) { - ChannelDelete(ch1, ssh->ctx->heap); - result = -1365; + ch1 = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); + if (ch1 == NULL) { result = -1364; goto done; } + if (ChannelAppend(ssh, ch1) != WS_SUCCESS) { + ChannelDelete(ch1, ssh->ctx->heap); + result = -1365; + goto done; + } + + /* The first channel appended is the head that stream_read() drains. */ + if (ssh->channelList != ch0) { result = -1366; goto done; } + + /* Stage stderr for the non-head channel. */ + s_recvPkt = pkt; + s_recvPktSz = BuildExtDataStderrPacket(pkt, ch1->channel, 0x11); + s_recvPktOff = 0; + + /* Reading the head channel receives the packet, sees it is for another + * channel, and reports WS_ERROR instead of the extended data. */ + ret = wolfSSH_stream_read(ssh, out, (word32)sizeof(out)); + if (ret != WS_ERROR) { result = -1367; goto done; } + + /* The bytes were buffered on ch1 and its window charged, not lost. */ + if (ch1->windowSz != 1024 - 10) { result = -1368; goto done; } + if (ch0->windowSz != 1024) { result = -1369; goto done; } + + /* A multi-channel app recovers them with the id-addressed reader. */ + ret = wolfSSH_ChannelIdReadExt(ssh, ch1->channel, out, + (word32)sizeof(out)); + if (ret != 10) { result = -1370; goto done; } + for (i = 0; i < 10; i++) + if (out[i] != 0x11) { result = -1371; goto done; } + if (ch1->windowSz != 1024) { result = -1372; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* Integration (L-5): wolfSSH_worker() reports the channel that extended data + * arrived on so a multi-channel caller can route the drain to + * wolfSSH_ChannelIdReadExt(). Drives a crafted stderr packet through the real + * receive path and confirms worker() returns WS_EXTDATA and writes the + * receiving channel's id into *channelId, and that the data then drains. */ +static int test_WorkerReportsExtDataChannel(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + int i; + word32 reportedId = 0xFFFFFFFF; + byte pkt[32]; + byte out[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1380; + wolfSSH_SetIOSend(ctx, DiscardIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1381; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); + if (ch == NULL) { result = -1382; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1383; + goto done; + } + + s_recvPkt = pkt; + s_recvPktSz = BuildExtDataStderrPacket(pkt, ch->channel, 0x22); + s_recvPktOff = 0; + + ret = wolfSSH_worker(ssh, &reportedId); + if (ret != WS_EXTDATA) { result = -1384; goto done; } + if (reportedId != ch->channel) { result = -1385; goto done; } + if (ch->windowSz != 1024 - 10) { result = -1386; goto done; } + + /* The reported channel is the one to drain. */ + ret = wolfSSH_ChannelIdReadExt(ssh, reportedId, out, (word32)sizeof(out)); + if (ret != 10) { result = -1387; goto done; } + for (i = 0; i < 10; i++) + if (out[i] != 0x22) { result = -1388; goto done; } + if (ch->windowSz != 1024) { result = -1389; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* Regression: extended data arriving while a rekey is in flight must still be + * reported. WS_EXTDATA is raised once, on arrival, so folding it into + * WS_REKEYING at the isKeying check in wolfSSH_worker() strands the buffered + * stderr and the window credit already charged for it -- nothing re-raises it. + * RFC 4253 section 7.1 requires accepting data the peer sent before it saw our + * KEXINIT, so this is a normal sequence, not a corner case. */ +static int test_WorkerReportsExtDataChannelKeying(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + int i; + word32 reportedId = 0xFFFFFFFF; + byte pkt[32]; + byte out[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1390; + wolfSSH_SetIOSend(ctx, DiscardIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1391; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); + if (ch == NULL) { result = -1392; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1393; + goto done; + } + + s_recvPkt = pkt; + s_recvPktSz = BuildExtDataStderrPacket(pkt, ch->channel, 0x33); + s_recvPktOff = 0; + + /* A rekey is underway when the stderr packet lands. */ + ssh->isKeying = WOLFSSH_SELF_IS_KEYING; + + ret = wolfSSH_worker(ssh, &reportedId); + if (ret != WS_EXTDATA) { result = -1394; goto done; } + if (reportedId != ch->channel) { result = -1395; goto done; } + if (ch->windowSz != 1024 - 10) { result = -1396; goto done; } + + /* The drain works mid-rekey; the credit parks rather than going out, since + * no WINDOW_ADJUST may be sent during KEX. */ + ret = wolfSSH_ChannelIdReadExt(ssh, reportedId, out, (word32)sizeof(out)); + if (ret != 10) { result = -1397; goto done; } + for (i = 0; i < 10; i++) + if (out[i] != 0x33) { result = -1398; goto done; } + if (ch->windowSz != 1024) { result = -1399; goto done; } + if (ch->pendingWindowAdjust != 10) { result = -1400; goto done; } + + /* Once keying completes the parked credit flushes. */ + ssh->isKeying = 0; + ret = wolfSSH_TestSendPendingChannelWindowAdjust(ssh); + if (ret != WS_SUCCESS) { result = -1401; goto done; } + if (ch->pendingWindowAdjust != 0) { result = -1402; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* channelId=0, type=1 (stderr), dataSz=10, payload all 0x44. */ +static const byte s_workerExtBlob[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x0A, + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44 +}; + +/* Bundles a window adjust into ssh->outputBuffer with the send blocked, + * then leaves the receive idle. */ +static int WorkerParkAdjust(WOLFSSH_CTX* ctx, WOLFSSH* ssh, + WOLFSSH_CHANNEL* channel) +{ + word32 idx = 0; + int ret; + byte out[32]; + + ret = wolfSSH_TestDoChannelExtendedData(ssh, (byte*)s_workerExtBlob, + (word32)sizeof(s_workerExtBlob), + &idx); + if (ret != WS_EXTDATA) + return WS_FATAL_ERROR; + if (channel->windowSz != 118) + return WS_FATAL_ERROR; + + wolfSSH_SetIOSend(ctx, WantWriteIoSend); + + ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out)); + if (ret != 10) + return WS_FATAL_ERROR; + if (ssh->outputBuffer.length == 0) + return WS_FATAL_ERROR; + if (channel->pendingWindowAdjust != 0) + return WS_FATAL_ERROR; + + /* No staged packet, so PacketIoRecv reports want-read. */ + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + + return WS_SUCCESS; +} + +/* A window adjust a short write left in ssh->outputBuffer goes out on a + * later wolfSSH_worker() call. The peer is out of window and sends nothing + * until it arrives, so the receive stays idle. */ +static int test_WorkerFlushesOnIdleReceive(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1700; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1701; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1702; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1703; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1704; + goto done; + } + + /* The socket takes writes again. */ + wolfSSH_SetIOSend(ctx, CountIoSend); + s_extSendCount = 0; + + ret = wolfSSH_worker(ssh, NULL); + + if (s_extSendCount != 1) { result = -1705; goto done; } + if (ssh->outputBuffer.length != 0) { result = -1706; goto done; } + + /* Nothing left queued, so the call reports the idle receive. */ + if (ret != WS_FATAL_ERROR) { result = -1707; goto done; } + if (wolfSSH_get_error(ssh) != WS_WANT_READ) { result = -1708; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* While the flush stays short, ssh->error keeps reporting WS_WANT_WRITE so + * the caller selects for writability instead of waiting on a read the peer + * cannot produce. */ +static int test_WorkerReportsOwedFlush(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1710; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1711; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1712; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1713; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1714; + goto done; + } + + /* The send still blocks, so the flush stays owed across both calls. */ + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_FATAL_ERROR) { result = -1715; goto done; } + if (wolfSSH_get_error(ssh) != WS_WANT_WRITE) { result = -1716; goto done; } + if (ssh->outputBuffer.length == 0) { result = -1717; goto done; } + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_FATAL_ERROR) { result = -1718; goto done; } + if (wolfSSH_get_error(ssh) != WS_WANT_WRITE) { result = -1719; goto done; } + if (ssh->outputBuffer.length == 0) { result = -1720; goto done; } + + /* Bundled credit is owed by the output buffer, not the channel. */ + if (ch->pendingWindowAdjust != 0) { result = -1721; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* A receive that failed keeps ssh->error: the flush runs on the same call + * and wolfSSH_SendPacket() would otherwise leave WS_WANT_WRITE there, which + * reads as transient and would have the caller retry a dead session. */ +static int test_WorkerHardRecvErrorOutranksFlush(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + byte pkt[8]; + + /* packet_length far past MAX_PACKET_SZ, so DoReceive() fails the length + * check with WS_OVERFLOW_E instead of blocking. */ + static const byte badLen[8] = { + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1730; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1731; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1732; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1733; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1734; + goto done; + } + + WMEMCPY(pkt, badLen, sizeof(pkt)); + s_recvPkt = pkt; + s_recvPktSz = (word32)sizeof(pkt); + s_recvPktOff = 0; + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_FATAL_ERROR) { result = -1735; goto done; } + if (wolfSSH_get_error(ssh) != WS_OVERFLOW_E) { result = -1736; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* A hard send failure on a pass whose receive was idle leaves + * WS_SOCKET_ERROR_E in ssh->error, not the receive's WS_WANT_READ. A caller + * routing on that would select for read on a reset socket. */ +static int test_WorkerHardSendErrorOnIdleReceive(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1740; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1741; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1742; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1743; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1744; + goto done; + } + + /* The receive stays idle and the peer resets under the flush. */ + wolfSSH_SetIOSend(ctx, ConnResetIoSend); + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_FATAL_ERROR) { result = -1745; goto done; } + if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) { + result = -1746; + goto done; + } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* Channel data arrives and the flush fails on the same call. ret carries + * WS_CHAN_RXD so the caller reads the data, and ssh->error carries the send + * failure, which is the rule wolfssh/ssh.h states for wolfSSH_worker(). */ +static int test_WorkerChanRxdSurfacesSendError(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + byte pkt[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1750; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1751; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1752; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1753; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1754; + goto done; + } + + /* Channel data arrives, and the peer resets the socket on the send. */ + s_recvPkt = pkt; + s_recvPktSz = BuildChannelDataPacket(pkt, ch->channel, 0x55); + s_recvPktOff = 0; + wolfSSH_SetIOSend(ctx, ConnResetIoSend); + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_CHAN_RXD) { result = -1755; goto done; } + if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) { + result = -1756; + goto done; + } + /* A reset does not discard, so the bytes stay owed. */ + if (ssh->outputBuffer.length == 0) { result = -1757; goto done; } + + /* And the failure persists rather than being a one-pass artefact. */ + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_FATAL_ERROR) { result = -1758; goto done; } + if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) { + result = -1759; + goto done; + } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +#ifndef NO_WOLFSSH_CLIENT +/* A high water mark firing on wolfSSH_shutdown()'s read sends a KEXINIT. + * If the socket will not take it, the teardown must not return success. */ +static int test_ShutdownReportsWorkerOwedFlush(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + byte pkt[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) + return -1860; + wolfSSH_SetIOSend(ctx, WantWriteIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1861; goto done; } + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); + if (ch == NULL) { result = -1862; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1863; + goto done; + } + ch->openConfirmed = 1; + ch->peerWindowSz = 1024; + ch->peerMaxPacketSz = 1024; + + /* The teardown sends are already done, so the read is all that is left + * and the output buffer is empty going into it. */ + ch->eofTxd = 1; + ch->closeTxd = 1; + + /* The mark fires on the first packet received. */ + if (wolfSSH_SetHighwater(ssh, 1) != WS_SUCCESS) { + result = -1864; + goto done; + } + + s_recvPkt = pkt; + s_recvPktSz = BuildChannelDataPacket(pkt, ch->channel, 0xAA); + s_recvPktOff = 0; + + ret = wolfSSH_shutdown(ssh); + if (ret != WS_WANT_WRITE) { result = -1865; goto done; } + if (!wolfSSH_OutputPending(ssh)) { result = -1866; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} +#endif /* NO_WOLFSSH_CLIENT */ + + +/* A packet whose framing fails leaves nothing queued behind it. The buffer + * keeps the packets already bundled, so only the aborted one is dropped. */ +static int test_BundlePacketFailureDropsPartial(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + word32 queued; + byte bundled = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1850; + wolfSSH_SetIOSend(ctx, WantWriteIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1851; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1852; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1853; + goto done; + } + + /* One adjust frames cleanly and parks in the buffer, since the send + * blocks. */ + ret = SendChannelWindowAdjust(ssh, ch->channel, 10, &bundled); + if (ret != WS_WANT_WRITE) { result = -1854; goto done; } + queued = ssh->outputBuffer.length; + if (queued == 0) { result = -1855; goto done; } + + /* Encrypt() has no case for ID_UNKNOWN, so the next bundle fails after + * PreparePacket() has already moved length past packetStartIdx. */ + ssh->encryptId = ID_UNKNOWN; + + ret = SendChannelWindowAdjust(ssh, ch->channel, 10, &bundled); + if (ret == WS_SUCCESS) { result = -1856; goto done; } + + /* The aborted packet is gone and the framed one is untouched. */ + if (ssh->outputBuffer.length != queued) { result = -1857; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + + +/* Extended data arrives and the flush fails on the same call. Neither the + * close nor the receive-failure rule applies, so ret keeps WS_EXTDATA and + * ssh->error carries the send failure. */ +static int test_WorkerExtDataSurfacesSendError(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + word32 reportedId = 0xFFFFFFFF; + byte pkt[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1830; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1831; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1832; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1833; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1834; + goto done; + } + + /* Stderr arrives, and the peer resets the socket on the send. */ + s_recvPkt = pkt; + s_recvPktSz = BuildExtDataStderrPacket(pkt, ch->channel, 0x99); + s_recvPktOff = 0; + wolfSSH_SetIOSend(ctx, ConnResetIoSend); + + ret = wolfSSH_worker(ssh, &reportedId); + if (ret != WS_EXTDATA) { result = -1835; goto done; } + if (reportedId != ch->channel) { result = -1836; goto done; } + if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) { + result = -1837; + goto done; + } + /* A reset does not discard, so the bytes stay owed. */ + if (ssh->outputBuffer.length == 0) { result = -1838; goto done; } + + /* The stderr is still there to drain, which is why ret kept it. */ + if (ch->extDataBuffer.length - ch->extDataBuffer.idx != 10) { + result = -1839; + goto done; + } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + + +/* The peer half-closes and the flush fails on the same call. ret keeps + * WS_EOF and ssh->error carries the send failure. */ +static int test_WorkerEofSurfacesSendError(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + word32 reportedId = 0xFFFFFFFF; + byte pkt[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1840; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1841; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1842; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1843; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1844; + goto done; + } + + /* The half-close arrives, and the peer resets the socket on the send. */ + s_recvPkt = pkt; + s_recvPktSz = BuildChannelEofPacket(pkt, ch->channel); + s_recvPktOff = 0; + wolfSSH_SetIOSend(ctx, ConnResetIoSend); + + ret = wolfSSH_worker(ssh, &reportedId); + if (ret != WS_EOF) { result = -1845; goto done; } + if (reportedId != ch->channel) { result = -1846; goto done; } + if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) { + result = -1847; + goto done; + } + /* A reset does not discard, so the bytes stay owed. */ + if (ssh->outputBuffer.length == 0) { result = -1848; goto done; } + + /* The half-close is latched, which is the durable half of the report. */ + if (!wolfSSH_ChannelGetEof(ch)) { result = -1849; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + + +/* The same pass with a rekey in flight. WS_REKEYING would tell the caller to + * keep turning the crank, so a flush that hard-failed keeps ssh->error and + * the rekey mask stands down. */ +static int test_WorkerKeyingSurfacesSendError(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + byte pkt[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1800; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1801; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1802; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1803; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1804; + goto done; + } + + s_recvPkt = pkt; + s_recvPktSz = BuildChannelDataPacket(pkt, ch->channel, 0x77); + s_recvPktOff = 0; + wolfSSH_SetIOSend(ctx, ConnResetIoSend); + ssh->isKeying = WOLFSSH_SELF_IS_KEYING; + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_CHAN_RXD) { result = -1805; goto done; } + if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) { + result = -1806; + goto done; + } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* The other arm: a rekey in flight and a flush that went out. The worker + * reports WS_REKEYING so the caller keeps driving the rekey. */ +static int test_WorkerKeyingReportsRekey(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + byte pkt[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1810; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1811; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1812; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1813; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1814; + goto done; + } + + /* Channel data arrives and the socket takes the flush. */ + s_recvPkt = pkt; + s_recvPktSz = BuildChannelDataPacket(pkt, ch->channel, 0x88); + s_recvPktOff = 0; + wolfSSH_SetIOSend(ctx, CountIoSend); + ssh->isKeying = WOLFSSH_SELF_IS_KEYING; + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_REKEYING) { result = -1815; goto done; } + if (wolfSSH_get_error(ssh) != WS_REKEYING) { + result = -1816; + goto done; + } + /* The flush ran and drained, which the rekey report is gated on. */ + if (ssh->outputBuffer.length != 0) { result = -1817; goto done; } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* A send that fails with WS_CBIO_ERR_GENERAL discards the output buffer, so + * no later call retries the flush. ssh->error has to keep the send failure + * even though the receive reported channel data. */ +static int test_WorkerDiscardedFlushKeepsError(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + byte pkt[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1760; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1761; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1762; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1763; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1764; goto done; } - /* The first channel appended is the head that stream_read() drains. */ - if (ssh->channelList != ch0) { result = -1366; goto done; } - - /* Stage stderr for the non-head channel. */ + /* Channel data arrives, and the send throws the queued adjust away. */ s_recvPkt = pkt; - s_recvPktSz = BuildExtDataStderrPacket(pkt, ch1->channel, 0x11); + s_recvPktSz = BuildChannelDataPacket(pkt, ch->channel, 0x66); s_recvPktOff = 0; + wolfSSH_SetIOSend(ctx, FailIoSend); - /* Reading the head channel receives the packet, sees it is for another - * channel, and reports WS_ERROR instead of the extended data. */ - ret = wolfSSH_stream_read(ssh, out, (word32)sizeof(out)); - if (ret != WS_ERROR) { result = -1367; goto done; } - - /* The bytes were buffered on ch1 and its window charged, not lost. */ - if (ch1->windowSz != 1024 - 10) { result = -1368; goto done; } - if (ch0->windowSz != 1024) { result = -1369; goto done; } - - /* A multi-channel app recovers them with the id-addressed reader. */ - ret = wolfSSH_ChannelIdReadExt(ssh, ch1->channel, out, - (word32)sizeof(out)); - if (ret != 10) { result = -1370; goto done; } - for (i = 0; i < 10; i++) - if (out[i] != 0x11) { result = -1371; goto done; } - if (ch1->windowSz != 1024) { result = -1372; goto done; } + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_CHAN_RXD) { result = -1765; goto done; } + /* Nothing is left to flush, so this is the only report there will be. */ + if (ssh->outputBuffer.length != 0) { result = -1766; goto done; } + if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) { + result = -1767; + goto done; + } done: s_recvPkt = NULL; @@ -6147,56 +7057,48 @@ static int test_StreamReadExtDataOtherChannel(void) return result; } -/* Integration (L-5): wolfSSH_worker() reports the channel that extended data - * arrived on so a multi-channel caller can route the drain to - * wolfSSH_ChannelIdReadExt(). Drives a crafted stderr packet through the real - * receive path and confirms worker() returns WS_EXTDATA and writes the - * receiving channel's id into *channelId, and that the data then drains. */ -static int test_WorkerReportsExtDataChannel(void) +/* A send callback reporting more bytes than it was handed trips the + * out-of-bounds check, and ssh->error carries WS_SEND_OOB_READ_E. */ +static int test_WorkerSendOobReadReported(void) { WOLFSSH_CTX* ctx = NULL; WOLFSSH* ssh = NULL; WOLFSSH_CHANNEL* ch = NULL; int result = 0; int ret; - int i; - word32 reportedId = 0xFFFFFFFF; - byte pkt[32]; - byte out[32]; ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (ctx == NULL) - return -1380; - wolfSSH_SetIOSend(ctx, DiscardIoSend); + return -1770; + wolfSSH_SetIOSend(ctx, CountIoSend); wolfSSH_SetIORecv(ctx, PacketIoRecv); ssh = wolfSSH_new(ctx); - if (ssh == NULL) { result = -1381; goto done; } + if (ssh == NULL) { result = -1771; goto done; } ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; - ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); - if (ch == NULL) { result = -1382; goto done; } + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1772; goto done; } if (ChannelAppend(ssh, ch) != WS_SUCCESS) { ChannelDelete(ch, ssh->ctx->heap); - result = -1383; + result = -1773; goto done; } - s_recvPkt = pkt; - s_recvPktSz = BuildExtDataStderrPacket(pkt, ch->channel, 0x22); - s_recvPktOff = 0; + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1774; + goto done; + } - ret = wolfSSH_worker(ssh, &reportedId); - if (ret != WS_EXTDATA) { result = -1384; goto done; } - if (reportedId != ch->channel) { result = -1385; goto done; } - if (ch->windowSz != 1024 - 10) { result = -1386; goto done; } + /* The receive stays idle, so only the send can set ssh->error. */ + wolfSSH_SetIOSend(ctx, OobIoSend); - /* The reported channel is the one to drain. */ - ret = wolfSSH_ChannelIdReadExt(ssh, reportedId, out, (word32)sizeof(out)); - if (ret != 10) { result = -1387; goto done; } - for (i = 0; i < 10; i++) - if (out[i] != 0x22) { result = -1388; goto done; } - if (ch->windowSz != 1024) { result = -1389; goto done; } + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_FATAL_ERROR) { result = -1775; goto done; } + if (wolfSSH_get_error(ssh) != WS_SEND_OOB_READ_E) { + result = -1776; + goto done; + } done: s_recvPkt = NULL; @@ -6207,70 +7109,79 @@ static int test_WorkerReportsExtDataChannel(void) return result; } -/* Regression: extended data arriving while a rekey is in flight must still be - * reported. WS_EXTDATA is raised once, on arrival, so folding it into - * WS_REKEYING at the isKeying check in wolfSSH_worker() strands the buffered - * stderr and the window credit already charged for it -- nothing re-raises it. - * RFC 4253 section 7.1 requires accepting data the peer sent before it saw our - * KEXINIT, so this is a normal sequence, not a corner case. */ -static int test_WorkerReportsExtDataChannelKeying(void) +/* A session with no send callback set. wolfSSH_SendPacket() reports the + * socket error before it reaches the transport, and ssh->error carries it. */ +static int test_WorkerNoSendCallbackReported(void) { WOLFSSH_CTX* ctx = NULL; WOLFSSH* ssh = NULL; - WOLFSSH_CHANNEL* ch = NULL; int result = 0; int ret; - int i; - word32 reportedId = 0xFFFFFFFF; - byte pkt[32]; - byte out[32]; ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (ctx == NULL) - return -1390; - wolfSSH_SetIOSend(ctx, DiscardIoSend); + return -1780; wolfSSH_SetIORecv(ctx, PacketIoRecv); ssh = wolfSSH_new(ctx); - if (ssh == NULL) { result = -1391; goto done; } + if (ssh == NULL) { result = -1781; goto done; } ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + ctx->ioSendCb = NULL; - ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); - if (ch == NULL) { result = -1392; goto done; } - if (ChannelAppend(ssh, ch) != WS_SUCCESS) { - ChannelDelete(ch, ssh->ctx->heap); - result = -1393; + /* Queued output, so the flush runs. */ + ssh->outputBuffer.length = 1; + ssh->outputBuffer.idx = 0; + ssh->outputBuffer.buffer[0] = 0; + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_FATAL_ERROR) { result = -1782; goto done; } + if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) { + result = -1783; goto done; } - s_recvPkt = pkt; - s_recvPktSz = BuildExtDataStderrPacket(pkt, ch->channel, 0x33); +done: + s_recvPkt = NULL; + s_recvPktSz = 0; s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} - /* A rekey is underway when the stderr packet lands. */ - ssh->isKeying = WOLFSSH_SELF_IS_KEYING; - ret = wolfSSH_worker(ssh, &reportedId); - if (ret != WS_EXTDATA) { result = -1394; goto done; } - if (reportedId != ch->channel) { result = -1395; goto done; } - if (ch->windowSz != 1024 - 10) { result = -1396; goto done; } +/* An output buffer whose length runs past its size. wolfSSH_SendPacket() + * stops on the sanity check and ssh->error carries WS_BUFFER_E. */ +static int test_WorkerBadBufferStateReported(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + int result = 0; + int ret; - /* The drain works mid-rekey; the credit parks rather than going out, since - * no WINDOW_ADJUST may be sent during KEX. */ - ret = wolfSSH_ChannelIdReadExt(ssh, reportedId, out, (word32)sizeof(out)); - if (ret != 10) { result = -1397; goto done; } - for (i = 0; i < 10; i++) - if (out[i] != 0x33) { result = -1398; goto done; } - if (ch->windowSz != 1024) { result = -1399; goto done; } - if (ch->pendingWindowAdjust != 10) { result = -1400; goto done; } + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1785; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); - /* Once keying completes the parked credit flushes. */ - ssh->isKeying = 0; - ret = wolfSSH_TestSendPendingChannelWindowAdjust(ssh); - if (ret != WS_SUCCESS) { result = -1401; goto done; } - if (ch->pendingWindowAdjust != 0) { result = -1402; goto done; } + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1786; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ssh->outputBuffer.idx = 0; + ssh->outputBuffer.length = ssh->outputBuffer.bufferSz + 1; + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_FATAL_ERROR) { result = -1787; goto done; } + if (wolfSSH_get_error(ssh) != WS_BUFFER_E) { + result = -1788; + goto done; + } done: + if (ssh != NULL) + ssh->outputBuffer.length = 0; s_recvPkt = NULL; s_recvPktSz = 0; s_recvPktOff = 0; @@ -6393,11 +7304,8 @@ static int test_SendPendingWindowAdjustReportsWantWrite(void) } /* A drain whose window adjust hits a dead socket must surface the transport - * failure. wolfSSH_SendPacket() sets ssh->error only for WS_WANT_WRITE, so - * leaving it untouched on a hard failure preserves whatever was there before -- - * in the documented flow, the WS_EXTDATA that prompted the drain -- and - * wolfSSH_get_error() reports a healthy session on a broken connection. Drives - * the real receive path so ssh->error genuinely holds WS_EXTDATA first. */ + * failure, not the WS_EXTDATA that prompted the drain. Drives the real + * receive path so ssh->error genuinely holds WS_EXTDATA first. */ static int test_ChannelReadExtHardFailureReported(void) { WOLFSSH_CTX* ctx = NULL; @@ -6787,6 +7695,23 @@ static int RefuseThenCaptureIoSend(WOLFSSH* ssh, void* buf, word32 sz, } +/* Refuses the sends DoChannelClose() makes, then resets the socket under the + * worker's flush. */ +static int RefuseThenResetIoSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) +{ + WOLFSSH_UNUSED(ssh); + WOLFSSH_UNUSED(buf); + WOLFSSH_UNUSED(sz); + WOLFSSH_UNUSED(ctx); + + if (s_sendRefusals > 0) { + s_sendRefusals--; + return WS_CBIO_ERR_WANT_WRITE; + } + return WS_CBIO_ERR_CONN_RST; +} + + /* DoPacket() consumes the peer's CHANNEL_CLOSE whatever DoChannelClose() * returns, so the reply gets one chance to be built. A blocked socket must not * cost it: the EOF and the close both have to be bundled, and the channel @@ -6948,6 +7873,63 @@ static int test_DoChannelCloseFlushesReply(void) return result; } +/* The peer's close, with both of DoChannelClose()'s sends refused and the + * worker's flush then hitting a reset socket. wolfSSH_worker() reports the + * close in ret and ssh->error, not the send's WS_SOCKET_ERROR_E. */ +static int test_DoChannelCloseHardFlushKeepsClose(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + int result = 0; + int ret; + byte pkt[16]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1790; + /* Two refusals cover the EOF and the close it sends. */ + s_sendRefusals = 2; + wolfSSH_SetIOSend(ctx, RefuseThenResetIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1791; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); + if (ch == NULL) { result = -1792; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1793; + goto done; + } + ch->openConfirmed = 1; + ch->peerWindowSz = 1024; + ch->peerMaxPacketSz = 1024; + + s_recvPkt = pkt; + s_recvPktSz = BuildChannelClosePacket(pkt, ch->channel); + s_recvPktOff = 0; + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_CHANNEL_CLOSED) { result = -1794; goto done; } + if (wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) { + result = -1795; + goto done; + } + +done: + s_sendRefusals = 0; + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + + #ifdef WOLFSSH_SFTP static int s_recvCalls = 0; @@ -7842,12 +8824,6 @@ static int test_SendChannelEofSendFails(void) /* A connection reset fails the send without discarding the buffer, so the EOF * is still queued and eofTxd has to latch: the retry flushes those bytes and * must not build a second EOF behind them. The other arm of the same test. */ -static int ConnResetIoSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) -{ - (void)ssh; (void)buf; (void)sz; (void)ctx; - return WS_CBIO_ERR_CONN_RST; -} - static int test_SendChannelEofConnReset(void) { WOLFSSH_CTX* ctx = NULL; @@ -7912,8 +8888,8 @@ static int EofRecordingCb(WOLFSSH_CHANNEL* channel, void* ctx) } /* The channel EOF callback is the durable half of the contract: the WS_EOF - * from wolfSSH_worker() is raised once and a back-pressure status can take - * its place, but the callback fires from DoChannelEof() itself. */ + * from wolfSSH_worker() is raised once and a flush failure can replace it in + * wolfSSH_get_error(), but the callback fires from DoChannelEof() itself. */ static int test_ChannelEofCallback(void) { WOLFSSH_CTX* ctx = NULL; @@ -8415,8 +9391,8 @@ static int test_stream_read_deferredWindowAdjust(void) result = -6994; goto done; } - /* A peer that reset rather than blocked. wolfSSH_SendPacket() records only - * WS_WANT_WRITE, so the read path has to record a hard failure itself. */ + /* A peer that reset rather than blocked. The read path records the code + * itself: the adjust can fail before it reaches the transport. */ wolfSSH_SetIOSend(ctx, FailIoSend); if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) { @@ -8519,8 +9495,8 @@ static int test_ChannelIdRead_deferredWindowAdjust(void) result = -7020; goto done; } - /* A peer that reset rather than blocked. wolfSSH_SendPacket() records only - * WS_WANT_WRITE, so the read path has to record a hard failure itself. */ + /* A peer that reset rather than blocked. The read path records the code + * itself: the adjust can fail before it reaches the transport. */ wolfSSH_SetIOSend(ctx, FailIoSend); if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) { @@ -20067,6 +21043,83 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + unitResult = test_WorkerFlushesOnIdleReceive(); + printf("WorkerFlushesOnIdleReceive: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerReportsOwedFlush(); + printf("WorkerReportsOwedFlush: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerHardRecvErrorOutranksFlush(); + printf("WorkerHardRecvErrorOutranksFlush: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerHardSendErrorOnIdleReceive(); + printf("WorkerHardSendErrorOnIdleReceive: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerChanRxdSurfacesSendError(); + printf("WorkerChanRxdSurfacesSendError: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + +#ifndef NO_WOLFSSH_CLIENT + unitResult = test_ShutdownReportsWorkerOwedFlush(); + printf("ShutdownReportsWorkerOwedFlush: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + + unitResult = test_BundlePacketFailureDropsPartial(); + printf("BundlePacketFailureDropsPartial: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerExtDataSurfacesSendError(); + printf("WorkerExtDataSurfacesSendError: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerEofSurfacesSendError(); + printf("WorkerEofSurfacesSendError: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerKeyingSurfacesSendError(); + printf("WorkerKeyingSurfacesSendError: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerKeyingReportsRekey(); + printf("WorkerKeyingReportsRekey: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerDiscardedFlushKeepsError(); + printf("WorkerDiscardedFlushKeepsError: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerSendOobReadReported(); + printf("WorkerSendOobReadReported: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerNoSendCallbackReported(); + printf("WorkerNoSendCallbackReported: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_WorkerBadBufferStateReported(); + printf("WorkerBadBufferStateReported: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + #ifndef NO_WOLFSSH_CLIENT unitResult = test_TriggerKeyExchangeKeepsError(); printf("TriggerKeyExchangeKeepsError: %s\n", @@ -20114,6 +21167,11 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + unitResult = test_DoChannelCloseHardFlushKeepsClose(); + printf("DoChannelCloseHardFlushKeepsClose: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + #ifdef WOLFSSH_SFTP unitResult = test_SftpReadEofNoPoll(); printf("SftpReadEofNoPoll: %s\n", diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 631669b74..2b7a50ad4 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -77,15 +77,18 @@ WOLFSSH_API void wolfSSH_free(WOLFSSH* ssh); * wolfSSH_ChannelIdReadExt() * WS_EOF the peer half-closed a channel; it sends no more data, * but the channel is still open for sending. Raised once, - * on arrival, and a back-pressure status from the flush - * that follows can supersede it, so an application that - * must not miss one tests wolfSSH_ChannelGetEof() or takes - * the channel EOF callback. Reply, if the protocol wants - * one, with wolfSSH_ChannelSendEof(); the library does - * not. + * on arrival, so an application that must not miss one + * tests wolfSSH_ChannelGetEof() or takes the channel EOF + * callback. Reply, if the protocol wants one, with + * wolfSSH_ChannelSendEof(); the library does not. * WS_CHANNEL_CLOSED the peer closed a channel, which has been retired - * WS_WANT_READ / WS_WANT_WRITE / WS_REKEYING / WS_WINDOW_FULL + * WS_WANT_READ / WS_WANT_WRITE / WS_REKEYING * transient; call again + * A status above survives in the return even when the flush that follows + * fails, and wolfSSH_get_error() then holds the flush's code. Two + * exceptions: a WS_CHANNEL_CLOSED whose flush failed hard keeps the close, + * which callers route their teardown on, and WS_REKEYING is not reported + * when the flush failed, so the transport error reaches the caller instead. * Anything else is an error: WS_BAD_ARGUMENT, or WS_FATAL_ERROR with the * cause in wolfSSH_get_error() -- WS_DISCONNECT for the peer's disconnect, * which is how most sessions end. @@ -413,8 +416,8 @@ WOLFSSH_API int wolfSSH_ChannelExit(WOLFSSH_CHANNEL* channel); * The library never answers a received EOF with one of its own. It reports it * as WS_EOF and through the channel EOF callback, and the application decides * whether to reply, with this call or wolfSSH_stream_send_eof(). A - * back-pressure status can supersede the WS_EOF from wolfSSH_worker(); - * wolfSSH_ChannelGetEof() is the durable check. + * flush failure can replace the WS_EOF in wolfSSH_get_error(), though not in + * wolfSSH_worker()'s return; wolfSSH_ChannelGetEof() is the durable check. * wolfSSH_ChannelExit() and wolfSSH_shutdown() send an EOF themselves while * tearing the channel down. * @@ -726,7 +729,7 @@ WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz); /* Returns the bytes read; the next read clears the status. WS_WANT_WRITE * from wolfSSH_get_error() means the adjust is queued; it goes out on the - * next send or a wolfSSH_worker() whose receive succeeded. Others failed. */ + * next send or the next wolfSSH_worker(). Others failed. */ WOLFSSH_API int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz); /* Half-closes the first channel in the list. See wolfSSH_ChannelSendEof().