diff --git a/src/internal.c b/src/internal.c index e0a73d79..ba095cce 100644 --- a/src/internal.c +++ b/src/internal.c @@ -7635,8 +7635,9 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) /* Returns amount bytes of receive-window credit to the peer, folding in credit * already parked on the channel. Credit that cannot reach the transport is * parked, not dropped: no WINDOW_ADJUST may be sent mid-rekey (RFC 4253 section - * 7.1), an unbundled packet queued nothing, and a socket error can discard what - * was bundled. Credit that reached the output buffer counts as delivered. */ + * 7.1) or after a disconnect (section 11.1), an unbundled packet queued + * nothing, and a socket error can discard what was bundled. Credit that reached + * the output buffer counts as delivered. */ int ChannelCreditWindow(WOLFSSH* ssh, WOLFSSH_CHANNEL* channel, word32 amount) { word32 total; @@ -7655,7 +7656,10 @@ int ChannelCreditWindow(WOLFSSH* ssh, WOLFSSH_CHANNEL* channel, word32 amount) if (total == 0) return WS_SUCCESS; - if (ssh->isKeying) { + /* The reads that drain what arrived before a disconnect still credit the + * window locally, but the session is over and nothing more may go out. + * Park the credit so the read reports its bytes, not a send failure. */ + if (ssh->isKeying || ssh->disconnected) { channel->pendingWindowAdjust = total; return WS_SUCCESS; } diff --git a/src/ssh.c b/src/ssh.c index 86a1710c..95dae60f 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) { @@ -1416,7 +1443,15 @@ int wolfSSH_stream_exit(WOLFSSH* ssh, int status) WLOG(WS_LOG_DEBUG, "Entering wolfSSH_stream_exit(), status = %d", status); - if (ssh == NULL || ssh->channelList == NULL) + if (ssh == NULL) + ret = WS_BAD_ARGUMENT; + + /* Ahead of the channel-list test, like the other stream calls, so a + * torn-down session reports the disconnect and not a bad argument. */ + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + + if (ret == WS_SUCCESS && ssh->channelList == NULL) ret = WS_BAD_ARGUMENT; if (ret == WS_SUCCESS) @@ -1442,6 +1477,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 +1489,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) { @@ -3570,6 +3613,9 @@ WOLFSSH_CHANNEL* wolfSSH_ChannelFwdNewLocal(WOLFSSH* ssh, if (ssh == NULL || ssh->ctx == NULL || host == NULL || origin == NULL) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + if (ret == WS_SUCCESS) { newChannel = ChannelNew(ssh, ID_CHANTYPE_TCPIP_DIRECT, ssh->ctx->windowSz, ssh->ctx->maxPacketSz); @@ -3609,6 +3655,9 @@ WOLFSSH_CHANNEL* wolfSSH_ChannelFwdNewRemote(WOLFSSH* ssh, if (ssh == NULL || ssh->ctx == NULL || host == NULL || origin == NULL) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + if (ret == WS_SUCCESS) { newChannel = ChannelNew(ssh, ID_CHANTYPE_TCPIP_FORWARD, ssh->ctx->windowSz, ssh->ctx->maxPacketSz); @@ -3677,6 +3726,9 @@ int wolfSSH_FwdRemoteSetup(WOLFSSH* ssh, const char* bindAddr, if (ret == WS_SUCCESS && ssh->ctx->side != WOLFSSH_ENDPOINT_CLIENT) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + /* A global request must not go out mid-rekey; only KEX traffic may. */ if (ret == WS_SUCCESS && ssh->isKeying) ret = WS_REKEYING; @@ -3712,6 +3764,9 @@ int wolfSSH_FwdRemoteCancel(WOLFSSH* ssh, const char* bindAddr, if (ret == WS_SUCCESS && ssh->ctx->side != WOLFSSH_ENDPOINT_CLIENT) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) + ret = WS_FATAL_ERROR; + /* A global request must not go out mid-rekey; only KEX traffic may. */ if (ret == WS_SUCCESS && ssh->isKeying) ret = WS_REKEYING; @@ -4029,6 +4084,9 @@ int wolfSSH_ChannelSend(WOLFSSH_CHANNEL* channel, WLOG(WS_LOG_DEBUG, "Entering wolfSSH_ChannelSend(), ID = %d, peerID = %d", channel->channel, channel->peerChannel); + if (channel->ssh != NULL && SendAfterDisconnect(channel->ssh)) + return WS_FATAL_ERROR; + #ifdef DEBUG_WOLFSSH DumpOctetString(buf, bufSz); #endif @@ -4064,6 +4122,9 @@ int wolfSSH_ChannelSendExt(WOLFSSH_CHANNEL* channel, "Entering wolfSSH_ChannelSendExt(), ID = %d, peerID = %d", channel->channel, channel->peerChannel); + if (channel->ssh != NULL && SendAfterDisconnect(channel->ssh)) + return WS_FATAL_ERROR; + #ifdef DEBUG_WOLFSSH DumpOctetString(buf, bufSz); #endif @@ -4093,6 +4154,10 @@ int wolfSSH_ChannelExit(WOLFSSH_CHANNEL* channel) if (channel == NULL) ret = WS_BAD_ARGUMENT; + if (ret == WS_SUCCESS && channel->ssh != NULL && + SendAfterDisconnect(channel->ssh)) + ret = WS_FATAL_ERROR; + if (ret == WS_SUCCESS) ret = SendChannelEof(channel->ssh, channel->peerChannel); diff --git a/tests/regress.c b/tests/regress.c index 48d0ee88..ae8b3739 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -2965,6 +2965,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. */ @@ -3011,6 +3133,206 @@ static void TestSendDisconnectIsTerminal(void) wolfSSH_CTX_free(ctx); } + +/* The reads that drain what arrived before a disconnect must not put a + * window adjust on the wire. The credit is parked on the channel instead, + * so the read still reports its bytes rather than a send failure. */ +static void TestDisconnectQuietWindowAdjust(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + WOLFSSH_CHANNEL* channel; + MemIo io; + byte in[128]; + byte out[256]; + byte payload[600]; + byte extPayload[64]; + byte data[600]; + word32 inSz; + word32 windowSz; + word32 pendingSz; + 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; + /* Past userauth, or the message filter blocks the adjust on its own and + * the wire check below proves nothing. */ + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + + /* More than half the channel buffer, so draining it trips the window + * update in _UpdateChannelWindow(). */ + WMEMSET(payload, 'a', sizeof(payload)); + AssertIntEQ(ChannelPutData(channel, payload, sizeof(payload)), WS_SUCCESS); + + /* Buffered stderr for the extended-data drain. */ + WMEMSET(extPayload, 'e', sizeof(extPayload)); + AssertIntEQ(GrowBuffer(&channel->extDataBuffer, sizeof(extPayload)), + WS_SUCCESS); + WMEMCPY(channel->extDataBuffer.buffer, extPayload, sizeof(extPayload)); + channel->extDataBuffer.length = sizeof(extPayload); + channel->extDataBuffer.idx = 0; + + 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; + + /* Two reads: the first leaves the index non-zero, the second is the one + * with credit to return. */ + ret = wolfSSH_stream_read(ssh, data, 300); + AssertIntEQ(ret, 300); + ret = wolfSSH_stream_read(ssh, data, 300); + AssertIntEQ(ret, 300); + AssertIntEQ(io.outSz, 0); + + /* The credit is owed, not lost. The window update runs before the read + * advances the index, so the second read is the one that credits the + * first read's 300 bytes. */ + AssertIntEQ(channel->pendingWindowAdjust, 300); + pendingSz = channel->pendingWindowAdjust; + windowSz = channel->windowSz; + + ret = wolfSSH_extended_data_read(ssh, data, sizeof(extPayload)); + AssertIntEQ(ret, (int)sizeof(extPayload)); + AssertIntEQ(io.outSz, 0); + AssertIntEQ(channel->pendingWindowAdjust, + pendingSz + (word32)sizeof(extPayload)); + AssertIntEQ(channel->windowSz, windowSz + (word32)sizeof(extPayload)); + + /* wolfsshd and echoserver read by channel ID, so cover that drain too. */ + AssertIntEQ(ChannelPutData(channel, payload, sizeof(payload)), WS_SUCCESS); + ret = wolfSSH_ChannelIdRead(ssh, channel->channel, data, sizeof(payload)); + AssertIntEQ(ret, (int)sizeof(payload)); + AssertIntEQ(io.outSz, 0); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* The channel-pointer and forwarding APIs are send calls too. */ +static void TestDisconnectBlocksChannelAndFwdSends(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + WOLFSSH_CHANNEL* channel; + MemIo io; + byte out[256]; + byte data[8]; + word32 quietSz; + + 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; + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + + 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; + + WMEMSET(data, 0, sizeof(data)); + + AssertIntEQ(wolfSSH_ChannelSend(channel, data, sizeof(data)), + WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + AssertIntEQ(wolfSSH_ChannelSendExt(channel, data, sizeof(data)), + WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + AssertIntEQ(wolfSSH_ChannelExit(channel), WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + +#ifdef WOLFSSH_FWD + AssertIntEQ(wolfSSH_FwdRemoteSetup(ssh, "127.0.0.1", 22, 0), + WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + AssertIntEQ(wolfSSH_FwdRemoteCancel(ssh, "127.0.0.1", 22, 0), + WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + AssertNull(wolfSSH_ChannelFwdNewLocal(ssh, "127.0.0.1", 22, + "127.0.0.1", 22)); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + AssertNull(wolfSSH_ChannelFwdNewRemote(ssh, "127.0.0.1", 22, + "127.0.0.1", 22)); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); +#endif + + /* The channel is still whole: nothing was torn down either. */ + AssertIntEQ(channel->eofTxd, 0); + AssertIntEQ(channel->closeTxd, 0); + AssertIntEQ(io.outSz, quietSz); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* wolfSSH_stream_exit() answers like the rest of its family on a session + * whose channel is already gone: the disconnect, not a bad argument. */ +static void TestStreamExitReportsDisconnect(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[256]; + + 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); + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + + 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); + AssertIntEQ(ChannelRemove(ssh, ssh->channelList->channel, + WS_CHANNEL_ID_SELF), WS_SUCCESS); + AssertNull(ssh->channelList); + + AssertIntEQ(wolfSSH_stream_exit(ssh, 0), WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + #ifdef WOLFSSH_SFTP static void TestOct2DecRejectsInvalidNonLeadingDigit(void) { @@ -6738,7 +7060,12 @@ int main(int argc, char** argv) #endif TestDisconnectSetsDisconnectError(); TestDisconnectTerminalWithChannel(); + TestDisconnectDrainsBufferedData(); + TestDisconnectBlocksEverySend(); TestSendDisconnectIsTerminal(); + TestDisconnectQuietWindowAdjust(); + TestDisconnectBlocksChannelAndFwdSends(); + TestStreamExitReportsDisconnect(); #if !(defined(WOLFSSH_NO_RSA) && defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) TestClientBuffersIdempotent(); #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 898eafca..6d113733 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1102,9 +1102,11 @@ 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 public 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 842de304..bc1b8313 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -558,9 +558,12 @@ 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 in this header, above this comment and below it, + * 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); @@ -588,7 +591,8 @@ WOLFSSH_API int wolfSSH_extended_data_send(WOLFSSH* ssh, byte* buf, word32 bufSz * can be short: the byte count is still returned, but wolfSSH_get_error() is * left at WS_WANT_WRITE to show a flush is owed. An app that only reads must * then flush, with wolfSSH_worker(), or the peer's window is never replenished - * and the channel stalls. + * and the channel stalls. After a disconnect there is nothing to flush: the + * credit is parked on the channel rather than sent. * * The buffer lives on the channel: anything unread when the channel is removed * (the peer's CHANNEL_CLOSE) is discarded with it. */