diff --git a/src/ssh.c b/src/ssh.c index ae7d0b2a0..c02609769 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -1149,17 +1149,20 @@ static int SendAfterDisconnect(WOLFSSH* ssh) } -/* A disconnect of ours left queued by a short send still has to reach the +/* Whatever a short send left in the output buffer still has to reach the * peer, and flushing bytes that are already bundled is not the new traffic - * RFC 4253 section 11.1 forbids. Only our own disconnect qualifies: a - * disconnect from the peer leaves nothing queued but unrelated traffic, - * which the session is over for. Call only after a NULL check of ssh. */ -static int FlushQueuedDisconnect(WOLFSSH* ssh) + * RFC 4253 section 11.1 forbids. Once the peer has disconnected, though, + * only our own queued disconnect still qualifies: anything else in there + * belongs to a session that is over. Call only after a NULL check of ssh. */ +static int FlushQueuedOutput(WOLFSSH* ssh) { - if (!ssh->disconnectTxd || !wolfSSH_OutputPending(ssh)) + if (!wolfSSH_OutputPending(ssh)) return 0; - WLOG(WS_LOG_DEBUG, "Flushing a disconnect left queued by a short send"); + if (ssh->disconnected && !ssh->disconnectTxd) + return 0; + + WLOG(WS_LOG_DEBUG, "Flushing output left queued by a short send"); return 1; } @@ -1168,6 +1171,7 @@ int wolfSSH_shutdown(WOLFSSH* ssh) { int ret = WS_SUCCESS; int flushRet = WS_SUCCESS; + int flushed = 0; WOLFSSH_CHANNEL* channel = NULL; WLOG(WS_LOG_DEBUG, "Entering wolfSSH_shutdown()"); @@ -1175,10 +1179,14 @@ int wolfSSH_shutdown(WOLFSSH* ssh) if (ssh == NULL) ret = WS_BAD_ARGUMENT; - /* This is a teardown call, so a disconnect of ours left queued by a - * short send goes out here, with or without a channel to tear down. */ - if (ret == WS_SUCCESS && FlushQueuedDisconnect(ssh)) + /* This is a teardown call, so anything a short send left queued goes out + * here, with or without a channel to tear down. A rejected auth's + * USERAUTH_FAILURE has no channel, and a channel close is retired off + * the channel the moment it is bundled. */ + if (ret == WS_SUCCESS && FlushQueuedOutput(ssh)) { flushRet = wolfSSH_SendPacket(ssh); + flushed = flushRet == WS_SUCCESS; + } if (ret == WS_SUCCESS && ssh->channelList == NULL) ret = WS_BAD_ARGUMENT; @@ -1202,6 +1210,13 @@ int wolfSSH_shutdown(WOLFSSH* ssh) if (ssh != NULL && ssh->disconnected && flushRet == WS_SUCCESS) ssh->error = WS_DISCONNECT; + /* A live session has no WS_DISCONNECT to displace that stale error with, + * and the widened flush reaches sessions that are still up. The write the + * short send latched WS_WANT_WRITE for is the one that just finished, so + * it is not owed twice. */ + else if (flushed && ssh->error == WS_WANT_WRITE) + ssh->error = WS_SUCCESS; + /* if channel close was not already sent then send it */ if (channel != NULL && !channel->closeTxd) { if (ret == WS_SUCCESS) { @@ -1227,8 +1242,11 @@ int wolfSSH_shutdown(WOLFSSH* ssh) /* if the channel was not yet removed then read to get - * response to SendChannelClose */ - if (channel != NULL && ret == WS_SUCCESS) { + * response to SendChannelClose. Not while the flush left output queued: + * the peer cannot answer a close it has not finished receiving, and the + * worker has nothing to read, so a want-read from it would send the + * caller to wait on the wrong side of the socket. */ + if (channel != NULL && ret == WS_SUCCESS && !wolfSSH_OutputPending(ssh)) { ret = wolfSSH_worker(ssh, NULL); if (ret == WS_CHAN_RXD || ret == WS_EOF) { /* received response */ @@ -1242,8 +1260,16 @@ int wolfSSH_shutdown(WOLFSSH* ssh) } /* An unfinished flush outranks the channel status: the caller has to - * come back for the rest of the disconnect. */ - if (flushRet != WS_SUCCESS) + * come back for the rest of the disconnect. Not so once a teardown send + * has carried the leftovers out with it: that write is settled, and the + * teardown result stands. Nor does it outrank a teardown send that + * failed: a reset leaves the buffer intact, so the flush still reads as + * owed while the send holds the real reason. A flush that failed + * outright reports whatever else happened. */ + if (flushRet != WS_SUCCESS && + (flushRet != WS_WANT_WRITE || + (wolfSSH_OutputPending(ssh) && + (ret == WS_SUCCESS || ret == WS_CHANNEL_CLOSED)))) ret = flushRet; WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_shutdown(), ret = %d", ret); @@ -1760,7 +1786,7 @@ int wolfSSH_SendDisconnect(WOLFSSH *ssh, word32 reason) * connection that is already over. A short send leaves the first one * queued, though, so that retry goes through. */ if (SendAfterDisconnect(ssh)) { - if (FlushQueuedDisconnect(ssh)) + if (FlushQueuedOutput(ssh)) return wolfSSH_SendPacket(ssh); return WS_FATAL_ERROR; } diff --git a/tests/regress.c b/tests/regress.c index eca4fd729..a846e8c0c 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -176,6 +176,20 @@ static WS_MAYBE_UNUSED word32 ReadUint32(const byte* buf) ((word32)buf[2] << 8) | (word32)buf[3]; } +/* Where the packet after this one starts: a length prefix and the bytes it + * counts. A packet that ends the output fits exactly, so summing spans can + * account for every byte written. */ +static word32 NextPacketOffset(const byte* out, word32 outSz) +{ + word32 packetLen; + + AssertTrue(outSz >= UINT32_SZ); + packetLen = ReadUint32(out); + + AssertTrue(outSz - UINT32_SZ >= packetLen); + return UINT32_SZ + packetLen; +} + static word32 AppendData(byte* buf, word32 bufSz, word32 idx, const byte* data, word32 dataSz) { @@ -5605,20 +5619,6 @@ static void ArmFwdReentrantFromSend(ChannelOpenHarness* harness, int action, fwdReentrantFromSend.port = port; } -/* Where the second packet in the output starts. What the callback sent or - * answered went out behind the request that was still in the buffer. */ -static word32 NextPacketOffset(const byte* out, word32 outSz) -{ - word32 packetLen; - - AssertTrue(outSz > UINT32_SZ); - WMEMCPY(&packetLen, out, sizeof(packetLen)); - packetLen = ntohl(packetLen); - - AssertTrue(outSz > UINT32_SZ + packetLen); - return UINT32_SZ + packetLen; -} - /* A cancel sent from inside a first setup's send names a forward that exists * only on the request in flight. It went out behind that setup, so the peer * holds no listener and neither may this side -- and the setup's commit must @@ -7032,6 +7032,449 @@ static void TestShutdownFlushesWithNoChannel(void) } +/* The flush is no longer tied to a disconnect, so it reaches sessions that + * are still up, where there is no WS_DISCONNECT to displace the WS_WANT_WRITE + * the short send latched. A completed flush is the write that error was + * asking for: left standing it sends the caller back for a write that is + * already done, and the apps answer a want-write with a worker loop. */ +static void TestShutdownFlushClearsWantWrite(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[256]; + byte data[8]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSendWantWrite); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + + MemIoInit(&io, NULL, 0, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + /* An IGNORE needs no channel, so the short send leaves the want-write + * with nothing but teardown left to answer it. */ + WMEMSET(data, 0, sizeof(data)); + MemSendWantWriteCount = 1; + AssertIntEQ(wolfSSH_SendIgnore(ssh, data, sizeof(data)), WS_WANT_WRITE); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_WRITE); + AssertTrue(wolfSSH_OutputPending(ssh)); + AssertFalse(ssh->disconnected); + AssertFalse(ssh->disconnectTxd); + AssertNull(ssh->channelList); + AssertIntEQ(io.outSz, 0); + + AssertIntEQ(wolfSSH_shutdown(ssh), WS_CHANNEL_CLOSED); + AssertFalse(wolfSSH_OutputPending(ssh)); + AssertIntEQ(ParseMsgId(out, io.outSz), MSGID_IGNORE); + AssertIntEQ(wolfSSH_get_error(ssh), WS_SUCCESS); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +#ifndef NO_WOLFSSH_SERVER + +static int RejectUserAuthCb(byte authType, WS_UserAuthData* authData, + void* ctx) +{ + WOLFSSH_UNUSED(authType); + WOLFSSH_UNUSED(authData); + WOLFSSH_UNUSED(ctx); + + return WOLFSSH_USERAUTH_REJECTED; +} + + +/* A rejected authentication owes the peer a USERAUTH_FAILURE, RFC 4252 + * section 5.1. Below the auth-failure cap nothing queues a disconnect to + * drag the buffer out, and authentication never completed so there is no + * channel: teardown is the only thing left that can push the reply. */ +static void TestShutdownFlushesQueuedUserAuthFailure(void) +{ + ChannelOpenHarness harness; + byte in[128]; + word32 inSz; + + inSz = BuildUserAuthPasswordRequest("alice", "pw", in, sizeof(in)); + + InitUserAuthHarness(&harness, in, inSz); + wolfSSH_SetUserAuth(harness.ctx, RejectUserAuthCb); + wolfSSH_SetIOSend(harness.ctx, MemSendWantWrite); + + /* The failure is bundled, then the socket refuses it. */ + MemSendWantWriteCount = 1; + AssertIntEQ(DoReceive(harness.ssh), WS_FATAL_ERROR); + AssertIntEQ(harness.ssh->error, WS_USER_AUTH_E); + AssertIntEQ(harness.io.outSz, 0); + AssertTrue(wolfSSH_OutputPending(harness.ssh)); + + /* Neither of the two things that would have flushed it anyway. */ + AssertFalse(harness.ssh->disconnectTxd); + AssertNull(harness.ssh->channelList); + + /* The socket takes bytes now, so only the gate stands between the + * queued failure and the peer. */ + AssertIntEQ(wolfSSH_shutdown(harness.ssh), WS_CHANNEL_CLOSED); + AssertFalse(wolfSSH_OutputPending(harness.ssh)); + + /* The failure, and nothing bundled behind it. */ + AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz), + MSGID_USERAUTH_FAILURE); + AssertIntEQ(harness.io.outSz, + NextPacketOffset(harness.io.out, harness.io.outSz)); + + FreeChannelOpenHarness(&harness); +} + +#endif /* !NO_WOLFSSH_SERVER */ + + +/* DoChannelClose() retires the channel once the close is bundled, so a close + * the socket refused is left with no channel to carry the retry. Teardown + * still owes the peer that close, RFC 4254 section 5.3. */ +static void TestShutdownFlushesQueuedChannelClose(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte in[128]; + byte out[512]; + word32 inSz; + word32 channelId; + word32 closeOff; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSendWantWrite); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AddSessionChannel(ssh); + channelId = ssh->channelList->channel; + /* Past userauth, or the message filter turns the close away and the + * wire checks below prove nothing. */ + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + + inSz = BuildChannelClosePacket(channelId, in, sizeof(in)); + MemIoInit(&io, in, inSz, out, sizeof(out)); + wolfSSH_SetIOReadCtx(ssh, &io); + wolfSSH_SetIOWriteCtx(ssh, &io); + + /* Our EOF and close answer the peer's close. Refuse both sends, or the + * second one carries the first out with it. */ + MemSendWantWriteCount = 2; + AssertIntEQ(DoReceive(ssh), WS_CHANNEL_CLOSED); + AssertIntEQ(io.outSz, 0); + AssertTrue(wolfSSH_OutputPending(ssh)); + + /* The channel that would have retried the send is already gone. */ + AssertNull(ssh->channelList); + AssertFalse(ssh->disconnectTxd); + + AssertIntEQ(wolfSSH_shutdown(ssh), WS_CHANNEL_CLOSED); + AssertFalse(wolfSSH_OutputPending(ssh)); + + /* Both halves reached the peer, EOF ahead of the close, and nothing + * else: the two packets account for every byte written. */ + AssertIntEQ(ParseMsgId(out, io.outSz), MSGID_CHANNEL_EOF); + closeOff = NextPacketOffset(out, io.outSz); + AssertIntEQ(ParseMsgId(out + closeOff, io.outSz - closeOff), + MSGID_CHANNEL_CLOSE); + AssertIntEQ(io.outSz, + closeOff + NextPacketOffset(out + closeOff, io.outSz - closeOff)); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* One copy each of the channel data and the three teardown messages, in + * order, accounting for every byte written. */ +static void AssertDataThenTeardownOnWire(const byte* out, word32 outSz) +{ + word32 eofOff; + word32 reqOff; + word32 closeOff; + + AssertIntEQ(ParseMsgId(out, outSz), MSGID_CHANNEL_DATA); + eofOff = NextPacketOffset(out, outSz); + AssertIntEQ(ParseMsgId(out + eofOff, outSz - eofOff), MSGID_CHANNEL_EOF); + reqOff = eofOff + NextPacketOffset(out + eofOff, outSz - eofOff); + AssertIntEQ(ParseMsgId(out + reqOff, outSz - reqOff), + MSGID_CHANNEL_REQUEST); + closeOff = reqOff + NextPacketOffset(out + reqOff, outSz - reqOff); + AssertIntEQ(ParseMsgId(out + closeOff, outSz - closeOff), + MSGID_CHANNEL_CLOSE); + AssertIntEQ(outSz, + closeOff + NextPacketOffset(out + closeOff, outSz - closeOff)); +} + + +/* The flush is no longer tied to a disconnect, so it can fire on a live + * session with the channel still listed and then short-send itself. The + * teardown sends bundle in behind bytes the socket has not taken, and the + * caller has to come back: the retry owes the peer the rest of the buffer + * and not a second copy of the EOF and close. */ +static void TestShutdownFlushShortSendsWithChannel(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + WOLFSSH_CHANNEL* channel; + MemIo io; + byte out[1024]; + byte data[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSendWantWrite); + + 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); + + /* Channel data the socket will not take, on a session that is neither + * disconnected nor out of channels. */ + WMEMSET(data, 'x', sizeof(data)); + MemSendWantWriteCount = 5; + AssertIntEQ(wolfSSH_stream_send(ssh, data, sizeof(data)), + (int)sizeof(data)); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_WRITE); + AssertTrue(wolfSSH_OutputPending(ssh)); + AssertFalse(ssh->disconnected); + AssertNotNull(ssh->channelList); + + /* The flush and all three teardown sends are refused, so the unfinished + * flush owns the return and the whole teardown is still queued. */ + AssertIntEQ(wolfSSH_shutdown(ssh), WS_WANT_WRITE); + AssertIntEQ(io.outSz, 0); + AssertTrue(wolfSSH_OutputPending(ssh)); + AssertIntEQ(MemSendWantWriteCount, 0); + + /* Bundled counts as sent, so the retry must not emit them again. The + * list comes first: a channel retired here would be freed, and the + * reads below it would be of freed memory. */ + AssertNotNull(ssh->channelList); + AssertTrue(channel->eofTxd); + AssertTrue(channel->closeTxd); + + /* The socket takes bytes now. The retry drains what was queued and + * lands on the wait for the peer's close, not on a stale want-write + * for a buffer that is already empty. */ + AssertIntEQ(wolfSSH_shutdown(ssh), WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_READ); + AssertFalse(wolfSSH_OutputPending(ssh)); + + /* One copy of each, in order, accounting for every byte written. */ + AssertDataThenTeardownOnWire(out, io.outSz); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* The retry's own flush can short-send too, with the teardown already + * bundled and the channel still listed, waiting on the peer's close. The + * worker then has nothing to read, and a want-read is the wrong answer: the + * peer cannot reply to a close it has not finished receiving, so the bytes + * still queued are what the caller has to come back for. */ +static void TestShutdownRetryFlushShortSendsWithChannel(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + WOLFSSH_CHANNEL* channel; + MemIo io; + byte out[1024]; + byte data[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSendWantWrite); + + 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); + + WMEMSET(data, 'x', sizeof(data)); + MemSendWantWriteCount = 5; + AssertIntEQ(wolfSSH_stream_send(ssh, data, sizeof(data)), + (int)sizeof(data)); + AssertIntEQ(wolfSSH_shutdown(ssh), WS_WANT_WRITE); + AssertIntEQ(io.outSz, 0); + AssertNotNull(ssh->channelList); + AssertTrue(channel->closeTxd); + + /* The retry's flush is refused as well. Nothing else in the call can + * carry the bytes: the teardown is skipped as already sent, and the + * worker has nothing to read. */ + MemSendWantWriteCount = 1; + AssertIntEQ(wolfSSH_shutdown(ssh), WS_WANT_WRITE); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_WRITE); + AssertTrue(wolfSSH_OutputPending(ssh)); + AssertIntEQ(io.outSz, 0); + AssertNotNull(ssh->channelList); + + /* The socket takes bytes now: the queue drains and the call lands on + * the wait for the peer's close. */ + AssertIntEQ(wolfSSH_shutdown(ssh), WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_READ); + AssertFalse(wolfSSH_OutputPending(ssh)); + AssertDataThenTeardownOnWire(out, io.outSz); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* The flush can also be the only short send: the socket takes the EOF, and + * the bytes it was queued behind go out with it. That settles the write the + * flush was owed, so the return belongs to the teardown, not to a stale + * WS_WANT_WRITE for a buffer that is already empty. */ +static void TestShutdownFlushSettledByTeardown(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + WOLFSSH_CHANNEL* channel; + MemIo io; + byte out[1024]; + byte data[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSendWantWrite); + + 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); + + /* The data send and the flush are refused; the EOF behind them is not. */ + WMEMSET(data, 'x', sizeof(data)); + MemSendWantWriteCount = 2; + AssertIntEQ(wolfSSH_stream_send(ssh, data, sizeof(data)), + (int)sizeof(data)); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_WRITE); + AssertTrue(wolfSSH_OutputPending(ssh)); + + /* Everything went out on the EOF send, and the call lands on the wait + * for the peer's close. */ + AssertIntEQ(wolfSSH_shutdown(ssh), WS_FATAL_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_READ); + AssertIntEQ(MemSendWantWriteCount, 0); + AssertFalse(wolfSSH_OutputPending(ssh)); + AssertNotNull(ssh->channelList); + AssertTrue(channel->eofTxd); + AssertTrue(channel->closeTxd); + + AssertDataThenTeardownOnWire(out, io.outSz); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + +/* A reset leaves the output buffer intact, so a flush that short-sent still + * reads as owed after the teardown send behind it hit the reset. That send + * holds the real reason the session is over, and it is what the caller has + * to see: a stale want-write sends them back to wait on a socket that is + * never going to take the rest. */ +static int MemSendResetAfterCount; + +static int MemSendWantWriteThenReset(WOLFSSH* ssh, void* buf, word32 sz, + void* ctx) +{ + WOLFSSH_UNUSED(ssh); + WOLFSSH_UNUSED(buf); + WOLFSSH_UNUSED(sz); + WOLFSSH_UNUSED(ctx); + + if (MemSendResetAfterCount > 0) { + MemSendResetAfterCount--; + return WS_CBIO_ERR_WANT_WRITE; + } + return WS_CBIO_ERR_CONN_RST; +} + +static void TestShutdownResetOutranksFlush(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte out[1024]; + byte data[32]; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + wolfSSH_SetIORecv(ctx, MemRecv); + wolfSSH_SetIOSend(ctx, MemSendWantWriteThenReset); + + 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); + + /* The data send and the flush are refused; the peer resets the + * connection under the teardown sends that follow. */ + WMEMSET(data, 'x', sizeof(data)); + MemSendResetAfterCount = 2; + AssertIntEQ(wolfSSH_stream_send(ssh, data, sizeof(data)), + (int)sizeof(data)); + AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_WRITE); + AssertTrue(wolfSSH_OutputPending(ssh)); + + /* The reset is the answer, not the write the flush is still owed for. */ + AssertIntEQ(wolfSSH_shutdown(ssh), WS_SOCKET_ERROR_E); + AssertTrue(ssh->connReset); + AssertIntEQ(io.outSz, 0); + + /* Nothing left to take those bytes, so a retry says so too rather than + * handing back another want-write for a buffer that cannot drain. */ + AssertTrue(wolfSSH_OutputPending(ssh)); + AssertIntEQ(wolfSSH_shutdown(ssh), WS_SOCKET_ERROR_E); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + /* The highwater callback is the application's, and its return propagates out * of wolfSSH_SendPacket(). A mark firing on the disconnect packet must not * fail a send that went out fine, whoever owns the callback. */ @@ -12283,6 +12726,15 @@ int main(int argc, char** argv) TestPeerDisconnectKeepsTrafficQueued(0); TestPeerDisconnectKeepsTrafficQueued(1); TestShutdownFlushesWithNoChannel(); + TestShutdownFlushClearsWantWrite(); +#ifndef NO_WOLFSSH_SERVER + TestShutdownFlushesQueuedUserAuthFailure(); +#endif + TestShutdownFlushesQueuedChannelClose(); + TestShutdownFlushShortSendsWithChannel(); + TestShutdownRetryFlushShortSendsWithChannel(); + TestShutdownFlushSettledByTeardown(); + TestShutdownResetOutranksFlush(); TestQueuedDisconnectFlushes(); TestShutdownFlushesQueuedDisconnect(); TestShutdownKeepsFlushWantWrite(); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 545ef9e3c..631669b74 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -701,6 +701,14 @@ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_accept(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_connect(WOLFSSH* ssh); +/* Tears down the first channel in the list, and flushes whatever a short + * send left queued, not just a disconnect of ours: a rejected auth's + * USERAUTH_FAILURE, and a CHANNEL_CLOSE whose channel was retired the + * moment it was bundled, have nothing else left to carry the retry. That + * flush can be short too, so a WS_WANT_WRITE from here may be owed to it + * rather than to the teardown sends; either way the caller retries. Once + * the peer has disconnected, only our own queued disconnect still goes + * out, per the comment below. */ WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh); /* A disconnect, sent or received, ends the session. Nothing more goes out: * wolfSSH_shutdown() above this comment, and every send call below it,