Skip to content

Commit c9f9857

Browse files
ssh, internal: always flush the worker's output
- wolfSSH_worker() calls wolfSSH_SendPacket() whenever ssh->outputBuffer holds bytes and the session is not disconnected, in place of doing so only for WS_SUCCESS, WS_WANT_READ, WS_CHAN_RXD or WS_EOF. ssh->error keeps the receive's code when the receive failed, and the close's when a WS_CHANNEL_CLOSED pass hard-failed its flush; a function-scope sendRet also masks the WS_REKEYING report. Drops the second DoReceive(), its WS_WINDOW_FULL case, the WOLFSSH_TEST_BLOCK fork, and the separate WS_CHANNEL_CLOSED flush. - BundlePacket() resets ssh->outputBuffer.length to ssh->packetStartIdx when the framing fails, and wolfSSH_shutdown() reports WS_WANT_WRITE when its read for the peer's close leaves output queued. - SendPacketFlush() records its code in ssh->error on every transport failure path; wolfSSH_SendPacket() says so and what a later write to that field owes it. - wolfssh/ssh.h drops WS_WINDOW_FULL from wolfSSH_worker() and names the two exceptions to a status surviving a failed flush: a hard-failed WS_CHANNEL_CLOSED, and WS_REKEYING. Comments in ssh.c, unit.c, the echoserver, the Espressif copy and portfwd name the channel's own state or the failing call instead of restating a contract. - Sixteen unit tests and the extended TestWorkerReportsDisconnect cover what ret and ssh->error hold after a receive, send, buffer, callback or framing failure, against channel data, extended data, a half-close, a rekey, a close, and a teardown read. The #ifndef WOLFSSH_TEST_BLOCK guards around TestWorkerReadsWhenSendWouldBlock go with the send-first fork.
1 parent 12c0d1a commit c9f9857

8 files changed

Lines changed: 1243 additions & 202 deletions

File tree

examples/echoserver/echoserver.c

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,12 +1016,9 @@ static int ssh_worker(thread_ctx_t* threadCtx)
10161016
rc = wolfSSH_get_error(ssh);
10171017

10181018
/* The peer is done sending: hand back the backlog and answer
1019-
* its EOF, or a client that half-closed waits on a server
1020-
* that never finishes -- the library no longer answers for
1021-
* us. Off the channel's own state, not the WS_EOF status: the
1022-
* flush inside wolfSSH_worker() can supersede that, and it is
1023-
* raised once. Echo mode only; a shell child on a pty is
1024-
* still producing, so its EOF waits for the child to exit. */
1019+
* its EOF, since the library no longer answers for us. Off
1020+
* the channel's own state, not the once-only WS_EOF status.
1021+
* Echo mode only; a shell child on a pty still produces. */
10251022
if (!eofAnswered && echoOnly) {
10261023
WOLFSSH_CHANNEL* eofChannel;
10271024

examples/portfwd/portfwd.c

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -781,10 +781,9 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args)
781781

782782
/* Relay the half-close so a local reader waiting on end-of-input
783783
* returns; nothing else relays it. Driven off the latched channel
784-
* state, not the WS_EOF status: the flush inside wolfSSH_worker()
785-
* can supersede that, and it is raised only once. Only the channel
786-
* appFd is wired to, since half-closing the wrong socket truncates
787-
* a live transfer. */
784+
* state, not the once-only WS_EOF status. Only the channel appFd
785+
* is wired to: half-closing the wrong socket truncates a live
786+
* transfer. */
788787
if (appFdSet && fwdChannel != NULL && !appFdHalfClosed
789788
&& wolfSSH_ChannelGetEof(fwdChannel)) {
790789
int drained;

ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,12 +1000,9 @@ static int ssh_worker(thread_ctx_t* threadCtx)
10001000
rc = wolfSSH_get_error(ssh);
10011001

10021002
/* The peer is done sending: hand back the backlog and answer
1003-
* its EOF, or a client that half-closed waits on a server
1004-
* that never finishes -- the library no longer answers for
1005-
* us. Off the channel's own state, not the WS_EOF status: the
1006-
* flush inside wolfSSH_worker() can supersede that, and it is
1007-
* raised once. Echo mode only; a shell child on a pty is
1008-
* still producing, so its EOF waits for the child to exit. */
1003+
* its EOF, since the library no longer answers for us. Off
1004+
* the channel's own state, not the once-only WS_EOF status.
1005+
* Echo mode only; a shell child on a pty still produces. */
10091006
if (!eofAnswered && echoOnly) {
10101007
WOLFSSH_CHANNEL* eofChannel;
10111008

src/internal.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5142,6 +5142,7 @@ static int SendPacketFlush(WOLFSSH* ssh)
51425142

51435143
if (ssh->ctx->ioSendCb == NULL) {
51445144
WLOG(WS_LOG_DEBUG, "Your IO Send callback is null, please set");
5145+
ssh->error = WS_SOCKET_ERROR_E;
51455146
return WS_SOCKET_ERROR_E;
51465147
}
51475148

@@ -5152,6 +5153,7 @@ static int SendPacketFlush(WOLFSSH* ssh)
51525153
if (ssh->outputBuffer.length > ssh->outputBuffer.bufferSz ||
51535154
ssh->outputBuffer.length < ssh->outputBuffer.idx) {
51545155
WLOG(WS_LOG_ERROR, "Bad buffer state");
5156+
ssh->error = WS_BUFFER_E;
51555157
return WS_BUFFER_E;
51565158
}
51575159

@@ -5190,11 +5192,13 @@ static int SendPacketFlush(WOLFSSH* ssh)
51905192
ssh->outputBuffer.plainSz = 0;
51915193
ShrinkBuffer(&ssh->outputBuffer, 1);
51925194
}
5195+
ssh->error = WS_SOCKET_ERROR_E;
51935196
return WS_SOCKET_ERROR_E;
51945197
}
51955198

51965199
if ((word32)sent > ssh->outputBuffer.length) {
51975200
WLOG(WS_LOG_DEBUG, "wolfSSH_SendPacket() out of bounds read");
5201+
ssh->error = WS_SEND_OOB_READ_E;
51985202
return WS_SEND_OOB_READ_E;
51995203
}
52005204

@@ -5219,7 +5223,9 @@ static int SendPacketFlush(WOLFSSH* ssh)
52195223
}
52205224

52215225

5222-
/* returns WS_SUCCESS on success */
5226+
/* returns WS_SUCCESS on success. Transport failures record their code in
5227+
* ssh->error, so a later write to that field on the same pass has to be
5228+
* conditional on this having succeeded, or it hides the dead transport. */
52235229
int wolfSSH_SendPacket(WOLFSSH* ssh)
52245230
{
52255231
int ret;
@@ -14364,6 +14370,10 @@ static int BundlePacket(WOLFSSH* ssh)
1436414370
}
1436514371
else {
1436614372
WLOG(WS_LOG_DEBUG, "BP: failed to encrypt buffer");
14373+
if (ssh != NULL) {
14374+
/* Drop the aborted packet */
14375+
ssh->outputBuffer.length = ssh->packetStartIdx;
14376+
}
1436714377
}
1436814378

1436914379
return ret;

src/ssh.c

Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,9 @@ int wolfSSH_shutdown(WOLFSSH* ssh)
12521252
/* received response */
12531253
ret = WS_SUCCESS;
12541254
}
1255+
/* A reply queued during that read has not gone out yet. */
1256+
if (ret == WS_SUCCESS && wolfSSH_OutputPending(ssh))
1257+
ret = WS_WANT_WRITE;
12551258
}
12561259

12571260
if (ssh != NULL && ssh->channelList == NULL) {
@@ -3734,6 +3737,7 @@ const char* wolfSSH_GetSessionCommand(const WOLFSSH* ssh)
37343737
int wolfSSH_worker(WOLFSSH* ssh, word32* channelId)
37353738
{
37363739
int ret = WS_SUCCESS;
3740+
int sendRet = WS_SUCCESS;
37373741

37383742
WLOG(WS_LOG_DEBUG, "Entering wolfSSH_worker()");
37393743

@@ -3750,59 +3754,30 @@ int wolfSSH_worker(WOLFSSH* ssh, word32* channelId)
37503754
return WS_FATAL_ERROR;
37513755
}
37523756

3753-
#ifdef WOLFSSH_TEST_BLOCK
3754-
/* In forced non-blocking test mode, keep legacy ordering (send before
3755-
* receive) to match the harness expectations and avoid synthetic spins. */
3756-
if (ret == WS_SUCCESS) {
3757-
if (ssh->outputBuffer.length != 0)
3758-
ret = wolfSSH_SendPacket(ssh);
3759-
}
3760-
if (ret == WS_SUCCESS)
3761-
ret = DoReceive(ssh);
3762-
#else
37633757
/* Always service inbound data first so window updates can unblock sends. */
37643758
if (ret == WS_SUCCESS) {
37653759
ret = DoReceive(ssh);
37663760
}
37673761

3768-
/* If receive only wanted read or delivered channel data, still try to
3769-
* flush any pending outbound packets. */
3770-
if (ret == WS_SUCCESS || ret == WS_WANT_READ || ret == WS_CHAN_RXD
3771-
|| ret == WS_EOF) {
3772-
int sendRet = WS_SUCCESS;
3773-
3774-
if (ssh->outputBuffer.length != 0)
3775-
sendRet = wolfSSH_SendPacket(ssh);
3776-
3777-
/* If send is back-pressured, immediately try another receive to pick
3778-
* up potential window-adjusts and then return the send status. The
3779-
* send status wins; a peer EOF stays latched on the channel. */
3780-
if (sendRet == WS_WANT_WRITE || sendRet == WS_WINDOW_FULL) {
3781-
int recv2 = DoReceive(ssh);
3782-
if (recv2 == WS_SUCCESS || recv2 == WS_WANT_READ || recv2 == WS_CHAN_RXD
3783-
|| recv2 == WS_EOF)
3784-
ret = sendRet;
3785-
else
3786-
ret = recv2;
3787-
}
3788-
else {
3789-
/* Preserve meaningful receive status when send succeeded. */
3790-
if (sendRet != WS_SUCCESS)
3762+
/* Flush queued output whatever DoReceive() made of the socket, since an
3763+
* idle receive reports WS_FATAL_ERROR. !ssh->disconnected gates it. */
3764+
if (ssh != NULL && !ssh->disconnected && ssh->outputBuffer.length != 0) {
3765+
int rxErr = ssh->error;
3766+
3767+
sendRet = wolfSSH_SendPacket(ssh);
3768+
if (sendRet != WS_SUCCESS) {
3769+
if (ret == WS_SUCCESS) {
37913770
ret = sendRet;
3792-
/* else leave ret as prior receive result (SUCCESS/WANT_READ/CHAN_RXD). */
3771+
}
3772+
else if ((ret == WS_CHANNEL_CLOSED && sendRet != WS_WANT_WRITE)
3773+
|| (ret == WS_FATAL_ERROR && rxErr != WS_WANT_READ)) {
3774+
/* A failed receive outranks the flush, and so does a close
3775+
* whose flush hard-failed: callers route teardown on it.
3776+
* Every other status keeps the code the send set. */
3777+
ssh->error = rxErr;
3778+
}
37933779
}
37943780
}
3795-
#endif /* WOLFSSH_TEST_BLOCK */
3796-
3797-
/* DoChannelClose() bundles the reply inside DoReceive(), and callers
3798-
* treat the close as terminal, so flush it here. The close stays the
3799-
* return value; a short flush leaves WS_WANT_WRITE latched. */
3800-
if (ret == WS_CHANNEL_CLOSED && ssh->outputBuffer.length != 0) {
3801-
int closeErr = ssh->error;
3802-
3803-
if (wolfSSH_SendPacket(ssh) == WS_SUCCESS)
3804-
ssh->error = closeErr;
3805-
}
38063781

38073782
/* WS_EXTDATA and WS_EOF report the channel too, so a multi-channel caller
38083783
* can route the drain, or see which channel half-closed. */
@@ -3812,12 +3787,10 @@ int wolfSSH_worker(WOLFSSH* ssh, word32* channelId)
38123787
*channelId = ssh->lastRxId;
38133788
}
38143789

3815-
/* WS_EXTDATA and WS_EOF are raised once, on arrival; masking either
3816-
* strands the event, and the stderr window credit with it. A
3817-
* disconnect cannot be seen here: the gate at the top returns before
3818-
* this, and the DISCONNECT that sets the flag mid-pass leaves ret
3819-
* fatal. */
3820-
if (ssh->isKeying && ret != WS_EXTDATA && ret != WS_EOF) {
3790+
/* Report the rekey, unless it would hide a once-only WS_EXTDATA
3791+
* or WS_EOF, or the error from a flush that failed. */
3792+
if (ssh->isKeying && ret != WS_EXTDATA && ret != WS_EOF
3793+
&& sendRet == WS_SUCCESS) {
38213794
ssh->error = WS_REKEYING;
38223795
return WS_REKEYING;
38233796
}
@@ -4255,8 +4228,9 @@ static int _ChannelRead(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz)
42554228
}
42564229
}
42574230
else {
4258-
/* SendPacket() records only WS_WANT_WRITE, so a hard failure has to
4259-
* be recorded here; rewriting WS_WANT_WRITE is deliberate. */
4231+
/* The adjust can fail before it reaches the transport, so the code
4232+
* is recorded here; the log skips a WS_WANT_WRITE, which only asks
4233+
* for a retry. */
42604234
ssh->error = updateResult;
42614235
if (updateResult != WS_WANT_WRITE) {
42624236
WLOG(WS_LOG_ERROR,
@@ -4316,8 +4290,9 @@ static int _ChannelReadExt(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz)
43164290
ssh->error = savedError;
43174291
}
43184292
else {
4319-
/* SendPacket() sets ssh->error only for WS_WANT_WRITE, so hard
4320-
* failures must be recorded here or they stay hidden. */
4293+
/* The adjust can fail before it reaches the transport, so the
4294+
* code is recorded here; the log skips a WS_WANT_WRITE, which
4295+
* only asks for a retry. */
43214296
ssh->error = adjustResult;
43224297
if (adjustResult != WS_WANT_WRITE) {
43234298
WLOG(WS_LOG_ERROR,

tests/regress.c

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8277,11 +8277,19 @@ static void TestWorkerReportsDisconnect(void)
82778277
wolfSSH_SetIOReadCtx(ssh, &io);
82788278
wolfSSH_SetIOWriteCtx(ssh, &io);
82798279

8280+
/* Queued output, so the flush on this pass has something to push. */
8281+
ssh->outputBuffer.length = 1;
8282+
ssh->outputBuffer.idx = 0;
8283+
ssh->outputBuffer.buffer[0] = 0;
8284+
82808285
AssertIntEQ(wolfSSH_worker(ssh, NULL), WS_FATAL_ERROR);
82818286
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
82828287
AssertTrue(ssh->disconnected);
82838288
AssertTrue(ssh->isKeying != 0);
8284-
io.outSz = 0;
8289+
8290+
/* The queued byte stays put: the session ended on this very pass. */
8291+
AssertIntEQ(io.outSz, 0);
8292+
AssertTrue(wolfSSH_OutputPending(ssh));
82858293

82868294
/* The message behind it is still queued, and every further pass reports
82878295
* the disconnect rather than the WS_SUCCESS of a skipped dispatch or the
@@ -8954,7 +8962,6 @@ static void TestPasswordEofNoCrash(void)
89548962
* still needs to service Receive() so window-adjusts can arrive and
89558963
* unblock the flow control. Verify the receive callback is invoked even
89568964
* when the first send attempt would block. */
8957-
#ifndef WOLFSSH_TEST_BLOCK
89588965
static int recvCallCount;
89598966

89608967
static int WantWriteSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
@@ -8970,7 +8977,6 @@ static int WantReadRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
89708977
return WS_CBIO_ERR_WANT_READ;
89718978
}
89728979

8973-
#ifndef WOLFSSH_TEST_BLOCK
89748980
static void TestWorkerReadsWhenSendWouldBlock(void)
89758981
{
89768982
WOLFSSH_CTX* ctx;
@@ -9005,8 +9011,6 @@ static void TestWorkerReadsWhenSendWouldBlock(void)
90059011
wolfSSH_free(ssh);
90069012
wolfSSH_CTX_free(ctx);
90079013
}
9008-
#endif /* !WOLFSSH_TEST_BLOCK */
9009-
#endif
90109014

90119015

90129016
#ifdef WOLFSSH_SFTP
@@ -13303,9 +13307,7 @@ int main(int argc, char** argv)
1330313307
TestClientBuffersIdempotent();
1330413308
#endif
1330513309
TestPasswordEofNoCrash();
13306-
#ifndef WOLFSSH_TEST_BLOCK
1330713310
TestWorkerReadsWhenSendWouldBlock();
13308-
#endif
1330913311

1331013312
#ifdef KEXDH_REPLY_REGRESS_KEX_ALGO
1331113313
#ifndef WOLFSSH_NO_RSA_SHA2_256

0 commit comments

Comments
 (0)