Skip to content

Commit cd5c5e8

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. - 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 states that 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; the wolfSSH_ChannelSendEof() and wolfSSH_stream_read() notes match. Nine comments in ssh.c, unit.c, the echoserver, the Espressif copy and portfwd name the channel's own state or the call that failed instead of restating either. - Twelve unit tests and the extended TestWorkerReportsDisconnect cover the idle-receive flush, the owed flush across calls, and what ssh->error holds after a receive, send, buffer or callback failure, with and without a rekey or a channel close. The #ifndef WOLFSSH_TEST_BLOCK guards around TestWorkerReadsWhenSendWouldBlock go with the send-first fork.
1 parent e7bb6de commit cd5c5e8

8 files changed

Lines changed: 972 additions & 213 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)