Skip to content

Commit fe68694

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. A failed flush reaches the return only when the receive reported nothing. 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. Drops the second DoReceive(), its WS_WINDOW_FULL case, the WOLFSSH_TEST_BLOCK fork, and the separate WS_CHANNEL_CLOSED flush. - sendRet moves to the top of wolfSSH_worker(), and the WS_REKEYING report is skipped when the flush failed. - SendPacketFlush() records its code in ssh->error on every transport failure path, and wolfSSH_SendPacket() says so and what a later write to that field owes it. - wolfssh/ssh.h states the rule on wolfSSH_worker(): a status survives in the return while wolfSSH_get_error() holds the flush's code, except that a WS_CHANNEL_CLOSED whose flush failed hard keeps the close. It drops WS_WINDOW_FULL, which the worker can no longer return. The echoserver, Espressif and portfwd comments now name the channel's own state rather than restating the worker's contract, and four comments in ssh.c and unit.c drop the former one. - The #ifndef WOLFSSH_TEST_BLOCK guards around TestWorkerReadsWhenSendWouldBlock go. The send-first fork they worked around is gone, so it runs in every configuration. - tests cover the flush on an idle receive, the owed flush across calls, what ssh->error holds after a receive failure, a hard send failure, channel data alongside a failed send with and without a rekey, a rekey the flush did not disturb, a discarded buffer, an out-of-bounds send, a missing send callback, a bad buffer state and a close whose flush hard-failed, and queued output staying unsent on the disconnect pass.
1 parent 85da4dd commit fe68694

8 files changed

Lines changed: 965 additions & 215 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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4955,6 +4955,7 @@ static int SendPacketFlush(WOLFSSH* ssh)
49554955

49564956
if (ssh->ctx->ioSendCb == NULL) {
49574957
WLOG(WS_LOG_DEBUG, "Your IO Send callback is null, please set");
4958+
ssh->error = WS_SOCKET_ERROR_E;
49584959
return WS_SOCKET_ERROR_E;
49594960
}
49604961

@@ -4965,6 +4966,7 @@ static int SendPacketFlush(WOLFSSH* ssh)
49654966
if (ssh->outputBuffer.length > ssh->outputBuffer.bufferSz ||
49664967
ssh->outputBuffer.length < ssh->outputBuffer.idx) {
49674968
WLOG(WS_LOG_ERROR, "Bad buffer state");
4969+
ssh->error = WS_BUFFER_E;
49684970
return WS_BUFFER_E;
49694971
}
49704972

@@ -5003,11 +5005,13 @@ static int SendPacketFlush(WOLFSSH* ssh)
50035005
ssh->outputBuffer.plainSz = 0;
50045006
ShrinkBuffer(&ssh->outputBuffer, 1);
50055007
}
5008+
ssh->error = WS_SOCKET_ERROR_E;
50065009
return WS_SOCKET_ERROR_E;
50075010
}
50085011

50095012
if ((word32)sent > ssh->outputBuffer.length) {
50105013
WLOG(WS_LOG_DEBUG, "wolfSSH_SendPacket() out of bounds read");
5014+
ssh->error = WS_SEND_OOB_READ_E;
50115015
return WS_SEND_OOB_READ_E;
50125016
}
50135017

@@ -5032,7 +5036,9 @@ static int SendPacketFlush(WOLFSSH* ssh)
50325036
}
50335037

50345038

5035-
/* returns WS_SUCCESS on success */
5039+
/* returns WS_SUCCESS on success. Transport failures record their code in
5040+
* ssh->error, so a later write to that field on the same pass has to be
5041+
* conditional on this having succeeded, or it hides the dead transport. */
50365042
int wolfSSH_SendPacket(WOLFSSH* ssh)
50375043
{
50385044
int ret;

src/ssh.c

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3707,6 +3707,7 @@ const char* wolfSSH_GetSessionCommand(const WOLFSSH* ssh)
37073707
int wolfSSH_worker(WOLFSSH* ssh, word32* channelId)
37083708
{
37093709
int ret = WS_SUCCESS;
3710+
int sendRet = WS_SUCCESS;
37103711

37113712
WLOG(WS_LOG_DEBUG, "Entering wolfSSH_worker()");
37123713

@@ -3723,59 +3724,30 @@ int wolfSSH_worker(WOLFSSH* ssh, word32* channelId)
37233724
return WS_FATAL_ERROR;
37243725
}
37253726

3726-
#ifdef WOLFSSH_TEST_BLOCK
3727-
/* In forced non-blocking test mode, keep legacy ordering (send before
3728-
* receive) to match the harness expectations and avoid synthetic spins. */
3729-
if (ret == WS_SUCCESS) {
3730-
if (ssh->outputBuffer.length != 0)
3731-
ret = wolfSSH_SendPacket(ssh);
3732-
}
3733-
if (ret == WS_SUCCESS)
3734-
ret = DoReceive(ssh);
3735-
#else
37363727
/* Always service inbound data first so window updates can unblock sends. */
37373728
if (ret == WS_SUCCESS) {
37383729
ret = DoReceive(ssh);
37393730
}
37403731

3741-
/* If receive only wanted read or delivered channel data, still try to
3742-
* flush any pending outbound packets. */
3743-
if (ret == WS_SUCCESS || ret == WS_WANT_READ || ret == WS_CHAN_RXD
3744-
|| ret == WS_EOF) {
3745-
int sendRet = WS_SUCCESS;
3746-
3747-
if (ssh->outputBuffer.length != 0)
3748-
sendRet = wolfSSH_SendPacket(ssh);
3749-
3750-
/* If send is back-pressured, immediately try another receive to pick
3751-
* up potential window-adjusts and then return the send status. The
3752-
* send status wins; a peer EOF stays latched on the channel. */
3753-
if (sendRet == WS_WANT_WRITE || sendRet == WS_WINDOW_FULL) {
3754-
int recv2 = DoReceive(ssh);
3755-
if (recv2 == WS_SUCCESS || recv2 == WS_WANT_READ || recv2 == WS_CHAN_RXD
3756-
|| recv2 == WS_EOF)
3757-
ret = sendRet;
3758-
else
3759-
ret = recv2;
3760-
}
3761-
else {
3762-
/* Preserve meaningful receive status when send succeeded. */
3763-
if (sendRet != WS_SUCCESS)
3732+
/* Flush queued output whatever DoReceive() made of the socket, since an
3733+
* idle receive reports WS_FATAL_ERROR. !ssh->disconnected gates it. */
3734+
if (ssh != NULL && !ssh->disconnected && ssh->outputBuffer.length != 0) {
3735+
int rxErr = ssh->error;
3736+
3737+
sendRet = wolfSSH_SendPacket(ssh);
3738+
if (sendRet != WS_SUCCESS) {
3739+
if (ret == WS_SUCCESS) {
37643740
ret = sendRet;
3765-
/* else leave ret as prior receive result (SUCCESS/WANT_READ/CHAN_RXD). */
3741+
}
3742+
else if ((ret == WS_CHANNEL_CLOSED && sendRet != WS_WANT_WRITE)
3743+
|| (ret == WS_FATAL_ERROR && rxErr != WS_WANT_READ)) {
3744+
/* A failed receive outranks the flush, and so does a close
3745+
* whose flush hard-failed: callers route teardown on it.
3746+
* Every other status keeps the code the send set. */
3747+
ssh->error = rxErr;
3748+
}
37663749
}
37673750
}
3768-
#endif /* WOLFSSH_TEST_BLOCK */
3769-
3770-
/* DoChannelClose() bundles the reply inside DoReceive(), and callers
3771-
* treat the close as terminal, so flush it here. The close stays the
3772-
* return value; a short flush leaves WS_WANT_WRITE latched. */
3773-
if (ret == WS_CHANNEL_CLOSED && ssh->outputBuffer.length != 0) {
3774-
int closeErr = ssh->error;
3775-
3776-
if (wolfSSH_SendPacket(ssh) == WS_SUCCESS)
3777-
ssh->error = closeErr;
3778-
}
37793751

37803752
/* WS_EXTDATA and WS_EOF report the channel too, so a multi-channel caller
37813753
* can route the drain, or see which channel half-closed. */
@@ -3785,12 +3757,10 @@ int wolfSSH_worker(WOLFSSH* ssh, word32* channelId)
37853757
*channelId = ssh->lastRxId;
37863758
}
37873759

3788-
/* WS_EXTDATA and WS_EOF are raised once, on arrival; masking either
3789-
* strands the event, and the stderr window credit with it. A
3790-
* disconnect cannot be seen here: the gate at the top returns before
3791-
* this, and the DISCONNECT that sets the flag mid-pass leaves ret
3792-
* fatal. */
3793-
if (ssh->isKeying && ret != WS_EXTDATA && ret != WS_EOF) {
3760+
/* Report the rekey, unless it would hide a once-only WS_EXTDATA
3761+
* or WS_EOF, or the error from a flush that failed. */
3762+
if (ssh->isKeying && ret != WS_EXTDATA && ret != WS_EOF
3763+
&& sendRet == WS_SUCCESS) {
37943764
ssh->error = WS_REKEYING;
37953765
return WS_REKEYING;
37963766
}
@@ -4228,8 +4198,9 @@ static int _ChannelRead(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz)
42284198
}
42294199
}
42304200
else {
4231-
/* SendPacket() records only WS_WANT_WRITE, so a hard failure has to
4232-
* be recorded here; rewriting WS_WANT_WRITE is deliberate. */
4201+
/* The adjust can fail before it reaches the transport, so the code
4202+
* is recorded here; the log skips a WS_WANT_WRITE, which only asks
4203+
* for a retry. */
42334204
ssh->error = updateResult;
42344205
if (updateResult != WS_WANT_WRITE) {
42354206
WLOG(WS_LOG_ERROR,
@@ -4289,8 +4260,9 @@ static int _ChannelReadExt(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz)
42894260
ssh->error = savedError;
42904261
}
42914262
else {
4292-
/* SendPacket() sets ssh->error only for WS_WANT_WRITE, so hard
4293-
* failures must be recorded here or they stay hidden. */
4263+
/* The adjust can fail before it reaches the transport, so the
4264+
* code is recorded here; the log skips a WS_WANT_WRITE, which
4265+
* only asks for a retry. */
42944266
ssh->error = adjustResult;
42954267
if (adjustResult != WS_WANT_WRITE) {
42964268
WLOG(WS_LOG_ERROR,

tests/regress.c

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6770,11 +6770,19 @@ static void TestWorkerReportsDisconnect(void)
67706770
wolfSSH_SetIOReadCtx(ssh, &io);
67716771
wolfSSH_SetIOWriteCtx(ssh, &io);
67726772

6773+
/* Queued output, so the flush on this pass has something to push. */
6774+
ssh->outputBuffer.length = 1;
6775+
ssh->outputBuffer.idx = 0;
6776+
ssh->outputBuffer.buffer[0] = 0;
6777+
67736778
AssertIntEQ(wolfSSH_worker(ssh, NULL), WS_FATAL_ERROR);
67746779
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
67756780
AssertTrue(ssh->disconnected);
67766781
AssertTrue(ssh->isKeying != 0);
6777-
io.outSz = 0;
6782+
6783+
/* The queued byte stays put: the session ended on this very pass. */
6784+
AssertIntEQ(io.outSz, 0);
6785+
AssertTrue(wolfSSH_OutputPending(ssh));
67786786

67796787
/* The message behind it is still queued, and every further pass reports
67806788
* the disconnect rather than the WS_SUCCESS of a skipped dispatch or the
@@ -7447,7 +7455,6 @@ static void TestPasswordEofNoCrash(void)
74477455
* still needs to service Receive() so window-adjusts can arrive and
74487456
* unblock the flow control. Verify the receive callback is invoked even
74497457
* when the first send attempt would block. */
7450-
#ifndef WOLFSSH_TEST_BLOCK
74517458
static int recvCallCount;
74527459

74537460
static int WantWriteSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
@@ -7463,7 +7470,6 @@ static int WantReadRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
74637470
return WS_CBIO_ERR_WANT_READ;
74647471
}
74657472

7466-
#ifndef WOLFSSH_TEST_BLOCK
74677473
static void TestWorkerReadsWhenSendWouldBlock(void)
74687474
{
74697475
WOLFSSH_CTX* ctx;
@@ -7498,8 +7504,6 @@ static void TestWorkerReadsWhenSendWouldBlock(void)
74987504
wolfSSH_free(ssh);
74997505
wolfSSH_CTX_free(ctx);
75007506
}
7501-
#endif /* !WOLFSSH_TEST_BLOCK */
7502-
#endif
75037507

75047508

75057509
#ifdef WOLFSSH_SFTP
@@ -11192,9 +11196,7 @@ int main(int argc, char** argv)
1119211196
TestClientBuffersIdempotent();
1119311197
#endif
1119411198
TestPasswordEofNoCrash();
11195-
#ifndef WOLFSSH_TEST_BLOCK
1119611199
TestWorkerReadsWhenSendWouldBlock();
11197-
#endif
1119811200

1119911201
#ifdef KEXDH_REPLY_REGRESS_KEX_ALGO
1120011202
#ifndef WOLFSSH_NO_RSA_SHA2_256

0 commit comments

Comments
 (0)