Skip to content

Commit c547220

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. The comments on _ChannelRead(), _ChannelReadExt() and test_ChannelReadExtHardFailureReported() drop the former contract. - wolfSSH_stream_read()'s note drops the receive-succeeded precondition. - 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 discarded buffer, an out-of-bounds send and a close whose flush hard-failed, and queued output staying unsent on the disconnect pass.
1 parent addef17 commit c547220

5 files changed

Lines changed: 682 additions & 69 deletions

File tree

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: 25 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,8 @@ 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 window adjust failed. ssh->error carries the code, and the
4202+
* log skips a WS_WANT_WRITE, which only asks for a retry. */
42334203
ssh->error = updateResult;
42344204
if (updateResult != WS_WANT_WRITE) {
42354205
WLOG(WS_LOG_ERROR,
@@ -4289,8 +4259,8 @@ static int _ChannelReadExt(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz)
42894259
ssh->error = savedError;
42904260
}
42914261
else {
4292-
/* SendPacket() sets ssh->error only for WS_WANT_WRITE, so hard
4293-
* failures must be recorded here or they stay hidden. */
4262+
/* The window adjust failed. ssh->error carries the code, and
4263+
* the log skips a WS_WANT_WRITE, which only asks for a retry. */
42944264
ssh->error = adjustResult;
42954265
if (adjustResult != WS_WANT_WRITE) {
42964266
WLOG(WS_LOG_ERROR,

tests/regress.c

Lines changed: 9 additions & 1 deletion
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

0 commit comments

Comments
 (0)