Skip to content

Commit b8b4032

Browse files
ejohnstownphilljj
authored andcommitted
Stop answering traffic after a disconnect
DoPacket() skips the whole message dispatch once ssh->disconnected is set, for every message but a DISCONNECT. The handlers that answer must not -- a close draws an EOF and a close of ours, a request a success or failure, an open a confirmation, an unknown message an UNIMPLEMENTED -- and what the rest would record is of no use to a caller that can no longer send. RFC 4253 section 11.1. - Inbound data from here on is dropped rather than buffered, so the read path hands back only what arrived before the disconnect. ssh.h and internal.h say so, beside the calls and beside the flag. - A DISCONNECT still reaches DoDisconnect(), which sends nothing and is what latches WS_DISCONNECT; SendDisconnect() sets the flag too, so ours can be the one that raised it. - The frame advance steps over the whole packet, so the stream stays in step with no payload bookkeeping of its own. - wolfSSH_worker() gates on SendAfterDisconnect() the way wolfSSH_accept() and wolfSSH_connect() do. With the dispatch skipped there is no non-success left to return, so it would answer a healthy session for as long as the peer kept talking and the SFTP and SCP drive loops would keep pumping a dead one. The rekey test below the gate needs no disconnect of its own: the gate returns first, and a DISCONNECT arriving mid-pass leaves ret fatal. wolfSSH_shutdown() drops the channel on a disconnect before its own pump, so the gate does not cost it the read it does there. - tests/regress.c pins all four: no reply goes out, the stream stays in step with a disconnect queued behind a skipped close, late channel data is dropped rather than queued, and the worker reports the disconnect on the pass that takes it and on every pass behind it.
1 parent 2398507 commit b8b4032

5 files changed

Lines changed: 323 additions & 11 deletions

File tree

src/internal.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12225,7 +12225,17 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed)
1222512225
return WS_MSGID_NOT_ALLOWED_E;
1222612226
}
1222712227

12228-
switch (msg) {
12228+
/* The session is over, RFC 4253 section 11.1, so skip the whole dispatch:
12229+
* the handlers that answer must not, and what the rest would record is of
12230+
* no use to a caller that can no longer send. Inbound data from here on is
12231+
* dropped rather than buffered. The frame advance at the end steps over
12232+
* the packet, so the stream stays in step. A DISCONNECT still dispatches,
12233+
* since DoDisconnect() sends nothing and latches the error. */
12234+
if (ssh->disconnected && msg != MSGID_DISCONNECT) {
12235+
WLOG(WS_LOG_DEBUG, "Ignoring message ID %u after a disconnect",
12236+
(word32)msg);
12237+
}
12238+
else switch (msg) {
1222912239

1223012240
case MSGID_DISCONNECT:
1223112241
WLOG(WS_LOG_DEBUG, "Decoding MSGID_DISCONNECT");

src/ssh.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3604,6 +3604,16 @@ int wolfSSH_worker(WOLFSSH* ssh, word32* channelId)
36043604
if (ssh == NULL)
36053605
ret = WS_BAD_ARGUMENT;
36063606

3607+
/* Nothing left to drive: no reply may go out and inbound messages are
3608+
* skipped, so every pass from here on would answer WS_SUCCESS off a
3609+
* dispatch that did nothing and a caller turning the crank would never
3610+
* see the session end. What arrived before the disconnect is still the
3611+
* caller's, through the read calls. RFC 4253 section 11.1. */
3612+
if (ret == WS_SUCCESS && SendAfterDisconnect(ssh)) {
3613+
WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_worker(), session disconnected");
3614+
return WS_FATAL_ERROR;
3615+
}
3616+
36073617
#ifdef WOLFSSH_TEST_BLOCK
36083618
/* In forced non-blocking test mode, keep legacy ordering (send before
36093619
* receive) to match the harness expectations and avoid synthetic spins. */
@@ -3653,7 +3663,9 @@ int wolfSSH_worker(WOLFSSH* ssh, word32* channelId)
36533663
}
36543664

36553665
/* WS_EXTDATA is raised once, on arrival; masking it would strand the
3656-
* buffered stderr and its window credit. */
3666+
* buffered stderr and its window credit. A disconnect cannot be seen
3667+
* here: the gate at the top returns before this, and the DISCONNECT
3668+
* that sets the flag mid-pass leaves ret fatal. */
36573669
if (ssh->isKeying && ret != WS_EXTDATA) {
36583670
ssh->error = WS_REKEYING;
36593671
return WS_REKEYING;

tests/regress.c

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,31 @@ static WS_MAYBE_UNUSED word32 BuildExtInfoSigAlgs(byte* buf, word32 bufSz,
265265
return AppendString(buf, bufSz, idx, sigAlgs);
266266
}
267267

268+
static word32 BuildChannelClosePacket(word32 peerChannelId, byte* out,
269+
word32 outSz)
270+
{
271+
byte payload[16];
272+
word32 idx = 0;
273+
274+
idx = AppendUint32(payload, sizeof(payload), idx, peerChannelId);
275+
276+
return WrapPacket(MSGID_CHANNEL_CLOSE, payload, idx, out, outSz);
277+
}
278+
279+
280+
static word32 BuildChannelDataPacket(word32 peerChannelId, const char* data,
281+
byte* out, word32 outSz)
282+
{
283+
byte payload[64];
284+
word32 idx = 0;
285+
286+
idx = AppendUint32(payload, sizeof(payload), idx, peerChannelId);
287+
idx = AppendString(payload, sizeof(payload), idx, data);
288+
289+
return WrapPacket(MSGID_CHANNEL_DATA, payload, idx, out, outSz);
290+
}
291+
292+
268293
#ifdef WOLFSSH_FWD
269294
static word32 BuildDirectTcpipExtra(const char* host, word32 hostPort,
270295
const char* origin, word32 originPort, byte* out, word32 outSz)
@@ -4194,6 +4219,68 @@ static void TestDisconnectOutranksRekey(void)
41944219
}
41954220

41964221

4222+
/* wolfSSH_worker() is the other drive loop, and the one the SFTP and SCP
4223+
* layers turn. Once the session is over it has nothing to drive: the
4224+
* dispatch is skipped, so a post-disconnect message would leave
4225+
* ssh->error at WS_SUCCESS and the worker would keep reporting a healthy
4226+
* session for as long as the peer talks. It also outranks a rekey the peer
4227+
* abandoned, which only NEWKEYS could clear. RFC 4253 section 11.1. */
4228+
static void TestWorkerReportsDisconnect(void)
4229+
{
4230+
WOLFSSH_CTX* ctx;
4231+
WOLFSSH* ssh;
4232+
MemIo io;
4233+
byte in[256];
4234+
byte out[256];
4235+
word32 inSz;
4236+
word32 idx;
4237+
4238+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
4239+
AssertNotNull(ctx);
4240+
4241+
wolfSSH_SetIORecv(ctx, MemRecv);
4242+
wolfSSH_SetIOSend(ctx, MemSend);
4243+
4244+
ssh = wolfSSH_new(ctx);
4245+
AssertNotNull(ssh);
4246+
AddSessionChannel(ssh);
4247+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
4248+
4249+
/* The peer's KEXINIT, the way DoKexInit records it. Nothing clears it
4250+
* after the disconnect below, so it latches for the session. */
4251+
ssh->isKeying |= WOLFSSH_PEER_IS_KEYING;
4252+
4253+
/* The peer's disconnect, then a message behind it. An IGNORE draws no
4254+
* reply of its own, so what goes out can only come from the worker. */
4255+
idx = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION,
4256+
in, sizeof(in));
4257+
inSz = idx + BuildPacket(MSGID_IGNORE, in + idx, sizeof(in) - idx);
4258+
MemIoInit(&io, in, inSz, out, sizeof(out));
4259+
wolfSSH_SetIOReadCtx(ssh, &io);
4260+
wolfSSH_SetIOWriteCtx(ssh, &io);
4261+
4262+
AssertIntEQ(wolfSSH_worker(ssh, NULL), WS_FATAL_ERROR);
4263+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4264+
AssertTrue(ssh->disconnected);
4265+
AssertTrue(ssh->isKeying != 0);
4266+
io.outSz = 0;
4267+
4268+
/* The message behind it is still queued, and every further pass reports
4269+
* the disconnect rather than the WS_SUCCESS of a skipped dispatch or the
4270+
* WS_REKEYING of a rekey that cannot finish. */
4271+
AssertIntEQ(wolfSSH_worker(ssh, NULL), WS_FATAL_ERROR);
4272+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4273+
AssertIntEQ(wolfSSH_worker(ssh, NULL), WS_FATAL_ERROR);
4274+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4275+
4276+
/* Nothing went out on any of them. */
4277+
AssertIntEQ(io.outSz, 0);
4278+
4279+
wolfSSH_free(ssh);
4280+
wolfSSH_CTX_free(ctx);
4281+
}
4282+
4283+
41974284

41984285
#ifndef NO_WOLFSSH_SERVER
41994286

@@ -4410,6 +4497,198 @@ static void TestDisconnectGatesConnect(void)
44104497
#endif /* !NO_WOLFSSH_CLIENT */
44114498

44124499

4500+
/* The public senders sit behind the disconnect gate, but the replies the
4501+
* library builds in answer to inbound traffic did not. A channel close
4502+
* draws an EOF and a close of ours out of DoChannelClose(), and a channel
4503+
* open a confirmation or a failure out of DoChannelOpen(); on a session
4504+
* that is already over, none of that may reach the peer. RFC 4253
4505+
* section 11.1. */
4506+
static void TestDisconnectSilencesInboundReplies(void)
4507+
{
4508+
WOLFSSH_CTX* ctx;
4509+
WOLFSSH* ssh;
4510+
WOLFSSH_CHANNEL* channel;
4511+
MemIo io;
4512+
byte in[256];
4513+
byte out[512];
4514+
word32 inSz;
4515+
word32 quietSz;
4516+
word32 channelId;
4517+
4518+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
4519+
AssertNotNull(ctx);
4520+
4521+
wolfSSH_SetIORecv(ctx, MemRecv);
4522+
wolfSSH_SetIOSend(ctx, MemSend);
4523+
4524+
ssh = wolfSSH_new(ctx);
4525+
AssertNotNull(ssh);
4526+
AddSessionChannel(ssh);
4527+
channel = ssh->channelList;
4528+
channelId = channel->channel;
4529+
/* Past userauth, or the message filter turns the inbound messages away
4530+
* on its own and the wire check below proves nothing. */
4531+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
4532+
4533+
inSz = BuildChannelClosePacket(channelId, in, sizeof(in));
4534+
MemIoInit(&io, in, inSz, out, sizeof(out));
4535+
wolfSSH_SetIOReadCtx(ssh, &io);
4536+
wolfSSH_SetIOWriteCtx(ssh, &io);
4537+
4538+
/* Our own disconnect goes out first, and is the last thing that may. */
4539+
AssertIntEQ(wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION),
4540+
WS_SUCCESS);
4541+
AssertTrue(ssh->disconnected);
4542+
quietSz = io.outSz;
4543+
AssertTrue(quietSz > 0);
4544+
4545+
/* The peer's close arrives anyway: a caller's own wolfSSH_worker() loop
4546+
* still reads after a disconnect, and shutdown's pump can find packets
4547+
* queued behind the peer's DISCONNECT. A skipped packet
4548+
* is not an error; assert that, so the quiet-wire checks below cannot
4549+
* pass on a receive that never reached DoPacket(). */
4550+
AssertIntEQ(DoReceive(ssh), WS_SUCCESS);
4551+
4552+
/* No EOF and no close went out, and the handler never ran, so the
4553+
* channel it would have torn down is still on the list. */
4554+
AssertIntEQ(io.outSz, quietSz);
4555+
AssertNotNull(ssh->channelList);
4556+
AssertIntEQ(ssh->channelList->channel, channelId);
4557+
AssertFalse(channel->eofTxd);
4558+
AssertFalse(channel->closeTxd);
4559+
4560+
/* A channel open is the other half: it answers with a confirmation or
4561+
* a failure, and neither may go out now. */
4562+
inSz = BuildChannelOpenPacket("session", 99, 1024, 1024, NULL, 0,
4563+
in, sizeof(in));
4564+
MemIoInit(&io, in, inSz, out, sizeof(out));
4565+
io.outSz = quietSz;
4566+
wolfSSH_SetIOReadCtx(ssh, &io);
4567+
wolfSSH_SetIOWriteCtx(ssh, &io);
4568+
4569+
AssertIntEQ(DoReceive(ssh), WS_SUCCESS);
4570+
AssertIntEQ(io.outSz, quietSz);
4571+
4572+
wolfSSH_free(ssh);
4573+
wolfSSH_CTX_free(ctx);
4574+
}
4575+
4576+
4577+
/* Skipping the dispatch is all the gate does: the frame advance steps over
4578+
* the whole packet on its own, so a packet behind a skipped one is still
4579+
* found where it should be. And a DISCONNECT is not skipped -- DoDisconnect()
4580+
* sends nothing, and it is what latches WS_DISCONNECT for the caller, so
4581+
* swallowing the peer's would report a live session on a dead one. */
4582+
static void TestDisconnectKeepsStreamInStep(void)
4583+
{
4584+
WOLFSSH_CTX* ctx;
4585+
WOLFSSH* ssh;
4586+
WOLFSSH_CHANNEL* channel;
4587+
MemIo io;
4588+
byte in[256];
4589+
byte out[512];
4590+
word32 inSz;
4591+
word32 quietSz;
4592+
word32 channelId;
4593+
4594+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
4595+
AssertNotNull(ctx);
4596+
4597+
wolfSSH_SetIORecv(ctx, MemRecv);
4598+
wolfSSH_SetIOSend(ctx, MemSend);
4599+
4600+
ssh = wolfSSH_new(ctx);
4601+
AssertNotNull(ssh);
4602+
AddSessionChannel(ssh);
4603+
channel = ssh->channelList;
4604+
channelId = channel->channel;
4605+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
4606+
4607+
/* A channel close for the gate to skip, with the peer's disconnect behind
4608+
* it in the same read. */
4609+
inSz = BuildChannelClosePacket(channelId, in, sizeof(in));
4610+
inSz += BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION,
4611+
in + inSz, (word32)sizeof(in) - inSz);
4612+
MemIoInit(&io, in, inSz, out, sizeof(out));
4613+
wolfSSH_SetIOReadCtx(ssh, &io);
4614+
wolfSSH_SetIOWriteCtx(ssh, &io);
4615+
4616+
/* Ours goes out first, and is the last thing that may. */
4617+
AssertIntEQ(wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION),
4618+
WS_SUCCESS);
4619+
AssertTrue(ssh->disconnected);
4620+
quietSz = io.outSz;
4621+
AssertTrue(quietSz > 0);
4622+
4623+
/* The close is skipped, so no reply and the channel stays. */
4624+
AssertIntEQ(DoReceive(ssh), WS_SUCCESS);
4625+
AssertIntEQ(io.outSz, quietSz);
4626+
AssertNotNull(ssh->channelList);
4627+
AssertIntEQ(ssh->channelList->channel, channelId);
4628+
AssertFalse(channel->eofTxd);
4629+
AssertFalse(channel->closeTxd);
4630+
4631+
/* The disconnect behind it decodes and latches, and still answers
4632+
* nothing. */
4633+
AssertIntEQ(DoReceive(ssh), WS_FATAL_ERROR);
4634+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4635+
AssertIntEQ(io.outSz, quietSz);
4636+
4637+
wolfSSH_free(ssh);
4638+
wolfSSH_CTX_free(ctx);
4639+
}
4640+
4641+
4642+
/* Skipping the dispatch drops what arrives, it does not queue it: the gate
4643+
* covers inbound data, not only replies. A caller that can no longer send has
4644+
* nothing to do with it. Pinned because the read path still hands back data
4645+
* that arrived before the disconnect, and the two are easy to confuse. */
4646+
static void TestDisconnectDropsLateChannelData(void)
4647+
{
4648+
WOLFSSH_CTX* ctx;
4649+
WOLFSSH* ssh;
4650+
MemIo io;
4651+
byte in[256];
4652+
byte out[512];
4653+
byte buf[32];
4654+
word32 inSz;
4655+
word32 channelId;
4656+
4657+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
4658+
AssertNotNull(ctx);
4659+
4660+
wolfSSH_SetIORecv(ctx, MemRecv);
4661+
wolfSSH_SetIOSend(ctx, MemSend);
4662+
4663+
ssh = wolfSSH_new(ctx);
4664+
AssertNotNull(ssh);
4665+
AddSessionChannel(ssh);
4666+
channelId = ssh->channelList->channel;
4667+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
4668+
4669+
inSz = BuildChannelDataPacket(channelId, "late", in, sizeof(in));
4670+
MemIoInit(&io, in, inSz, out, sizeof(out));
4671+
wolfSSH_SetIOReadCtx(ssh, &io);
4672+
wolfSSH_SetIOWriteCtx(ssh, &io);
4673+
4674+
AssertIntEQ(wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION),
4675+
WS_SUCCESS);
4676+
AssertTrue(ssh->disconnected);
4677+
4678+
/* Read it, and it is gone: nothing buffered on the channel. */
4679+
AssertIntEQ(DoReceive(ssh), WS_SUCCESS);
4680+
AssertNotNull(ssh->channelList);
4681+
AssertIntEQ(ssh->channelList->inputBuffer.length
4682+
- ssh->channelList->inputBuffer.idx, 0);
4683+
4684+
/* So the read reports the dead session rather than the dropped bytes. */
4685+
AssertIntEQ(wolfSSH_stream_read(ssh, buf, sizeof(buf)), WS_FATAL_ERROR);
4686+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4687+
4688+
wolfSSH_free(ssh);
4689+
wolfSSH_CTX_free(ctx);
4690+
}
4691+
44134692
/* disconnectTxd means "a flush is owed", not "a disconnect was sent". Once
44144693
* ours has gone out, a teardown call must not push whatever the internal
44154694
* senders queued behind it. */
@@ -8301,6 +8580,9 @@ int main(int argc, char** argv)
83018580
#ifndef NO_WOLFSSH_CLIENT
83028581
TestDisconnectGatesConnect();
83038582
#endif
8583+
TestDisconnectSilencesInboundReplies();
8584+
TestDisconnectKeepsStreamInStep();
8585+
TestDisconnectDropsLateChannelData();
83048586
TestDisconnectQuietWindowAdjust();
83058587
TestDisconnectBlocksChannelAndFwdSends();
83068588
TestStreamExitReportsDisconnect();
@@ -8315,6 +8597,7 @@ int main(int argc, char** argv)
83158597
TestShutdownKeepsFlushWantWrite();
83168598
TestDisconnectTxdClearsOnFlush();
83178599
TestDisconnectOutranksRekey();
8600+
TestWorkerReportsDisconnect();
83188601
#if defined(WOLFSSH_TERM) && !defined(NO_FILESYSTEM)
83198602
TestTerminalResizeBlockedAfterDisconnect();
83208603
#endif

wolfssh/internal.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1106,7 +1106,11 @@ struct WOLFSSH {
11061106
* calls, so nothing more goes out. Reads still hand back what arrived
11071107
* before the disconnect; the head-of-list reads report it once their
11081108
* buffer runs dry, unless a CHANNEL_EOF arrived first.
1109-
* wolfSSH_worker() is not gated; the shutdown paths pump the worker. */
1109+
* wolfSSH_worker() reports it as well, so a drive loop stops turning;
1110+
* wolfSSH_shutdown() drops the channel first and so never pumps it.
1111+
* DoPacket() skips the inbound dispatch too, for every message but a
1112+
* DISCONNECT, so nothing arriving afterward is buffered, answered or
1113+
* reported through the channel callbacks. */
11101114
byte disconnected;
11111115
/* Set once SendDisconnect() has bundled our own DISCONNECT into the
11121116
* output buffer, and cleared once wolfSSH_SendPacket() drains it, so it

0 commit comments

Comments
 (0)