Skip to content

Commit 173a85b

Browse files
ejohnstownphilljj
authored andcommitted
Disconnect outranks a stuck rekey
wolfSSH_stream_peek() and wolfSSH_stream_read() test isKeying before disconnected, so a peer that rekeys then disconnects wedges both: only NEWKEYS clears isKeying and none is coming. Callers spin on WS_REKEYING and never get the buffered data. Both gates and read's copy step now defer to disconnected. Covered by TestDisconnectOutranksRekey. Issue: F-8837
1 parent 18b9249 commit 173a85b

2 files changed

Lines changed: 87 additions & 4 deletions

File tree

src/ssh.c

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,7 +1257,10 @@ int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz)
12571257
return WS_BAD_ARGUMENT;
12581258
}
12591259

1260-
if (ssh->isKeying) {
1260+
/* A rekey the peer abandoned with a disconnect never completes, since
1261+
* only NEWKEYS clears the flag. Report the dead session instead, or the
1262+
* caller turns the crank forever. */
1263+
if (ssh->isKeying && !ssh->disconnected) {
12611264
ssh->error = WS_REKEYING;
12621265
return WS_REKEYING;
12631266
}
@@ -1324,7 +1327,9 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz)
13241327
return WS_ERROR;
13251328
}
13261329

1327-
if (ssh->isKeying) {
1330+
/* See wolfSSH_stream_peek(): a disconnect ends a rekey that can no
1331+
* longer finish, so it outranks it here too. */
1332+
if (ssh->isKeying && !ssh->disconnected) {
13281333
ssh->error = WS_REKEYING;
13291334
return WS_FATAL_ERROR;
13301335
}
@@ -1374,8 +1379,11 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz)
13741379
}
13751380
}
13761381

1377-
/* update internal input buffer based on data read */
1378-
if (ret == WS_SUCCESS && !ssh->isKeying) {
1382+
/* update internal input buffer based on data read. DoReceive() above may
1383+
* have started a rekey, which holds the copy back -- unless a disconnect
1384+
* came with it, since then the rekey never finishes and the buffered data
1385+
* would never be handed back. */
1386+
if (ret == WS_SUCCESS && (!ssh->isKeying || ssh->disconnected)) {
13791387
int n;
13801388

13811389
n = min(bufSz, inputBuffer->length - inputBuffer->idx);

tests/regress.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3734,6 +3734,80 @@ static void TestShutdownFlushesQueuedDisconnect(void)
37343734
}
37353735

37363736

3737+
/* A rekey that the peer abandons with a DISCONNECT never completes: only
3738+
* NEWKEYS clears isKeying, and nothing more arrives. The read calls test
3739+
* isKeying first, so they report WS_REKEYING forever and the caller's
3740+
* "keep turning the crank" branch spins for the life of the connection.
3741+
* A dead session outranks a rekey that can no longer finish. */
3742+
static void TestDisconnectOutranksRekey(void)
3743+
{
3744+
WOLFSSH_CTX* ctx;
3745+
WOLFSSH* ssh;
3746+
WOLFSSH_CHANNEL* channel;
3747+
MemIo io;
3748+
byte in[128];
3749+
byte out[256];
3750+
byte payload[32];
3751+
byte data[64];
3752+
word32 inSz;
3753+
int ret;
3754+
3755+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
3756+
AssertNotNull(ctx);
3757+
3758+
wolfSSH_SetIORecv(ctx, MemRecv);
3759+
wolfSSH_SetIOSend(ctx, MemSend);
3760+
3761+
ssh = wolfSSH_new(ctx);
3762+
AssertNotNull(ssh);
3763+
AddSessionChannel(ssh);
3764+
channel = ssh->channelList;
3765+
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
3766+
3767+
/* Channel data that arrived before the rekey started. */
3768+
WMEMSET(payload, 'a', sizeof(payload));
3769+
AssertIntEQ(ChannelPutData(channel, payload, sizeof(payload)), WS_SUCCESS);
3770+
3771+
/* The peer's KEXINIT, the way DoKexInit records it. */
3772+
ssh->isKeying |= WOLFSSH_PEER_IS_KEYING;
3773+
3774+
/* DISCONNECT is a transport-generic message, so the rekey filter in
3775+
* IsMessageAllowed() lets it through. */
3776+
inSz = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION,
3777+
in, sizeof(in));
3778+
MemIoInit(&io, in, inSz, out, sizeof(out));
3779+
wolfSSH_SetIOReadCtx(ssh, &io);
3780+
wolfSSH_SetIOWriteCtx(ssh, &io);
3781+
3782+
AssertIntEQ(DoReceive(ssh), WS_FATAL_ERROR);
3783+
AssertTrue(ssh->disconnected);
3784+
/* The rekey is stuck: no NEWKEYS is ever coming. */
3785+
AssertTrue(ssh->isKeying != 0);
3786+
io.outSz = 0;
3787+
3788+
/* What arrived before the disconnect is still the caller's. */
3789+
ret = wolfSSH_stream_peek(ssh, data, sizeof(data));
3790+
AssertIntEQ(ret, (int)sizeof(payload));
3791+
ret = wolfSSH_stream_read(ssh, data, sizeof(data));
3792+
AssertIntEQ(ret, (int)sizeof(payload));
3793+
3794+
/* Drained, so both report the disconnect instead of a rekey that will
3795+
* never finish. */
3796+
ret = wolfSSH_stream_peek(ssh, data, sizeof(data));
3797+
AssertIntEQ(ret, WS_FATAL_ERROR);
3798+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
3799+
ret = wolfSSH_stream_read(ssh, data, sizeof(data));
3800+
AssertIntEQ(ret, WS_FATAL_ERROR);
3801+
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
3802+
3803+
/* The drain stayed quiet, as it does outside a rekey. */
3804+
AssertIntEQ(io.outSz, 0);
3805+
3806+
wolfSSH_free(ssh);
3807+
wolfSSH_CTX_free(ctx);
3808+
}
3809+
3810+
37373811
/* A flush that is itself short owns ssh->error. Callers gate their retry on
37383812
* WS_WANT_WRITE, so the disconnect gate must not overwrite it. */
37393813
static void TestShutdownKeepsFlushWantWrite(void)
@@ -7567,6 +7641,7 @@ int main(int argc, char** argv)
75677641
TestQueuedDisconnectFlushes();
75687642
TestShutdownFlushesQueuedDisconnect();
75697643
TestShutdownKeepsFlushWantWrite();
7644+
TestDisconnectOutranksRekey();
75707645
#if defined(WOLFSSH_TERM) && !defined(NO_FILESYSTEM)
75717646
TestTerminalResizeBlockedAfterDisconnect();
75727647
#endif

0 commit comments

Comments
 (0)