Skip to content

Commit 5c6cdbf

Browse files
committed
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. - 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, and the retry adds no second EOF or close.
1 parent 9cedb5a commit 5c6cdbf

3 files changed

Lines changed: 245 additions & 11 deletions

File tree

src/ssh.c

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,17 +1149,20 @@ static int SendAfterDisconnect(WOLFSSH* ssh)
11491149
}
11501150

11511151

1152-
/* A disconnect of ours left queued by a short send still has to reach the
1152+
/* Whatever a short send left in the output buffer still has to reach the
11531153
* peer, and flushing bytes that are already bundled is not the new traffic
1154-
* RFC 4253 section 11.1 forbids. Only our own disconnect qualifies: a
1155-
* disconnect from the peer leaves nothing queued but unrelated traffic,
1156-
* which the session is over for. Call only after a NULL check of ssh. */
1157-
static int FlushQueuedDisconnect(WOLFSSH* ssh)
1154+
* RFC 4253 section 11.1 forbids. Once the peer has disconnected, though,
1155+
* only our own queued disconnect still qualifies: anything else in there
1156+
* belongs to a session that is over. Call only after a NULL check of ssh. */
1157+
static int FlushQueuedOutput(WOLFSSH* ssh)
11581158
{
1159-
if (!ssh->disconnectTxd || !wolfSSH_OutputPending(ssh))
1159+
if (!wolfSSH_OutputPending(ssh))
11601160
return 0;
11611161

1162-
WLOG(WS_LOG_DEBUG, "Flushing a disconnect left queued by a short send");
1162+
if (ssh->disconnected && !ssh->disconnectTxd)
1163+
return 0;
1164+
1165+
WLOG(WS_LOG_DEBUG, "Flushing output left queued by a short send");
11631166
return 1;
11641167
}
11651168

@@ -1175,9 +1178,11 @@ int wolfSSH_shutdown(WOLFSSH* ssh)
11751178
if (ssh == NULL)
11761179
ret = WS_BAD_ARGUMENT;
11771180

1178-
/* This is a teardown call, so a disconnect of ours left queued by a
1179-
* short send goes out here, with or without a channel to tear down. */
1180-
if (ret == WS_SUCCESS && FlushQueuedDisconnect(ssh))
1181+
/* This is a teardown call, so anything a short send left queued goes out
1182+
* here, with or without a channel to tear down. A rejected auth's
1183+
* USERAUTH_FAILURE has no channel, and a channel close is retired off
1184+
* the channel the moment it is bundled. */
1185+
if (ret == WS_SUCCESS && FlushQueuedOutput(ssh))
11811186
flushRet = wolfSSH_SendPacket(ssh);
11821187

11831188
if (ret == WS_SUCCESS && ssh->channelList == NULL)
@@ -1760,7 +1765,7 @@ int wolfSSH_SendDisconnect(WOLFSSH *ssh, word32 reason)
17601765
* connection that is already over. A short send leaves the first one
17611766
* queued, though, so that retry goes through. */
17621767
if (SendAfterDisconnect(ssh)) {
1763-
if (FlushQueuedDisconnect(ssh))
1768+
if (FlushQueuedOutput(ssh))
17641769
return wolfSSH_SendPacket(ssh);
17651770
return WS_FATAL_ERROR;
17661771
}

tests/regress.c

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6308,6 +6308,222 @@ static void TestShutdownFlushesWithNoChannel(void)
63086308
}
63096309

63106310

6311+
/* Two packets can share one flush, so step over the first to reach the
6312+
* second: a length prefix and the bytes it counts. */
6313+
static word32 NextPacketOffset(const byte* pkt, word32 sz)
6314+
{
6315+
word32 packetLen;
6316+
6317+
AssertTrue(sz >= LENGTH_SZ);
6318+
packetLen = ReadUint32(pkt);
6319+
AssertTrue(sz - LENGTH_SZ >= packetLen);
6320+
6321+
return packetLen + LENGTH_SZ;
6322+
}
6323+
6324+
6325+
#ifndef NO_WOLFSSH_SERVER
6326+
6327+
static int RejectUserAuthCb(byte authType, WS_UserAuthData* authData,
6328+
void* ctx)
6329+
{
6330+
WOLFSSH_UNUSED(authType);
6331+
WOLFSSH_UNUSED(authData);
6332+
WOLFSSH_UNUSED(ctx);
6333+
6334+
return WOLFSSH_USERAUTH_REJECTED;
6335+
}
6336+
6337+
6338+
/* A rejected authentication owes the peer a USERAUTH_FAILURE, RFC 4252
6339+
* section 5.1. Below the auth-failure cap nothing queues a disconnect to
6340+
* drag the buffer out, and authentication never completed so there is no
6341+
* channel: teardown is the only thing left that can push the reply. */
6342+
static void TestShutdownFlushesQueuedUserAuthFailure(void)
6343+
{
6344+
ChannelOpenHarness harness;
6345+
byte in[128];
6346+
word32 inSz;
6347+
6348+
inSz = BuildUserAuthPasswordRequest("alice", "pw", in, sizeof(in));
6349+
6350+
InitUserAuthHarness(&harness, in, inSz);
6351+
wolfSSH_SetUserAuth(harness.ctx, RejectUserAuthCb);
6352+
wolfSSH_SetIOSend(harness.ctx, MemSendWantWrite);
6353+
6354+
/* The failure is bundled, then the socket refuses it. */
6355+
MemSendWantWriteCount = 1;
6356+
AssertIntEQ(DoReceive(harness.ssh), WS_FATAL_ERROR);
6357+
AssertIntEQ(harness.ssh->error, WS_USER_AUTH_E);
6358+
AssertIntEQ(harness.io.outSz, 0);
6359+
AssertTrue(wolfSSH_OutputPending(harness.ssh));
6360+
6361+
/* Neither of the two things that would have flushed it anyway. */
6362+
AssertFalse(harness.ssh->disconnectTxd);
6363+
AssertNull(harness.ssh->channelList);
6364+
6365+
/* The socket takes bytes now, so only the gate stands between the
6366+
* queued failure and the peer. */
6367+
AssertIntEQ(wolfSSH_shutdown(harness.ssh), WS_CHANNEL_CLOSED);
6368+
AssertFalse(wolfSSH_OutputPending(harness.ssh));
6369+
6370+
/* The failure, and nothing bundled behind it. */
6371+
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz),
6372+
MSGID_USERAUTH_FAILURE);
6373+
AssertIntEQ(harness.io.outSz,
6374+
NextPacketOffset(harness.io.out, harness.io.outSz));
6375+
6376+
FreeChannelOpenHarness(&harness);
6377+
}
6378+
6379+
#endif /* !NO_WOLFSSH_SERVER */
6380+
6381+
6382+
/* DoChannelClose() retires the channel once the close is bundled, so a close
6383+
* the socket refused is left with no channel to carry the retry. Teardown
6384+
* still owes the peer that close, RFC 4254 section 5.3. */
6385+
static void TestShutdownFlushesQueuedChannelClose(void)
6386+
{
6387+
WOLFSSH_CTX* ctx;
6388+
WOLFSSH* ssh;
6389+
MemIo io;
6390+
byte in[128];
6391+
byte out[512];
6392+
word32 inSz;
6393+
word32 channelId;
6394+
word32 closeOff;
6395+
6396+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
6397+
AssertNotNull(ctx);
6398+
6399+
wolfSSH_SetIORecv(ctx, MemRecv);
6400+
wolfSSH_SetIOSend(ctx, MemSendWantWrite);
6401+
6402+
ssh = wolfSSH_new(ctx);
6403+
AssertNotNull(ssh);
6404+
AddSessionChannel(ssh);
6405+
channelId = ssh->channelList->channel;
6406+
/* Past userauth, or the message filter turns the close away and the
6407+
* wire checks below prove nothing. */
6408+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
6409+
6410+
inSz = BuildChannelClosePacket(channelId, in, sizeof(in));
6411+
MemIoInit(&io, in, inSz, out, sizeof(out));
6412+
wolfSSH_SetIOReadCtx(ssh, &io);
6413+
wolfSSH_SetIOWriteCtx(ssh, &io);
6414+
6415+
/* Our EOF and close answer the peer's close. Refuse both sends, or the
6416+
* second one carries the first out with it. */
6417+
MemSendWantWriteCount = 2;
6418+
AssertIntEQ(DoReceive(ssh), WS_CHANNEL_CLOSED);
6419+
AssertIntEQ(io.outSz, 0);
6420+
AssertTrue(wolfSSH_OutputPending(ssh));
6421+
6422+
/* The channel that would have retried the send is already gone. */
6423+
AssertNull(ssh->channelList);
6424+
AssertFalse(ssh->disconnectTxd);
6425+
6426+
AssertIntEQ(wolfSSH_shutdown(ssh), WS_CHANNEL_CLOSED);
6427+
AssertFalse(wolfSSH_OutputPending(ssh));
6428+
6429+
/* Both halves reached the peer, EOF ahead of the close, and nothing
6430+
* else: the two packets account for every byte written. */
6431+
AssertIntEQ(ParseMsgId(out, io.outSz), MSGID_CHANNEL_EOF);
6432+
closeOff = NextPacketOffset(out, io.outSz);
6433+
AssertIntEQ(ParseMsgId(out + closeOff, io.outSz - closeOff),
6434+
MSGID_CHANNEL_CLOSE);
6435+
AssertIntEQ(io.outSz,
6436+
closeOff + NextPacketOffset(out + closeOff, io.outSz - closeOff));
6437+
6438+
wolfSSH_free(ssh);
6439+
wolfSSH_CTX_free(ctx);
6440+
}
6441+
6442+
6443+
/* The flush is no longer tied to a disconnect, so it can fire on a live
6444+
* session with the channel still listed and then short-send itself. The
6445+
* teardown sends bundle in behind bytes the socket has not taken, and the
6446+
* caller has to come back: the retry owes the peer the rest of the buffer
6447+
* and not a second copy of the EOF and close. */
6448+
static void TestShutdownFlushShortSendsWithChannel(void)
6449+
{
6450+
WOLFSSH_CTX* ctx;
6451+
WOLFSSH* ssh;
6452+
WOLFSSH_CHANNEL* channel;
6453+
MemIo io;
6454+
byte out[1024];
6455+
byte data[32];
6456+
word32 eofOff;
6457+
word32 reqOff;
6458+
word32 closeOff;
6459+
6460+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
6461+
AssertNotNull(ctx);
6462+
6463+
wolfSSH_SetIORecv(ctx, MemRecv);
6464+
wolfSSH_SetIOSend(ctx, MemSendWantWrite);
6465+
6466+
ssh = wolfSSH_new(ctx);
6467+
AssertNotNull(ssh);
6468+
AddSessionChannel(ssh);
6469+
channel = ssh->channelList;
6470+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
6471+
6472+
MemIoInit(&io, NULL, 0, out, sizeof(out));
6473+
wolfSSH_SetIOReadCtx(ssh, &io);
6474+
wolfSSH_SetIOWriteCtx(ssh, &io);
6475+
6476+
/* Channel data the socket will not take, on a session that is neither
6477+
* disconnected nor out of channels. */
6478+
WMEMSET(data, 'x', sizeof(data));
6479+
MemSendWantWriteCount = 5;
6480+
AssertIntEQ(wolfSSH_stream_send(ssh, data, sizeof(data)),
6481+
(int)sizeof(data));
6482+
AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_WRITE);
6483+
AssertTrue(wolfSSH_OutputPending(ssh));
6484+
AssertFalse(ssh->disconnected);
6485+
AssertNotNull(ssh->channelList);
6486+
6487+
/* The flush and all three teardown sends are refused, so the unfinished
6488+
* flush owns the return and the whole teardown is still queued. */
6489+
AssertIntEQ(wolfSSH_shutdown(ssh), WS_WANT_WRITE);
6490+
AssertIntEQ(io.outSz, 0);
6491+
AssertTrue(wolfSSH_OutputPending(ssh));
6492+
AssertIntEQ(MemSendWantWriteCount, 0);
6493+
6494+
/* Bundled counts as sent, so the retry must not emit them again. The
6495+
* list comes first: a channel retired here would be freed, and the
6496+
* reads below it would be of freed memory. */
6497+
AssertNotNull(ssh->channelList);
6498+
AssertTrue(channel->eofTxd);
6499+
AssertTrue(channel->closeTxd);
6500+
6501+
/* The socket takes bytes now. The retry drains what was queued and
6502+
* lands on the wait for the peer's close, not on a stale want-write
6503+
* for a buffer that is already empty. */
6504+
AssertIntEQ(wolfSSH_shutdown(ssh), WS_FATAL_ERROR);
6505+
AssertIntEQ(wolfSSH_get_error(ssh), WS_WANT_READ);
6506+
AssertFalse(wolfSSH_OutputPending(ssh));
6507+
6508+
/* One copy of each, in order, accounting for every byte written. */
6509+
AssertIntEQ(ParseMsgId(out, io.outSz), MSGID_CHANNEL_DATA);
6510+
eofOff = NextPacketOffset(out, io.outSz);
6511+
AssertIntEQ(ParseMsgId(out + eofOff, io.outSz - eofOff),
6512+
MSGID_CHANNEL_EOF);
6513+
reqOff = eofOff + NextPacketOffset(out + eofOff, io.outSz - eofOff);
6514+
AssertIntEQ(ParseMsgId(out + reqOff, io.outSz - reqOff),
6515+
MSGID_CHANNEL_REQUEST);
6516+
closeOff = reqOff + NextPacketOffset(out + reqOff, io.outSz - reqOff);
6517+
AssertIntEQ(ParseMsgId(out + closeOff, io.outSz - closeOff),
6518+
MSGID_CHANNEL_CLOSE);
6519+
AssertIntEQ(io.outSz,
6520+
closeOff + NextPacketOffset(out + closeOff, io.outSz - closeOff));
6521+
6522+
wolfSSH_free(ssh);
6523+
wolfSSH_CTX_free(ctx);
6524+
}
6525+
6526+
63116527
/* The highwater callback is the application's, and its return propagates out
63126528
* of wolfSSH_SendPacket(). A mark firing on the disconnect packet must not
63136529
* fail a send that went out fine, whoever owns the callback. */
@@ -10966,6 +11182,11 @@ int main(int argc, char** argv)
1096611182
TestPeerDisconnectKeepsTrafficQueued(0);
1096711183
TestPeerDisconnectKeepsTrafficQueued(1);
1096811184
TestShutdownFlushesWithNoChannel();
11185+
#ifndef NO_WOLFSSH_SERVER
11186+
TestShutdownFlushesQueuedUserAuthFailure();
11187+
#endif
11188+
TestShutdownFlushesQueuedChannelClose();
11189+
TestShutdownFlushShortSendsWithChannel();
1096911190
TestQueuedDisconnectFlushes();
1097011191
TestShutdownFlushesQueuedDisconnect();
1097111192
TestShutdownKeepsFlushWantWrite();

wolfssh/ssh.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,14 @@ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx,
688688

689689
WOLFSSH_API int wolfSSH_accept(WOLFSSH* ssh);
690690
WOLFSSH_API int wolfSSH_connect(WOLFSSH* ssh);
691+
/* Tears down the first channel in the list, and flushes whatever a short
692+
* send left queued, not just a disconnect of ours: a rejected auth's
693+
* USERAUTH_FAILURE, and a CHANNEL_CLOSE whose channel was retired the
694+
* moment it was bundled, have nothing else left to carry the retry. That
695+
* flush can be short too, so a WS_WANT_WRITE from here may be owed to it
696+
* rather than to the teardown sends; either way the caller retries. Once
697+
* the peer has disconnected, only our own queued disconnect still goes
698+
* out, per the comment below. */
691699
WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh);
692700
/* A disconnect, sent or received, ends the session. Nothing more goes out:
693701
* wolfSSH_shutdown() above this comment, and every send call below it,

0 commit comments

Comments
 (0)