ssh: flush any queued output at teardown

wolfSSH_shutdown() flushes whatever a short send left in the output
buffer, not just a queued disconnect. A rejected auth's
USERAUTH_FAILURE has no channel, and DoChannelClose() retires the
channel as soon as the close is bundled, so neither had anything
left to carry the retry.

- The gate still refuses a flush once the peer has disconnected,
  unless our own disconnect is the thing queued.
- A flush that finishes on a live session clears the WS_WANT_WRITE
  the short send latched, as the disconnected path already did.
- ssh.h documents the widened flush and the WS_WANT_WRITE a short
  one leaves wolfSSH_shutdown() returning.
- Tests cover the rejected auth with no channel and the close whose
  channel DoChannelClose() already retired.
- A flush that short-sends on a live channel leaves the teardown
  queued behind it. The retry adds no second EOF or close, and skips
  the close wait while its own flush is still short.
- A teardown send that drained the leftovers settles the flush; one
  that failed outranks it, since a reset leaves the buffer intact.
pull/1229/head
John Safranek 2026-09-01 17:23:02 -07:00 committed by philljj
parent 54610fd36e
commit ee7fa04bec
3 changed files with 515 additions and 29 deletions

View File

@ -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;
}

View File

@ -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)
{
@ -6148,20 +6162,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
@ -7575,6 +7575,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. */
@ -12838,6 +13281,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();

View File

@ -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,