From d4bf5e0274621dc98cd37e7fc7260ee9149c0db1 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 21 Aug 2026 15:06:46 -0700 Subject: [PATCH 1/6] Fix the shutdown channel teardown wolfSSH_shutdown() searched for the session channel by the peer's channel ID while telling ChannelFind() to match the local ID field. Each side numbers its channels independently, so the search usually found nothing. - The session channel is the head of the list; take it directly instead of searching for what is already in hand. - Restores the EOF, exit-status and close sends, and the drain that waits on the peer's close, all skipped on the NULL result. - Only bit when the two IDs differ, so the single-channel tests, where both sides pick 0, never saw it. - unit.c: shut down a channel whose peer ID is not its local ID, then check that EOF and close went out. Issue: F-8817 --- src/ssh.c | 4 +-- tests/unit.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/ssh.c b/src/ssh.c index 1f3f7cc49..77d753694 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1113,9 +1113,9 @@ int wolfSSH_shutdown(WOLFSSH* ssh) if (ssh == NULL || ssh->channelList == NULL) ret = WS_BAD_ARGUMENT; - /* look up the channel if it still exists */ + /* The session channel is the head of the list. */ if (ret == WS_SUCCESS) { - channel = ChannelFind(ssh, ssh->channelList->peerChannel, WS_CHANNEL_ID_SELF); + channel = ssh->channelList; } /* if channel close was not already sent then send it */ diff --git a/tests/unit.c b/tests/unit.c index e1eff261c..feadbc754 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -16100,6 +16100,75 @@ static int test_ResolveOffset(void) #endif /* WOLFSSH_TEST_RESOLVE_OFFSET */ +#if defined(WOLFSSH_TEST_INTERNAL) && !defined(NO_WOLFSSH_SERVER) + +/* IORecv mock reporting nothing to read yet, so the shutdown drain below + * completes without a live socket. */ +static int ShutdownIoRecv(WOLFSSH* ssh, void* data, word32 sz, void* ctx) +{ + WOLFSSH_UNUSED(ssh); + WOLFSSH_UNUSED(data); + WOLFSSH_UNUSED(sz); + WOLFSSH_UNUSED(ctx); + return WS_CBIO_ERR_WANT_READ; +} + +/* wolfSSH_shutdown() has to reach the session channel when the peer numbered + * it differently than this side did, which is the normal case: each side + * picks its own channel IDs. Looking the channel up by the peer's ID while + * matching against the local ID field found nothing, and the whole teardown + * was skipped. Only the EOF and close sends leave a flag behind to check; + * the exit-status request in between does not. */ +static int test_ShutdownPeerChannelId(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WOLFSSH_CHANNEL* ch = NULL; + word32 peerChannel; + int result = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1080; + wolfSSH_SetIOSend(ctx, DiscardIoSend); + wolfSSH_SetIORecv(ctx, ShutdownIoRecv); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { result = -1081; goto done; } + + /* Let the channel messages past the message filter. */ + ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64); + if (ch == NULL) { result = -1082; goto done; } + if (ChannelAppend(ssh, ch) != WS_SUCCESS) { + ChannelDelete(ch, ssh->ctx->heap); + result = -1083; + goto done; + } + + peerChannel = ch->channel + 7; + ch->peerChannel = peerChannel; + ch->openConfirmed = 1; + + /* The drain at the end of shutdown only sees a want-read, so the return + * is not the interesting part here; what got sent is. */ + (void)wolfSSH_shutdown(ssh); + + ch = ChannelFind(ssh, peerChannel, WS_CHANNEL_ID_PEER); + if (ch == NULL) { result = -1084; goto done; } + if (!ch->eofTxd) { result = -1085; goto done; } + if (!ch->closeTxd) { result = -1086; goto done; } + +done: + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + +#endif /* WOLFSSH_TEST_INTERNAL && !NO_WOLFSSH_SERVER */ + + int wolfSSH_UnitTest(int argc, char** argv) { int testResult = 0, unitResult = 0; @@ -16770,6 +16839,13 @@ int wolfSSH_UnitTest(int argc, char** argv) testResult = testResult || unitResult; #endif +#if defined(WOLFSSH_TEST_INTERNAL) && !defined(NO_WOLFSSH_SERVER) + unitResult = test_ShutdownPeerChannelId(); + printf("ShutdownPeerChannelId: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + wolfSSH_Cleanup(); return (testResult ? 1 : 0); From 76a72914438161c816ee9e681ca4dd12f2c97bf3 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 21 Aug 2026 15:06:46 -0700 Subject: [PATCH 2/6] Make a disconnect end the session SSH_MSG_DISCONNECT left nothing behind but ssh->error, which wolfSSH_stream_read() clears on entry. An application looping on the stream calls lost the code and went back to a connection already over. - Add WOLFSSH.disconnected, set by DoDisconnect() and SendDisconnect(). - DoDisconnect() sets it before decoding the payload, so a malformed message still ends the session. RFC 4253 section 11.1. - wolfSSH_stream_read() and wolfSSH_stream_send() report WS_DISCONNECT from the flag instead of reaching for the transport again. - Both guards run ahead of the channelList NULL test, so a torn-down session reports the disconnect rather than WS_BAD_ARGUMENT. - ssh.h states that undrained channel data goes with the session; internal.h states which calls the flag gates and which it does not. - regress.c: the receive side, the send side, and both of those again on a session with an open channel. Issue: F-8837 --- src/internal.c | 9 ++++ src/ssh.c | 20 ++++++- tests/regress.c | 127 +++++++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 4 ++ wolfssh/ssh.h | 3 ++ 5 files changed, 161 insertions(+), 2 deletions(-) diff --git a/src/internal.c b/src/internal.c index 316397e69..50029eb2c 100644 --- a/src/internal.c +++ b/src/internal.c @@ -8166,6 +8166,10 @@ static int DoDisconnect(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) WOLFSSH_UNUSED(reasonStr); + /* RFC 4253 section 11.1, the peer is gone whether or not the rest of + * the message decodes. */ + ssh->disconnected = 1; + ret = GetUint32(&reason, buf, len, &begin); if (ret == WS_SUCCESS) { /* Skip the description text. */ @@ -16731,6 +16735,11 @@ int SendDisconnect(WOLFSSH* ssh, word32 reason) if (ssh == NULL) ret = WS_BAD_ARGUMENT; + /* Mark the session over before the send. A partial or failed send + * still ends it. */ + if (ret == WS_SUCCESS) + ssh->disconnected = 1; + if (ret == WS_SUCCESS) ret = PreparePacket(ssh, MSG_ID_SZ + UINT32_SZ + (LENGTH_SZ * 2)); diff --git a/src/ssh.c b/src/ssh.c index 77d753694..24156d19a 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1228,7 +1228,15 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz) WLOG(WS_LOG_DEBUG, "Entering wolfSSH_stream_read()"); - if (ssh == NULL || buf == NULL || bufSz == 0 || ssh->channelList == NULL) + if (ssh == NULL || buf == NULL || bufSz == 0) + return WS_BAD_ARGUMENT; + + if (ssh->disconnected) { + ssh->error = WS_DISCONNECT; + return WS_FATAL_ERROR; + } + + if (ssh->channelList == NULL) return WS_BAD_ARGUMENT; if (ssh->channelList->eofRxd) { @@ -1307,7 +1315,15 @@ int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz) WLOG(WS_LOG_DEBUG, "Entering wolfSSH_stream_send()"); - if (ssh == NULL || buf == NULL || ssh->channelList == NULL) + if (ssh == NULL || buf == NULL) + return WS_BAD_ARGUMENT; + + if (ssh->disconnected) { + ssh->error = WS_DISCONNECT; + return WS_FATAL_ERROR; + } + + if (ssh->channelList == NULL) return WS_BAD_ARGUMENT; if (ssh->isKeying) { diff --git a/tests/regress.c b/tests/regress.c index 826ed1d12..61599b9df 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -88,6 +88,7 @@ static void ResetSession(WOLFSSH* ssh) ssh->connectState = CONNECT_BEGIN; ssh->acceptState = ACCEPT_BEGIN; ssh->error = 0; + ssh->disconnected = 0; } @@ -2571,6 +2572,7 @@ static void TestDisconnectSetsDisconnectError(void) MemIo io; byte in[128]; byte out[32]; + byte data[8]; word32 inSz; int ret; @@ -2594,6 +2596,129 @@ static void TestDisconnectSetsDisconnectError(void) AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); AssertIntEQ(io.inOff, io.inSz); + /* The disconnect is terminal, not just this call's error. Later stream + * calls must report it rather than clearing the error and reading or + * writing more. */ + AssertTrue(ssh->disconnected); + + WMEMSET(data, 0, sizeof(data)); + ret = wolfSSH_stream_read(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + ret = wolfSSH_stream_send(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* Append a bare session channel so the stream calls have a channel to work + * on, the state a disconnect actually arrives in. */ +static void AddSessionChannel(WOLFSSH* ssh) +{ + WOLFSSH_CHANNEL* ch; + + ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024); + AssertNotNull(ch); + AssertIntEQ(ChannelAppend(ssh, ch), WS_SUCCESS); + ch->openConfirmed = 1; +} + + +/* The same received disconnect on an established session. Without a channel + * the stream calls bail out on the NULL channel list before they reach + * anything, so this is the case that shows the gate doing work. */ +static void TestDisconnectTerminalWithChannel(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte in[128]; + byte out[128]; + byte data[8]; + word32 inSz; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSend); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AddSessionChannel(ssh); + + inSz = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION, + in, sizeof(in)); + MemIoInit(&io, in, inSz, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + ret = DoReceive(ssh); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + AssertTrue(ssh->disconnected); + + WMEMSET(data, 0, sizeof(data)); + ret = wolfSSH_stream_read(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + /* Nothing may go out on the channel either. */ + ret = wolfSSH_stream_send(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + AssertIntEQ(io.outSz, 0); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* Sending SSH_MSG_DISCONNECT ends the session the same way receiving one + * does: RFC 4253 section 11.1 says the connection is over once the message + * goes out, so the stream calls must refuse afterwards. */ +static void TestSendDisconnectIsTerminal(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[128]; + byte data[8]; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSend); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AddSessionChannel(ssh); + + MemIoInit(&io, NULL, 0, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + ret = wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION); + AssertIntEQ(ret, WS_SUCCESS); + AssertTrue(ssh->disconnected); + AssertTrue(io.outSz > 0); + + WMEMSET(data, 0, sizeof(data)); + ret = wolfSSH_stream_send(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + ret = wolfSSH_stream_read(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } @@ -6328,6 +6453,8 @@ int main(int argc, char** argv) TestDoNewKeys(); #endif TestDisconnectSetsDisconnectError(); + TestDisconnectTerminalWithChannel(); + TestSendDisconnectIsTerminal(); #if !(defined(WOLFSSH_NO_RSA) && defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) TestClientBuffersIdempotent(); #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index c40d78edc..ee728489b 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1073,6 +1073,10 @@ struct WOLFSSH { #endif byte connReset; byte isClosed; + /* Set when a DISCONNECT is sent or received. Only wolfSSH_stream_read() + * and wolfSSH_stream_send() are gated on it; the channel-id calls and + * wolfSSH_worker() are not, since the shutdown paths still pump them. */ + byte disconnected; byte clientOpenSSH; byte kexId; diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index dfb45ab60..842de3049 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -558,6 +558,9 @@ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_accept(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_connect(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh); +/* A disconnect, sent or received, ends the session: wolfSSH_stream_read() + * and wolfSSH_stream_send() report WS_DISCONNECT from then on, and channel + * data that arrived before it but was never drained is dropped. */ WOLFSSH_API int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz); From 12b8f5338a6fe1b1e10306eeb10a96c6becd35b9 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 21 Aug 2026 15:14:53 -0700 Subject: [PATCH 3/6] Block every send after a disconnect The disconnect flag gated wolfSSH_stream_read() and wolfSSH_stream_send(), which is the client-side API. wolfsshd and echoserver drive their channels through the channel-id calls, so the daemon was never gated at all. - New SendAfterDisconnect() helper, used by the six send entry points: stream_send, stream_exit, ChannelIdSend, ChannelIdSendExt, extended_data_send and global_request. - Reads stay open, since data that arrived before the disconnect is still the caller's. wolfSSH_stream_read() drains its buffer and reports WS_DISCONNECT only once it runs dry. - wolfSSH_worker() stays ungated; the shutdown paths still pump it. - ssh.h and internal.h describe the split. - regress.c: buffered data survives the disconnect, and every send call refuses without a byte leaving the session. Issue: F-8837 --- src/ssh.c | 58 +++++++++++++++++---- tests/regress.c | 124 +++++++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 7 +-- wolfssh/ssh.h | 8 +-- 4 files changed, 181 insertions(+), 16 deletions(-) diff --git a/src/ssh.c b/src/ssh.c index 24156d19a..0b1465e81 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1103,6 +1103,21 @@ int wolfSSH_connect(WOLFSSH* ssh) #endif /* NO_WOLFSSH_CLIENT */ +/* A disconnect, sent or received, ends the session, so nothing further may + * go out. RFC 4253 section 11.1. Reads are deliberately not gated on this: + * channel data that arrived before the disconnect is still the caller's. + * Call only after ssh has been checked for NULL. */ +static int SendAfterDisconnect(WOLFSSH* ssh) +{ + if (ssh->disconnected) { + WLOG(WS_LOG_DEBUG, "Send attempted after a disconnect"); + ssh->error = WS_DISCONNECT; + return 1; + } + return 0; +} + + int wolfSSH_shutdown(WOLFSSH* ssh) { int ret = WS_SUCCESS; @@ -1231,13 +1246,14 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz) if (ssh == NULL || buf == NULL || bufSz == 0) return WS_BAD_ARGUMENT; - if (ssh->disconnected) { - ssh->error = WS_DISCONNECT; - return WS_FATAL_ERROR; - } - - if (ssh->channelList == NULL) + if (ssh->channelList == NULL) { + /* No channel left to drain, so the disconnect is all there is. */ + if (ssh->disconnected) { + ssh->error = WS_DISCONNECT; + return WS_FATAL_ERROR; + } return WS_BAD_ARGUMENT; + } if (ssh->channelList->eofRxd) { ssh->error = WS_EOF; @@ -1252,6 +1268,13 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz) inputBuffer = &ssh->channelList->inputBuffer; ssh->error = WS_SUCCESS; + /* Hand back whatever arrived before the disconnect, then report it once + * the buffer runs dry rather than going back to a dead transport. */ + if (ssh->disconnected && inputBuffer->length - inputBuffer->idx == 0) { + ssh->error = WS_DISCONNECT; + return WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { WLOG(WS_LOG_DEBUG, " Stream read index of %u", inputBuffer->idx); WLOG(WS_LOG_DEBUG, " Stream read ava data %u", inputBuffer->length); @@ -1318,10 +1341,8 @@ int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz) if (ssh == NULL || buf == NULL) return WS_BAD_ARGUMENT; - if (ssh->disconnected) { - ssh->error = WS_DISCONNECT; + if (SendAfterDisconnect(ssh)) return WS_FATAL_ERROR; - } if (ssh->channelList == NULL) return WS_BAD_ARGUMENT; @@ -1350,6 +1371,9 @@ int wolfSSH_ChannelIdSend(WOLFSSH* ssh, word32 channelId, if (ssh == NULL || buf == NULL) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + if (ret == WS_SUCCESS) { channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF); if (channel == NULL) { @@ -1386,6 +1410,9 @@ int wolfSSH_ChannelIdSendExt(WOLFSSH* ssh, word32 channelId, if (ssh == NULL || buf == NULL) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + if (ret == WS_SUCCESS) { channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF); if (channel == NULL) { @@ -1419,6 +1446,9 @@ int wolfSSH_stream_exit(WOLFSSH* ssh, int status) if (ssh == NULL || ssh->channelList == NULL) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + if (ret == WS_SUCCESS) ret = SendChannelExit(ssh, ssh->channelList->peerChannel, status); @@ -1442,6 +1472,8 @@ int wolfSSH_global_request(WOLFSSH *ssh, const unsigned char* data, word32 dataS return WS_BAD_ARGUMENT; if (reply != 0 && reply != 1) return WS_BAD_ARGUMENT; + if (SendAfterDisconnect(ssh)) + return WS_FATAL_ERROR; return SendGlobalRequest(ssh, data, dataSz, reply); } @@ -1452,7 +1484,13 @@ int wolfSSH_extended_data_send(WOLFSSH* ssh, byte* buf, word32 bufSz) WLOG(WS_LOG_DEBUG, "Entering wolfSSH_extended_data_send()"); - if (ssh == NULL || buf == NULL || ssh->channelList == NULL) + if (ssh == NULL || buf == NULL) + return WS_BAD_ARGUMENT; + + if (SendAfterDisconnect(ssh)) + return WS_FATAL_ERROR; + + if (ssh->channelList == NULL) return WS_BAD_ARGUMENT; if (ssh->isKeying) { diff --git a/tests/regress.c b/tests/regress.c index 61599b9df..b7ae01c96 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -2679,6 +2679,128 @@ static void TestDisconnectTerminalWithChannel(void) } +/* The disconnect stops sends, not reads. Channel data that arrived before + * it is still the caller's, and only once that runs dry does the read + * report the disconnect. */ +static void TestDisconnectDrainsBufferedData(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte in[128]; + byte out[128]; + byte data[16]; + byte payload[] = { 'h', 'e', 'l', 'l', 'o' }; + word32 inSz; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSend); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AddSessionChannel(ssh); + + AssertIntEQ(ChannelPutData(ssh->channelList, payload, sizeof(payload)), + WS_SUCCESS); + + inSz = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION, + in, sizeof(in)); + MemIoInit(&io, in, inSz, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + ret = DoReceive(ssh); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + AssertTrue(ssh->disconnected); + + WMEMSET(data, 0, sizeof(data)); + ret = wolfSSH_stream_read(ssh, data, sizeof(data)); + AssertIntEQ(ret, (int)sizeof(payload)); + AssertIntEQ(WMEMCMP(data, payload, sizeof(payload)), 0); + + /* Buffer is dry now, so the disconnect is what is left to report. */ + ret = wolfSSH_stream_read(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* Every send entry point refuses after a disconnect, not just the stream + * calls. wolfsshd and echoserver drive their channels through the + * channel-id and extended-data calls and never touch wolfSSH_stream_send(). */ +static void TestDisconnectBlocksEverySend(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[256]; + byte data[8]; + word32 quietSz; + word32 channelId; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSend); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AddSessionChannel(ssh); + channelId = ssh->channelList->channel; + + MemIoInit(&io, NULL, 0, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + AssertIntEQ(wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION), + WS_SUCCESS); + AssertTrue(ssh->disconnected); + quietSz = io.outSz; + + WMEMSET(data, 0, sizeof(data)); + + ret = wolfSSH_stream_send(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + ret = wolfSSH_ChannelIdSend(ssh, channelId, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + ret = wolfSSH_ChannelIdSendExt(ssh, channelId, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + ret = wolfSSH_extended_data_send(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + ret = wolfSSH_global_request(ssh, data, sizeof(data), 0); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + ret = wolfSSH_stream_exit(ssh, 0); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + /* Not one byte left the session after the disconnect. */ + AssertIntEQ(io.outSz, quietSz); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + /* Sending SSH_MSG_DISCONNECT ends the session the same way receiving one * does: RFC 4253 section 11.1 says the connection is over once the message * goes out, so the stream calls must refuse afterwards. */ @@ -6454,6 +6576,8 @@ int main(int argc, char** argv) #endif TestDisconnectSetsDisconnectError(); TestDisconnectTerminalWithChannel(); + TestDisconnectDrainsBufferedData(); + TestDisconnectBlocksEverySend(); TestSendDisconnectIsTerminal(); #if !(defined(WOLFSSH_NO_RSA) && defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) TestClientBuffersIdempotent(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index ee728489b..a43422939 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1073,9 +1073,10 @@ struct WOLFSSH { #endif byte connReset; byte isClosed; - /* Set when a DISCONNECT is sent or received. Only wolfSSH_stream_read() - * and wolfSSH_stream_send() are gated on it; the channel-id calls and - * wolfSSH_worker() are not, since the shutdown paths still pump them. */ + /* Set when a DISCONNECT is sent or received. Gates every send call, so + * nothing more goes out. The read calls are not gated: data that + * arrived before the disconnect can still be drained. wolfSSH_worker() + * is not gated either, since the shutdown paths still pump it. */ byte disconnected; byte clientOpenSSH; diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 842de3049..7ed2e1a21 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -558,9 +558,11 @@ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_accept(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_connect(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh); -/* A disconnect, sent or received, ends the session: wolfSSH_stream_read() - * and wolfSSH_stream_send() report WS_DISCONNECT from then on, and channel - * data that arrived before it but was never drained is dropped. */ +/* A disconnect, sent or received, ends the session. Nothing more goes out: + * every send call below reports WS_DISCONNECT from then on. Reads are not + * gated, so channel data that arrived before the disconnect can still be + * drained; wolfSSH_stream_read() reports WS_DISCONNECT once its buffer + * runs dry. RFC 4253 section 11.1. */ WOLFSSH_API int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz); From ad66ac0542173d1aa606b60f4572eec4f9c33a96 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 21 Aug 2026 15:20:33 -0700 Subject: [PATCH 4/6] Report a disconnect from stream_peek wolfSSH_stream_peek() is how the shell loops decide whether a channel is drained. It had no disconnect check, so a dead session looked exactly like a drained one: zero bytes available, nothing to tell them apart. - Report WS_DISCONNECT once the buffered data runs dry, the same shape wolfSSH_stream_read() uses. What is still buffered comes back first. - ssh.h and internal.h name peek alongside the read call, and no longer claim the read side is ungated outright. - regress.c: peek sees the buffered byte, then sees the disconnect. Raised from the channel-eof branch, where peek becomes the drain gate for the wolfsshd and echoserver shell loops. Issue: F-8837 --- src/ssh.c | 16 ++++++++++++++-- tests/regress.c | 9 +++++++++ wolfssh/internal.h | 6 +++--- wolfssh/ssh.h | 4 ++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/ssh.c b/src/ssh.c index 0b1465e81..7ac752195 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1198,6 +1198,7 @@ int wolfSSH_TriggerKeyExchange(WOLFSSH* ssh) int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz) { WOLFSSH_BUFFER* inputBuffer; + word32 avail; WLOG(WS_LOG_DEBUG, "Entering wolfSSH_stream_peek()"); @@ -1214,11 +1215,22 @@ int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz) } inputBuffer = &ssh->channelList->inputBuffer; - bufSz = min(bufSz, inputBuffer->length - inputBuffer->idx); + avail = inputBuffer->length - inputBuffer->idx; + + /* Report the disconnect only once the buffered data is drained, the + * same way wolfSSH_stream_read() does. Callers use this to tell a + * drained channel from one with more to come, and a dead session is + * neither. */ + if (avail == 0 && ssh->disconnected) { + ssh->error = WS_DISCONNECT; + return WS_FATAL_ERROR; + } + + bufSz = min(bufSz, avail); if (buf != NULL) { WMEMCPY(buf, inputBuffer->buffer + inputBuffer->idx, bufSz); } - return bufSz; + return (int)bufSz; } diff --git a/tests/regress.c b/tests/regress.c index b7ae01c96..4f080539c 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -2718,12 +2718,21 @@ static void TestDisconnectDrainsBufferedData(void) AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); AssertTrue(ssh->disconnected); + /* Peek is the drain gate the shell loops use, so it has to tell a + * channel with data left from a session that is over. */ + ret = wolfSSH_stream_peek(ssh, NULL, 1); + AssertIntEQ(ret, 1); + WMEMSET(data, 0, sizeof(data)); ret = wolfSSH_stream_read(ssh, data, sizeof(data)); AssertIntEQ(ret, (int)sizeof(payload)); AssertIntEQ(WMEMCMP(data, payload, sizeof(payload)), 0); /* Buffer is dry now, so the disconnect is what is left to report. */ + ret = wolfSSH_stream_peek(ssh, NULL, 1); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + ret = wolfSSH_stream_read(ssh, data, sizeof(data)); AssertIntEQ(ret, WS_FATAL_ERROR); AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index a43422939..11bb199ce 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1074,9 +1074,9 @@ struct WOLFSSH { byte connReset; byte isClosed; /* Set when a DISCONNECT is sent or received. Gates every send call, so - * nothing more goes out. The read calls are not gated: data that - * arrived before the disconnect can still be drained. wolfSSH_worker() - * is not gated either, since the shutdown paths still pump it. */ + * nothing more goes out. Reads still hand back what arrived before the + * disconnect; the head-of-list reads report it once their buffer runs + * dry. wolfSSH_worker() is not gated, the shutdown paths pump it. */ byte disconnected; byte clientOpenSSH; diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 7ed2e1a21..a4eae930a 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -561,8 +561,8 @@ WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh); /* A disconnect, sent or received, ends the session. Nothing more goes out: * every send call below reports WS_DISCONNECT from then on. Reads are not * gated, so channel data that arrived before the disconnect can still be - * drained; wolfSSH_stream_read() reports WS_DISCONNECT once its buffer - * runs dry. RFC 4253 section 11.1. */ + * drained; wolfSSH_stream_read() and wolfSSH_stream_peek() report + * WS_DISCONNECT once their buffer runs dry. RFC 4253 section 11.1. */ WOLFSSH_API int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz); WOLFSSH_API int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz); From e2e820ac3d1451c453eee0b132979152335be6b9 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 24 Aug 2026 14:25:17 -0700 Subject: [PATCH 5/6] Gate the last three senders on a disconnect The ssh.h comment promised that every send call below it reports WS_DISCONNECT, but three did not: wolfSSH_TriggerKeyExchange(), wolfSSH_SendIgnore() and wolfSSH_SendDisconnect(). - All three now take the SendAfterDisconnect() gate, so the sentence in ssh.h describes the code rather than the intent. - TriggerKeyExchange() is the highwater callback's rekey trigger, so this also stops a rekey starting on a session the peer has ended. - SendIgnore() and SendDisconnect() gained the NULL check the gate needs; both already reported WS_BAD_ARGUMENT for that from the callee. - A second disconnect is refused: one ends the session. - regress.c: the three calls join the send sweep. Issue: F-8837 --- src/ssh.c | 19 +++++++++++++++++++ tests/regress.c | 13 +++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/ssh.c b/src/ssh.c index 7ac752195..29b6eecae 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1185,6 +1185,9 @@ int wolfSSH_TriggerKeyExchange(WOLFSSH* ssh) if (ssh == NULL) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + if (ret == WS_SUCCESS) ret = ssh->error = SendKexInit(ssh); @@ -1542,6 +1545,13 @@ int wolfSSH_SendIgnore(WOLFSSH* ssh, const byte* buf, word32 bufSz) WOLFSSH_UNUSED(buf); WOLFSSH_UNUSED(bufSz); + + if (ssh == NULL) + return WS_BAD_ARGUMENT; + + if (SendAfterDisconnect(ssh)) + return WS_FATAL_ERROR; + WMEMSET(scratch, 0, sizeof(scratch)); return SendIgnore(ssh, scratch, sizeof(scratch)); @@ -1551,6 +1561,15 @@ int wolfSSH_SendIgnore(WOLFSSH* ssh, const byte* buf, word32 bufSz) int wolfSSH_SendDisconnect(WOLFSSH *ssh, word32 reason) { WLOG(WS_LOG_DEBUG, "Entering wolfSSH_SendDisconnect"); + + if (ssh == NULL) + return WS_BAD_ARGUMENT; + + /* One disconnect ends the session; a second is more traffic on a + * connection that is already over. */ + if (SendAfterDisconnect(ssh)) + return WS_FATAL_ERROR; + return SendDisconnect(ssh, reason); } diff --git a/tests/regress.c b/tests/regress.c index 4f080539c..b676f6d72 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -2802,6 +2802,19 @@ static void TestDisconnectBlocksEverySend(void) AssertIntEQ(ret, WS_FATAL_ERROR); AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + ret = wolfSSH_TriggerKeyExchange(ssh); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + ret = wolfSSH_SendIgnore(ssh, data, sizeof(data)); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + /* Including a second disconnect. */ + ret = wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + /* Not one byte left the session after the disconnect. */ AssertIntEQ(io.outSz, quietSz); From 87327f743b2af356f3ea8a5ea539e5e3ef948aff Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 24 Aug 2026 15:27:12 -0700 Subject: [PATCH 6/6] Close the last three post-disconnect sends The disconnect gate left three ways for traffic to reach a peer that had already ended the session, and it made the default highwater callback report a failure for a packet that had gone out fine. - wolfSSH_shutdown() drops the channel when ssh->disconnected is set, so the EOF, exit status and close are skipped along with the wait for a close the peer will never send - wsHighwater() skips the rekey request on a disconnected session, so a firing high water mark no longer turns SendDisconnect() and SendChannelEof() into failures - wolfSSH_ChangeTerminalSize() gained the SendAfterDisconnect() gate, making the ssh.h contract true for every send declared below it - regress covers all three, including that shutdown leaves eofTxd and closeTxd clear and puts nothing on the wire Issue: F-8837 --- src/internal.c | 5 +- src/ssh.c | 11 ++++ tests/regress.c | 133 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/src/internal.c b/src/internal.c index 50029eb2c..73cce3ebf 100644 --- a/src/internal.c +++ b/src/internal.c @@ -554,7 +554,10 @@ static int wsHighwater(byte dir, void* ctx) wolfSSH_GetHighwater(ssh), (dir == WOLFSSH_HWSIDE_RECEIVE) ? "receive" : "transmit"); - ret = wolfSSH_TriggerKeyExchange(ssh); + /* A rekey on a dead session would fail the send that fired the + * mark. */ + if (!ssh->disconnected) + ret = wolfSSH_TriggerKeyExchange(ssh); } return ret; diff --git a/src/ssh.c b/src/ssh.c index 29b6eecae..14217f0bc 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1133,6 +1133,14 @@ int wolfSSH_shutdown(WOLFSSH* ssh) channel = ssh->channelList; } + /* Session already over. Drop the channel to skip the teardown sends + * and the wait for a close that will not come. RFC 4253 section 11.1. */ + if (channel != NULL && ssh->disconnected) { + WLOG(WS_LOG_DEBUG, "Session already disconnected, nothing to send"); + ssh->error = WS_DISCONNECT; + channel = NULL; + } + /* if channel close was not already sent then send it */ if (channel != NULL && !channel->closeTxd) { if (ret == WS_SUCCESS) { @@ -1671,6 +1679,9 @@ int wolfSSH_ChangeTerminalSize(WOLFSSH* ssh, word32 columns, word32 rows, if (ssh == NULL) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + if (ret == WS_SUCCESS) { ret = SendChannelTerminalResize(ssh, columns, rows, widthPixels, heightPixels); diff --git a/tests/regress.c b/tests/regress.c index b676f6d72..0c3a279e0 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -2867,6 +2867,134 @@ static void TestSendDisconnectIsTerminal(void) wolfSSH_CTX_free(ctx); } + +/* wolfSSH_shutdown() is a send path too: no teardown on the wire, and no + * wait for a close that will not come. */ +static void TestShutdownQuietAfterDisconnect(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + WOLFSSH_CHANNEL* channel; + MemIo io; + byte in[128]; + byte out[256]; + word32 inSz; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSend); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AddSessionChannel(ssh); + channel = ssh->channelList; + + inSz = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION, + in, sizeof(in)); + MemIoInit(&io, in, inSz, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + ret = DoReceive(ssh); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertTrue(ssh->disconnected); + io.outSz = 0; + + /* Nothing left to tear down. */ + ret = wolfSSH_shutdown(ssh); + AssertIntEQ(ret, WS_SUCCESS); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + AssertIntEQ(io.outSz, 0); + AssertIntEQ(channel->eofTxd, 0); + AssertIntEQ(channel->closeTxd, 0); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* The mark firing on the disconnect packet must not fail a send that + * went out fine. */ +static void TestHighwaterQuietAfterDisconnect(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[256]; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSend); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AddSessionChannel(ssh); + + MemIoInit(&io, NULL, 0, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + /* Low enough that the disconnect packet trips it. */ + AssertIntEQ(wolfSSH_SetHighwater(ssh, 1), WS_SUCCESS); + + ret = wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION); + AssertIntEQ(ret, WS_SUCCESS); + AssertTrue(ssh->highwaterFlag); + AssertTrue(ssh->disconnected); + + /* The mark fired, but no key exchange was started. */ + AssertIntEQ(ssh->isKeying, 0); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +#if defined(WOLFSSH_TERM) && !defined(NO_FILESYSTEM) +/* A window-change request is a send like any other. */ +static void TestTerminalResizeBlockedAfterDisconnect(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[256]; + word32 quietSz; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSend); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AddSessionChannel(ssh); + + MemIoInit(&io, NULL, 0, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + AssertIntEQ(wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION), + WS_SUCCESS); + quietSz = io.outSz; + + ret = wolfSSH_ChangeTerminalSize(ssh, 80, 24, 0, 0); + AssertIntEQ(ret, WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + AssertIntEQ(io.outSz, quietSz); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} +#endif /* WOLFSSH_TERM && !NO_FILESYSTEM */ + #ifdef WOLFSSH_SFTP static void TestOct2DecRejectsInvalidNonLeadingDigit(void) { @@ -6601,6 +6729,11 @@ int main(int argc, char** argv) TestDisconnectDrainsBufferedData(); TestDisconnectBlocksEverySend(); TestSendDisconnectIsTerminal(); + TestShutdownQuietAfterDisconnect(); + TestHighwaterQuietAfterDisconnect(); +#if defined(WOLFSSH_TERM) && !defined(NO_FILESYSTEM) + TestTerminalResizeBlockedAfterDisconnect(); +#endif #if !(defined(WOLFSSH_NO_RSA) && defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) TestClientBuffersIdempotent(); #endif