Skip to content

Commit 2398507

Browse files
ejohnstownphilljj
authored andcommitted
Gate the handshake drivers on a disconnect
wolfSSH_accept() and wolfSSH_connect() drive the handshake only while the session is live. Both gate on SendAfterDisconnect() ahead of the pending-send block, which would otherwise flush a queued disconnect and count it as the next handshake message; the shutdown paths own that flush. - The prototype sits ahead of both drivers, since either can be the only one built. - TestDisconnectGatesAccept() and TestDisconnectGatesConnect() cover a local disconnect, one from the peer, and a queued short send. One test per endpoint, so a single-sided build keeps the coverage that applies to it. - A received disconnect used to reach the error-state test in wolfSSH_accept() and report WS_INVALID_STATE_E; the gate answers WS_FATAL_ERROR first, and both tests pin that. - The contract comments in ssh.h and internal.h drop the ungated note.
1 parent 9731cfe commit 2398507

4 files changed

Lines changed: 242 additions & 6 deletions

File tree

src/ssh.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,10 @@ int wolfSSH_CTX_UseTpmHostKey(WOLFSSH_CTX* ctx,
550550
#endif /* WOLFSSH_TPM */
551551

552552

553+
/* Defined below, ahead of both drivers; either can be the only one built. */
554+
static int SendAfterDisconnect(WOLFSSH* ssh);
555+
556+
553557
#ifndef NO_WOLFSSH_SERVER
554558

555559
const char acceptError[] = "accept error: %s, %d";
@@ -563,6 +567,11 @@ int wolfSSH_accept(WOLFSSH* ssh)
563567
if (ssh == NULL)
564568
return WS_BAD_ARGUMENT;
565569

570+
/* No handshake on a session that is over. The pending-send block below
571+
* would flush a queued disconnect as the next handshake message. */
572+
if (SendAfterDisconnect(ssh))
573+
return WS_FATAL_ERROR;
574+
566575
/* clear want read/writes for retry */
567576
if (ssh->error == WS_WANT_READ || ssh->error == WS_WANT_WRITE || ssh->error == WS_AUTH_PENDING)
568577
ssh->error = 0;
@@ -826,6 +835,11 @@ int wolfSSH_connect(WOLFSSH* ssh)
826835
if (ssh == NULL)
827836
return WS_BAD_ARGUMENT;
828837

838+
/* See wolfSSH_accept(). No error-state test here, so the peer's
839+
* disconnect reaches the state machine like a local one. */
840+
if (SendAfterDisconnect(ssh))
841+
return WS_FATAL_ERROR;
842+
829843
/* check if data pending to be sent */
830844
if (ssh->outputBuffer.length > 0 &&
831845
ssh->connectState < CONNECT_SERVER_CHANNEL_REQUEST_DONE) {

tests/regress.c

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4194,6 +4194,222 @@ static void TestDisconnectOutranksRekey(void)
41944194
}
41954195

41964196

4197+
4198+
#ifndef NO_WOLFSSH_SERVER
4199+
4200+
/* wolfSSH_accept() drives the handshake, so a session that is already over
4201+
* must stop it the way it stops every other sender. The short-send case is
4202+
* the sharp one: the pending-send block at the top would push out a
4203+
* disconnect left queued by a short send and then count it as the handshake
4204+
* message the state machine was waiting for. RFC 4253 section 11.1. */
4205+
static void TestDisconnectGatesAccept(void)
4206+
{
4207+
WOLFSSH_CTX* ctx;
4208+
WOLFSSH* ssh;
4209+
MemIo io;
4210+
byte in[128];
4211+
byte out[512];
4212+
word32 inSz;
4213+
word32 quietSz;
4214+
byte state;
4215+
int ret;
4216+
4217+
/* A local disconnect leaves ssh->error clear, so the "in error state"
4218+
* test in wolfSSH_accept() never sees it. */
4219+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
4220+
AssertNotNull(ctx);
4221+
wolfSSH_SetIORecv(ctx, MemRecv);
4222+
wolfSSH_SetIOSend(ctx, MemSend);
4223+
4224+
ssh = wolfSSH_new(ctx);
4225+
AssertNotNull(ssh);
4226+
AddSessionChannel(ssh);
4227+
/* Not one of the states wolfSSH_accept() holds back, so an unwanted
4228+
* advance shows up in the assertions below. */
4229+
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
4230+
4231+
MemIoInit(&io, NULL, 0, out, sizeof(out));
4232+
wolfSSH_SetIOReadCtx(ssh, &io);
4233+
wolfSSH_SetIOWriteCtx(ssh, &io);
4234+
4235+
AssertIntEQ(wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION),
4236+
WS_SUCCESS);
4237+
AssertTrue(ssh->disconnected);
4238+
AssertIntEQ(wolfSSH_get_error(ssh), 0);
4239+
quietSz = io.outSz;
4240+
state = ssh->acceptState;
4241+
4242+
ret = wolfSSH_accept(ssh);
4243+
AssertIntEQ(ret, WS_FATAL_ERROR);
4244+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4245+
/* No handshake packet, and the state machine did not move. */
4246+
AssertIntEQ(io.outSz, quietSz);
4247+
AssertIntEQ(ssh->acceptState, state);
4248+
4249+
wolfSSH_free(ssh);
4250+
wolfSSH_CTX_free(ctx);
4251+
4252+
/* Our disconnect short-sends, so it is sitting in the output buffer
4253+
* with a flush owed. wolfSSH_shutdown() and wolfSSH_SendDisconnect()
4254+
* own that flush; wolfSSH_accept() must leave it alone. */
4255+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
4256+
AssertNotNull(ctx);
4257+
wolfSSH_SetIORecv(ctx, MemRecv);
4258+
wolfSSH_SetIOSend(ctx, MemSendWantWrite);
4259+
4260+
ssh = wolfSSH_new(ctx);
4261+
AssertNotNull(ssh);
4262+
AddSessionChannel(ssh);
4263+
/* Not one of the states wolfSSH_accept() holds back, so an unwanted
4264+
* advance shows up in the assertions below. */
4265+
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
4266+
4267+
MemIoInit(&io, NULL, 0, out, sizeof(out));
4268+
wolfSSH_SetIOReadCtx(ssh, &io);
4269+
wolfSSH_SetIOWriteCtx(ssh, &io);
4270+
4271+
MemSendWantWriteCount = 1;
4272+
AssertIntEQ(wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION),
4273+
WS_WANT_WRITE);
4274+
AssertTrue(ssh->disconnected);
4275+
AssertTrue(ssh->disconnectTxd);
4276+
AssertTrue(wolfSSH_OutputPending(ssh));
4277+
AssertIntEQ(io.outSz, 0);
4278+
state = ssh->acceptState;
4279+
4280+
ret = wolfSSH_accept(ssh);
4281+
AssertIntEQ(ret, WS_FATAL_ERROR);
4282+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4283+
/* Still queued, and not mistaken for the awaited handshake message. */
4284+
AssertIntEQ(io.outSz, 0);
4285+
AssertTrue(wolfSSH_OutputPending(ssh));
4286+
AssertIntEQ(ssh->acceptState, state);
4287+
4288+
wolfSSH_free(ssh);
4289+
wolfSSH_CTX_free(ctx);
4290+
4291+
/* The peer's disconnect latches WS_DISCONNECT, which the error-state
4292+
* test below the gate used to answer with WS_INVALID_STATE_E. */
4293+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
4294+
AssertNotNull(ctx);
4295+
wolfSSH_SetIORecv(ctx, MemRecv);
4296+
wolfSSH_SetIOSend(ctx, MemSend);
4297+
4298+
ssh = wolfSSH_new(ctx);
4299+
AssertNotNull(ssh);
4300+
AddSessionChannel(ssh);
4301+
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
4302+
4303+
inSz = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION,
4304+
in, sizeof(in));
4305+
MemIoInit(&io, in, inSz, out, sizeof(out));
4306+
wolfSSH_SetIOReadCtx(ssh, &io);
4307+
wolfSSH_SetIOWriteCtx(ssh, &io);
4308+
4309+
AssertIntEQ(DoReceive(ssh), WS_FATAL_ERROR);
4310+
AssertTrue(ssh->disconnected);
4311+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4312+
quietSz = io.outSz;
4313+
state = ssh->acceptState;
4314+
4315+
ret = wolfSSH_accept(ssh);
4316+
AssertIntEQ(ret, WS_FATAL_ERROR);
4317+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4318+
AssertIntEQ(io.outSz, quietSz);
4319+
AssertIntEQ(ssh->acceptState, state);
4320+
4321+
wolfSSH_free(ssh);
4322+
wolfSSH_CTX_free(ctx);
4323+
}
4324+
4325+
#endif /* !NO_WOLFSSH_SERVER */
4326+
4327+
4328+
#ifndef NO_WOLFSSH_CLIENT
4329+
4330+
/* The same gate on the client's driver, which has no error-state test of
4331+
* its own. Split from the accept test so a single-sided build keeps the
4332+
* coverage that applies to it. */
4333+
static void TestDisconnectGatesConnect(void)
4334+
{
4335+
WOLFSSH_CTX* ctx;
4336+
WOLFSSH* ssh;
4337+
MemIo io;
4338+
byte in[128];
4339+
byte out[512];
4340+
word32 inSz;
4341+
byte state;
4342+
int ret;
4343+
4344+
/* wolfSSH_connect() has no error-state test of its own, so it reaches
4345+
* the state machine after a disconnect from either side. */
4346+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
4347+
AssertNotNull(ctx);
4348+
wolfSSH_SetIORecv(ctx, MemRecv);
4349+
wolfSSH_SetIOSend(ctx, MemSendWantWrite);
4350+
4351+
ssh = wolfSSH_new(ctx);
4352+
AssertNotNull(ssh);
4353+
AddSessionChannel(ssh);
4354+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
4355+
4356+
MemIoInit(&io, NULL, 0, out, sizeof(out));
4357+
wolfSSH_SetIOReadCtx(ssh, &io);
4358+
wolfSSH_SetIOWriteCtx(ssh, &io);
4359+
4360+
MemSendWantWriteCount = 1;
4361+
AssertIntEQ(wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION),
4362+
WS_WANT_WRITE);
4363+
AssertTrue(ssh->disconnected);
4364+
AssertTrue(wolfSSH_OutputPending(ssh));
4365+
AssertIntEQ(io.outSz, 0);
4366+
state = ssh->connectState;
4367+
4368+
ret = wolfSSH_connect(ssh);
4369+
AssertIntEQ(ret, WS_FATAL_ERROR);
4370+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4371+
AssertIntEQ(io.outSz, 0);
4372+
AssertTrue(wolfSSH_OutputPending(ssh));
4373+
AssertIntEQ(ssh->connectState, state);
4374+
4375+
wolfSSH_free(ssh);
4376+
wolfSSH_CTX_free(ctx);
4377+
4378+
/* The peer's disconnect reaches the same gate. */
4379+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
4380+
AssertNotNull(ctx);
4381+
wolfSSH_SetIORecv(ctx, MemRecv);
4382+
wolfSSH_SetIOSend(ctx, MemSend);
4383+
4384+
ssh = wolfSSH_new(ctx);
4385+
AssertNotNull(ssh);
4386+
AddSessionChannel(ssh);
4387+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
4388+
4389+
inSz = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION,
4390+
in, sizeof(in));
4391+
MemIoInit(&io, in, inSz, out, sizeof(out));
4392+
wolfSSH_SetIOReadCtx(ssh, &io);
4393+
wolfSSH_SetIOWriteCtx(ssh, &io);
4394+
4395+
AssertIntEQ(DoReceive(ssh), WS_FATAL_ERROR);
4396+
AssertTrue(ssh->disconnected);
4397+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4398+
state = ssh->connectState;
4399+
4400+
ret = wolfSSH_connect(ssh);
4401+
AssertIntEQ(ret, WS_FATAL_ERROR);
4402+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
4403+
AssertIntEQ(io.outSz, 0);
4404+
AssertIntEQ(ssh->connectState, state);
4405+
4406+
wolfSSH_free(ssh);
4407+
wolfSSH_CTX_free(ctx);
4408+
}
4409+
4410+
#endif /* !NO_WOLFSSH_CLIENT */
4411+
4412+
41974413
/* disconnectTxd means "a flush is owed", not "a disconnect was sent". Once
41984414
* ours has gone out, a teardown call must not push whatever the internal
41994415
* senders queued behind it. */
@@ -8079,6 +8295,12 @@ int main(int argc, char** argv)
80798295
TestDisconnectDrainsBufferedData();
80808296
TestDisconnectBlocksEverySend();
80818297
TestSendDisconnectIsTerminal();
8298+
#ifndef NO_WOLFSSH_SERVER
8299+
TestDisconnectGatesAccept();
8300+
#endif
8301+
#ifndef NO_WOLFSSH_CLIENT
8302+
TestDisconnectGatesConnect();
8303+
#endif
80828304
TestDisconnectQuietWindowAdjust();
80838305
TestDisconnectBlocksChannelAndFwdSends();
80848306
TestStreamExitReportsDisconnect();

wolfssh/internal.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,9 +1105,8 @@ struct WOLFSSH {
11051105
/* Set when a DISCONNECT is sent or received. Gates the public send
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
1108-
* buffer runs dry, unless a CHANNEL_EOF arrived first. wolfSSH_worker(),
1109-
* wolfSSH_accept() and wolfSSH_connect() are not gated; the shutdown
1110-
* paths pump the worker. */
1108+
* buffer runs dry, unless a CHANNEL_EOF arrived first.
1109+
* wolfSSH_worker() is not gated; the shutdown paths pump the worker. */
11111110
byte disconnected;
11121111
/* Set once SendDisconnect() has bundled our own DISCONNECT into the
11131112
* output buffer, and cleared once wolfSSH_SendPacket() drains it, so it

wolfssh/ssh.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -563,9 +563,10 @@ WOLFSSH_API int wolfSSH_connect(WOLFSSH* ssh);
563563
WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh);
564564
/* A disconnect, sent or received, ends the session. Nothing more goes out:
565565
* wolfSSH_shutdown() above this comment, and every send call below it,
566-
* report WS_DISCONNECT from then on. wolfSSH_accept() and
567-
* wolfSSH_connect() are not gated; do not drive the handshake after a
568-
* disconnect. Reads are not gated either, so channel data that
566+
* report WS_DISCONNECT from then on, as do wolfSSH_accept() and
567+
* wolfSSH_connect(). Neither driver flushes a disconnect of ours left queued
568+
* by a short send; wolfSSH_shutdown() or another wolfSSH_SendDisconnect()
569+
* owns that. Reads are not gated, so channel data that
569570
* arrived before the disconnect can still be drained; wolfSSH_stream_read()
570571
* and wolfSSH_stream_peek() report WS_DISCONNECT once their buffer runs
571572
* dry. A CHANNEL_EOF already received outranks that drain: both report

0 commit comments

Comments
 (0)