diff --git a/src/internal.c b/src/internal.c index 316397e69..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; @@ -8166,6 +8169,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 +16738,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 1f3f7cc49..14217f0bc 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; @@ -1113,9 +1128,17 @@ 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; + } + + /* 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 */ @@ -1170,6 +1193,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); @@ -1183,6 +1209,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()"); @@ -1199,11 +1226,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; } @@ -1228,8 +1266,17 @@ 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->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; @@ -1244,6 +1291,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); @@ -1307,7 +1361,13 @@ 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 (SendAfterDisconnect(ssh)) + return WS_FATAL_ERROR; + + if (ssh->channelList == NULL) return WS_BAD_ARGUMENT; if (ssh->isKeying) { @@ -1334,6 +1394,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) { @@ -1370,6 +1433,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) { @@ -1403,6 +1469,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); @@ -1426,6 +1495,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); } @@ -1436,7 +1507,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) { @@ -1476,6 +1553,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)); @@ -1485,6 +1569,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); } @@ -1586,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 826ed1d12..0c3a279e0 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,10 +2596,405 @@ 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); +} + + +/* 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); + + /* 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); + + 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); + + 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); + + 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); +} + + +/* 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) { @@ -6328,6 +6725,15 @@ int main(int argc, char** argv) TestDoNewKeys(); #endif TestDisconnectSetsDisconnectError(); + TestDisconnectTerminalWithChannel(); + 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 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); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index c40d78edc..11bb199ce 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1073,6 +1073,11 @@ struct WOLFSSH { #endif byte connReset; byte isClosed; + /* Set when a DISCONNECT is sent or received. Gates every send call, so + * 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; byte kexId; diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index dfb45ab60..a4eae930a 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -558,6 +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. 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() 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);