diff --git a/apps/wolfssh/wolfssh.c b/apps/wolfssh/wolfssh.c index 9cd153ab4..e66c0be37 100644 --- a/apps/wolfssh/wolfssh.c +++ b/apps/wolfssh/wolfssh.c @@ -332,14 +332,20 @@ static int FlushQueuedSend(WOLFSSH* ssh, wolfSSL_Mutex* lock) if (lock != NULL) { wc_UnLockMutex(lock); } - } while (ret == WS_WANT_WRITE && WTIME(NULL) < deadline); - - /* The queue is out. Whatever the worker made of the peer's end of the - * conversation is for the reader to sort out. A rekey started on the way - * through is the reader's as well, the send itself went out. */ - if (ret == WS_WANT_READ || ret == WS_CHAN_RXD || ret == WS_EXTDATA - || ret == WS_REKEYING || ret == WS_EOF) { - ret = WS_SUCCESS; + + /* None of these is a failure for the flush. */ + if (ret == WS_WANT_READ || ret == WS_CHAN_RXD || ret == WS_EXTDATA + || ret == WS_REKEYING || ret == WS_EOF + || ret == WS_WANT_WRITE) { + ret = WS_SUCCESS; + } + } while (ret == WS_SUCCESS + && wolfSSH_get_error(ssh) == WS_WANT_WRITE + && WTIME(NULL) < deadline); + + /* The deadline can run out with the packet still queued. */ + if (ret == WS_SUCCESS && wolfSSH_get_error(ssh) == WS_WANT_WRITE) { + ret = WS_WANT_WRITE; } return ret; diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index f057e9554..c1bd3a6d8 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -2056,6 +2056,8 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, /* read in case a window-change packet might be queued */ { int rc; + int selected = WS_SELECT_SEND_READY; + WS_SOCKET_T fd = wolfSSH_get_fd(ssh); word32 lastChannel = 0; do { @@ -2063,7 +2065,20 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (rc < 0) { rc = wolfSSH_get_error(ssh); } - } while (rc == WS_WANT_WRITE); + if (rc == WS_WANT_WRITE) { + selected = tcp_select_write(fd, 1); + } + } while (rc == WS_WANT_WRITE + && selected == WS_SELECT_SEND_READY); + + /* A dead socket ends the session */ + if (ret == WS_SUCCESS + && (selected == WS_SELECT_ERROR_READY + || selected == WS_SELECT_FAIL + || rc == WS_SOCKET_ERROR_E + || rc == WS_DISCONNECT)) { + ret = WS_FATAL_ERROR; + } } } } @@ -2263,6 +2278,10 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, cnt_r = wolfSSH_worker(ssh, &lastChannel); if (cnt_r < 0) { rc = wolfSSH_get_error(ssh); + if (cnt_r == WS_CHAN_RXD || cnt_r == WS_CHANNEL_CLOSED + || cnt_r == WS_EOF) { + rc = cnt_r; + } if (rc == WS_CHAN_RXD) { if (lastChannel == shellChannelId) { cnt_r = wolfSSH_ChannelIdRead(ssh, @@ -2296,7 +2315,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/client/client.c b/examples/client/client.c index 7abdffe65..0041bd361 100644 --- a/examples/client/client.c +++ b/examples/client/client.c @@ -1203,7 +1203,7 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args) * is still owed, so the drain below is exactly what is wanted. */ if (ret != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) { - if (ret != WS_SUCCESS) { + if (ret != WS_SUCCESS && ret != WS_WANT_WRITE) { ClientFreeBuffers(pubKeyName, privKeyName, NULL); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 435cec473..e45ee806e 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -1035,17 +1035,16 @@ static int ssh_worker(thread_ctx_t* threadCtx) channel. The additional channel is only used with the agent. */ cnt_r = wolfSSH_worker(ssh, &lastChannel); - /* Take the worker's status before the drain below: its - * reads and sends latch their own into ssh->error. */ rc = wolfSSH_get_error(ssh); + if (cnt_r == WS_CHAN_RXD || cnt_r == WS_REKEYING + || cnt_r == WS_CHANNEL_CLOSED || cnt_r == WS_EOF) { + rc = cnt_r; + } /* 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; @@ -1243,7 +1242,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); @@ -1533,8 +1532,12 @@ static int sftp_worker(thread_ctx_t* threadCtx) ret = error = wolfSSH_get_error(ssh); /* there is an edge case where the last SFTP handshake message sent got a - * WANT_WRITE case, keep trying to send it here. */ + * WANT_WRITE case, keep trying to send it here. Waits for the socket to + * take bytes again rather than retrying into a full one. */ while (error == WS_WANT_WRITE) { + selected = tcp_select_write(s, TEST_SFTP_TIMEOUT); + if (selected != WS_SELECT_SEND_READY) + break; ret = wolfSSH_worker(ssh, NULL); error = wolfSSH_get_error(ssh); } diff --git a/examples/portfwd/portfwd.c b/examples/portfwd/portfwd.c index c96410d73..953bcf074 100644 --- a/examples/portfwd/portfwd.c +++ b/examples/portfwd/portfwd.c @@ -832,10 +832,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; @@ -935,7 +934,9 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) } ret = wolfSSH_shutdown(ssh); - if (ret != WS_SUCCESS) + /* The socket closes next, so a queued write and a retired channel are + * both done as far as this teardown is concerned. */ + if (ret != WS_SUCCESS && ret != WS_WANT_WRITE && ret != WS_CHANNEL_CLOSED) err_sys("Closing port forward stream failed."); WCLOSESOCKET(sshFd); diff --git a/examples/scpclient/scpclient.c b/examples/scpclient/scpclient.c index 7fba41e92..0872f30de 100644 --- a/examples/scpclient/scpclient.c +++ b/examples/scpclient/scpclient.c @@ -323,7 +323,7 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) if (ret != WS_CHANNEL_CLOSED && ret != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) { - if (ret != WS_SUCCESS) { + if (ret != WS_SUCCESS && ret != WS_WANT_WRITE) { WLOG(WS_LOG_DEBUG, "Sending the shutdown messages failed."); } else { @@ -351,7 +351,7 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) #endif if ((ret != WS_SUCCESS) && (ret != WS_CHANNEL_CLOSED) - && (ret != WS_EOF)) + && (ret != WS_EOF) && (ret != WS_WANT_WRITE)) ((func_args*)args)->return_code = 1; return 0; } 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..0e8db096c 100644 --- a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c +++ b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c @@ -995,17 +995,16 @@ static int ssh_worker(thread_ctx_t* threadCtx) channel. The additional channel is only used with the agent. */ cnt_r = wolfSSH_worker(ssh, &lastChannel); - /* Take the worker's status before the drain below: its - * reads and sends latch their own into ssh->error. */ rc = wolfSSH_get_error(ssh); + if (cnt_r == WS_CHAN_RXD || cnt_r == WS_REKEYING + || cnt_r == WS_CHANNEL_CLOSED || cnt_r == WS_EOF) { + rc = cnt_r; + } /* 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; @@ -1170,7 +1169,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/src/internal.c b/src/internal.c index b2153e323..ed909ef2d 100644 --- a/src/internal.c +++ b/src/internal.c @@ -5474,6 +5474,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; } @@ -5484,6 +5485,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; } @@ -5522,11 +5524,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; } @@ -5551,7 +5555,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; @@ -14805,6 +14811,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 83f41d762..7fc7b5e46 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1259,6 +1259,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) { @@ -1295,8 +1298,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; @@ -4540,6 +4546,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()"); @@ -4556,59 +4563,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. */ @@ -4618,12 +4596,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; } @@ -5061,8 +5037,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, @@ -5122,8 +5099,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/src/wolfscp.c b/src/wolfscp.c index ad7dc8373..9f28d43ff 100644 --- a/src/wolfscp.c +++ b/src/wolfscp.c @@ -1788,6 +1788,10 @@ int ReceiveScpMessage(WOLFSSH* ssh) int rc; rc = wolfSSH_get_error(ssh); + if (err == WS_CHAN_RXD || err == WS_EXTDATA + || err == WS_CHANNEL_CLOSED) { + rc = err; + } switch (rc) { case WS_CHAN_RXD: sz = wolfSSH_ChannelIdRead(ssh, lastChannel, diff --git a/tests/regress.c b/tests/regress.c index c8c3016de..b0e091683 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -8732,11 +8732,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 @@ -9405,11 +9413,9 @@ static void TestPasswordEofNoCrash(void) ClientFreeBuffers(); } -/* When the send path is back-pressured (WANT_WRITE), wolfSSH_worker() - * 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 +/* An idle receive still gets its flush. The receive runs first and reports + * want-read, and the queued byte is pushed anyway, so ssh->error carries the + * write the socket would not take. */ static int recvCallCount; static int WantWriteSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) @@ -9425,7 +9431,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; @@ -9448,20 +9453,16 @@ static void TestWorkerReadsWhenSendWouldBlock(void) recvCallCount = 0; - /* call worker; expect it to attempt send, notice back-pressure, and have - * invoked recv once. Depending on how DoReceive handles WANT_READ, the - * return may be WANT_WRITE or a fatal error; the important part is that - * recv was exercised. */ ret = wolfSSH_worker(ssh, NULL); - AssertTrue(ret == WS_WANT_WRITE || ret == WS_FATAL_ERROR); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_WRITE); AssertIntEQ(recvCallCount, 1); + AssertTrue(wolfSSH_OutputPending(ssh)); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } -#endif /* !WOLFSSH_TEST_BLOCK */ -#endif #ifdef WOLFSSH_SFTP @@ -13774,9 +13775,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 f67c84084..1f7e9e7df 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -4423,6 +4423,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; @@ -6314,13 +6330,904 @@ static int test_WorkerReportsExtDataChannelKeying(void) 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; } + /* 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; +} + + + +/* 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, 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; } + + /* An unknown cipher fails the next bundle. Check the framing error and + * the staged packet, so an earlier bail cannot stand in for them. */ + ssh->encryptId = ID_UNKNOWN; + + ret = SendChannelWindowAdjust(ssh, ch->peerChannel, 10, &bundled); + if (ret != WS_INVALID_ALGO_ID) { result = -1856; goto done; } + if (ssh->packetStartIdx != queued) { result = -1858; 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; +} + + +/* Channel data arrives and the flush only short-writes. ret keeps the data + * report, ssh->error carries the want-write, and the bytes stay owed. */ +static int test_WorkerChanRxdSurfacesShortFlush(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 -1870; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1871; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1872; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1873; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1874; + goto done; + } + + /* Channel data arrives while the socket still refuses writes. */ + s_recvPkt = pkt; + s_recvPktSz = BuildChannelDataPacket(pkt, ch->channel, 0xBB); + s_recvPktOff = 0; + wolfSSH_SetIOSend(ctx, WantWriteIoSend); + + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_CHAN_RXD) { result = -1875; goto done; } + if (wolfSSH_get_error(ssh) != WS_WANT_WRITE) { result = -1876; goto done; } + if (!wolfSSH_OutputPending(ssh)) { result = -1877; 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; + word32 reportedId = 0; + 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, &reportedId); + if (ret != WS_REKEYING) { result = -1815; goto done; } + if (wolfSSH_get_error(ssh) != WS_REKEYING) { + result = -1816; + goto done; + } + if (reportedId != ch->channel) { result = -1818; 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; + } + + /* Channel data arrives, and the send throws the queued adjust away. */ + s_recvPkt = pkt; + s_recvPktSz = BuildChannelDataPacket(pkt, ch->channel, 0x66); + s_recvPktOff = 0; + wolfSSH_SetIOSend(ctx, FailIoSend); + + 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; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* 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; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1770; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1771; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + 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 = -1773; + goto done; + } + + if (WorkerParkAdjust(ctx, ssh, ch) != WS_SUCCESS) { + result = -1774; + goto done; + } + + /* The receive stays idle, so only the send can set ssh->error. */ + wolfSSH_SetIOSend(ctx, OobIoSend); + + 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; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +/* 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; + int result = 0; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1780; + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1781; goto done; } + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + ctx->ioSendCb = NULL; + + /* 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; + } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + + +/* 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; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1785; + wolfSSH_SetIOSend(ctx, CountIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + 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; @@ -6443,11 +7350,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; @@ -6837,6 +7741,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 @@ -6998,6 +7919,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; @@ -7218,6 +8196,7 @@ static int test_StreamReadHeadOpenFailed(void) #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 * DoReceive() passes it through, but the accept loop must not read a negative @@ -7411,6 +8390,163 @@ static int test_WorkerReportsEofChannelKeying(void) #endif /* NO_WOLFSSH_SERVER */ + +#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 */ + + +#ifndef NO_WOLFSSH_CLIENT +/* The mark fires from inside wolfSSH_SendPacket() on a flush that went out. + * The status the pass was already carrying has to survive that. */ +static int test_HighwaterRekeyKeepsError(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 -1890; + wolfSSH_SetIOSend(ctx, DiscardIoSend); + wolfSSH_SetIORecv(ctx, PacketIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1891; goto done; } + + /* Queued output, so the flush has something to carry out. */ + ssh->outputBuffer.length = 1; + ssh->outputBuffer.idx = 0; + ssh->outputBuffer.buffer[0] = 0; + + /* The mark is due on this flush. */ + if (wolfSSH_SetHighwater(ssh, 1) != WS_SUCCESS) { + result = -1892; + goto done; + } + ssh->txCount = 1; + + ssh->error = WS_CHANNEL_CLOSED; + + ret = wolfSSH_SendPacket(ssh); + if (ret != WS_SUCCESS) { result = -1893; goto done; } + if (!ssh->highwaterFlag) { result = -1894; goto done; } + if (!ssh->isKeying) { result = -1895; goto done; } + if (wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) { + result = -1896; + goto done; + } + +done: + s_recvPkt = NULL; + s_recvPktSz = 0; + s_recvPktOff = 0; + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + + +/* 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 */ + + + #ifndef NO_WOLFSSH_CLIENT /* wolfSSH_stream_send_eof() and wolfSSH_ChannelSendEof() close the * application's own sending direction. Sends afterwards must fail, reads must @@ -7852,12 +8988,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; @@ -7922,8 +9052,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; @@ -8425,8 +9555,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) { @@ -8529,8 +9659,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) { @@ -20790,6 +21920,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; + + + 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_WorkerChanRxdSurfacesShortFlush(); + printf("WorkerChanRxdSurfacesShortFlush: %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; + + unitResult = test_StreamReadExtDataHeadChannel(); printf("StreamReadExtDataHeadChannel: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); @@ -20830,6 +22037,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", @@ -20873,6 +22085,26 @@ int wolfSSH_UnitTest(int argc, char** argv) #endif /* NO_WOLFSSH_SERVER */ +#ifndef NO_WOLFSSH_CLIENT + unitResult = test_ShutdownReportsWorkerOwedFlush(); + printf("ShutdownReportsWorkerOwedFlush: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + +#ifndef NO_WOLFSSH_CLIENT + unitResult = test_HighwaterRekeyKeepsError(); + printf("HighwaterRekeyKeepsError: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + + unitResult = test_TriggerKeyExchangeKeepsError(); + printf("TriggerKeyExchangeKeepsError: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + + #ifndef NO_WOLFSSH_CLIENT unitResult = test_SendEofApi(); printf("SendEofApi: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index f768fe9e7..04253a85d 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -82,22 +82,38 @@ 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 - * Anything else is an error: WS_BAD_ARGUMENT, or WS_FATAL_ERROR with the + * Take the event from the return, not from wolfSSH_get_error(). The return + * names what arrived; wolfSSH_get_error() names what the transport did, and a + * flush on the same pass overwrites it. A loop that switches on + * wolfSSH_get_error() to find WS_CHAN_RXD or WS_EXTDATA misses them whenever + * that flush does not complete. + * Back-pressure reaches the caller either as WS_WANT_WRITE in the return or + * as WS_FATAL_ERROR with the want in wolfSSH_get_error(), so resolve a + * WS_FATAL_ERROR through wolfSSH_get_error() rather than testing the return + * for a want. Either way it is transient: call again. A caller that + * tolerates only WS_WANT_READ drops live sessions, since a queued write now + * reports WS_WANT_WRITE where it once reported WS_WANT_READ. + * When the flush fails on the same pass: + * the return keeps the event -- except that a clean receive reports + * the flush's code, and WS_REKEYING is withheld + * get_error() holds the flush's code -- except after a hard-failed + * WS_CHANNEL_CLOSED, or a receive that failed for any + * reason but want-read, where it keeps the receive's + * Any other code 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. * - * For WS_CHAN_RXD, WS_EXTDATA, WS_EOF and WS_SUCCESS, channelId (when not - * NULL) names the channel the event belongs to. It is left alone for every - * other status, WS_CHANNEL_CLOSED included; use wolfSSH_GetLastRxId() there. + * For WS_CHAN_RXD, WS_EXTDATA, WS_EOF, WS_SUCCESS and a WS_REKEYING that + * displaced one of those, channelId (when not NULL) names the channel the + * event belongs to. It is left alone for every other status, + * WS_CHANNEL_CLOSED included; use wolfSSH_GetLastRxId() there. * * Note that after a peer half-close wolfSSH_stream_send() keeps working: the * library latches only the EOF it sends, not the one it receives. */ @@ -399,10 +415,9 @@ WOLFSSH_API int wolfSSH_ChannelSendExt(WOLFSSH_CHANNEL* channel, * A WS_WANT_WRITE means the teardown is incomplete: the close is only built * once the EOF is away, so call again until it reports something else. The * retry costs nothing, a bundled EOF is not sent twice. WS_SUCCESS means both - * messages are bundled, not that they reached the peer: keep driving - * wolfSSH_worker() until it stops reporting WS_WANT_WRITE before dropping the - * socket. A channel whose open the peer has not confirmed has no peer id to - * address and reports WS_CHANNEL_NOT_CONF. + * messages are bundled, not that they reached the peer. A channel whose open + * the peer has not confirmed has no peer id to address and reports + * WS_CHANNEL_NOT_CONF. * * A peer that never answers leaves the channel on the list for the life of * the session; there is no reclaim short of wolfSSH_free(). */ @@ -417,9 +432,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. + * whether to reply, with this call or wolfSSH_stream_send_eof(). + * wolfSSH_ChannelGetEof() is the durable check for a received EOF. * wolfSSH_ChannelExit() and wolfSSH_shutdown() send an EOF themselves while * tearing the channel down. * @@ -778,7 +792,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(). @@ -817,6 +831,8 @@ WOLFSSH_API int wolfSSH_extended_data_send(WOLFSSH* ssh, byte* buf, word32 bufSz * (the peer's CHANNEL_CLOSE) is discarded with it. */ WOLFSSH_API int wolfSSH_extended_data_read(WOLFSSH* ssh, byte* out, word32 outSz); +/* Starts a key exchange. A clean start leaves ssh->error alone; only a + * failure records its code there. */ WOLFSSH_API int wolfSSH_TriggerKeyExchange(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_SendIgnore(WOLFSSH* ssh, const byte* buf, word32 bufSz); /* One disconnect ends the session, so a second call reports WS_DISCONNECT. diff --git a/wolfssh/test.h b/wolfssh/test.h index 7da657318..8ed3d24de 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -714,7 +714,8 @@ enum { WS_SELECT_FAIL, WS_SELECT_TIMEOUT, WS_SELECT_RECV_READY, - WS_SELECT_ERROR_READY + WS_SELECT_ERROR_READY, + WS_SELECT_SEND_READY }; #if (defined(WOLFSSH_TEST_SERVER) || defined(WOLFSSH_TEST_CLIENT)) && !defined(FREESCALE_MQX) @@ -795,6 +796,35 @@ static INLINE int tcp_select(SOCKET_T socketfd, int to_sec) return WS_SELECT_FAIL; } + +/* tcp_select() waits on the read side. This is the write side, for a caller + * holding output the socket would not take. */ +static INLINE int tcp_select_write(SOCKET_T socketfd, int to_sec) +{ + WFD_SET_TYPE sendfds, errfds; + int nfds = (int)socketfd + 1; + struct timeval timeout = {(to_sec > 0) ? to_sec : 0, 100}; + int result; + + WFD_ZERO(&sendfds); + WFD_SET(socketfd, &sendfds); + WFD_ZERO(&errfds); + WFD_SET(socketfd, &errfds); + + result = wSelect(nfds, NULL, &sendfds, &errfds, &timeout); + + if (result == 0) + return WS_SELECT_TIMEOUT; + else if (result > 0) { + if (WFD_ISSET(socketfd, &sendfds)) + return WS_SELECT_SEND_READY; + else if (WFD_ISSET(socketfd, &errfds)) + return WS_SELECT_ERROR_READY; + } + + return WS_SELECT_FAIL; +} + #endif /* WOLFSSH_TEST_SERVER || WOLFSSH_TEST_CLIENT */