Skip to content

Commit 2b8e647

Browse files
committed
Fix the EOF retry loop and the drain guards
Findings from the second review round on the EOF and close changes. - wolfSSH_stream_read() held inputBuffer across the new EOF retry. When DoChannelClose() retired the head inside DoReceive() and the next head already had its EOF latched and drained, the override forced WS_EOF while lastRxId still named the channel that had gone, so the retry looped forever on freed memory. Both messages that get there are peer-controlled. Capture the head's id and only loop while it is unchanged. - The EOF drain guards read a peek of WS_REKEYING as a drained channel. wolfSSH_stream_peek() reports it before it looks at the buffer, and wolfSSH_worker() no longer masks WS_EOF during a rekey, so this is reachable. Distinguish it in wolfsshd and both echoservers. - wolfsshd closed the child's stdin without checking that the buffered channel data had been handed over. The read above it is skipped while windowFull, so a later read would write to a closed pipe. - DoChannelClose() reported WS_WANT_WRITE and left lastRxId unset when the flush was back-pressured, costing the caller the graceful-close signal for a channel that is already closed and already bundled. The pending flush belongs to the output buffer, not the channel. - test_SendEofApi's idempotence check could not fail: DiscardIoSend flushes everything, so the output buffer length was 0 either way. Count what reaches the transport instead. Add coverage for the retry loop and the deferred flush; both fail without the fix.
1 parent bf8c40f commit 2b8e647

6 files changed

Lines changed: 276 additions & 26 deletions

File tree

‎apps/wolfsshd/wolfsshd.c‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -934,9 +934,15 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
934934
continue;
935935
}
936936

937-
/* Drain what is buffered before leaving on the EOF. */
938-
if (error == WS_EOF && wolfSSH_stream_peek(ssh, NULL, 1) <= 0) {
939-
break;
937+
/* Drain what is buffered before leaving on the EOF. A rekey
938+
* is not a drained channel: peek reports WS_REKEYING before
939+
* it ever looks at the buffer. */
940+
if (error == WS_EOF) {
941+
int peekRet = wolfSSH_stream_peek(ssh, NULL, 1);
942+
943+
if (peekRet != WS_REKEYING && peekRet <= 0) {
944+
break;
945+
}
940946
}
941947
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD
942948
&& ret != WS_EOF) {
@@ -1967,13 +1973,24 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
19671973
}
19681974
}
19691975

1970-
/* Peer done sending: close the child's stdin. */
1976+
/* Peer done sending: close the child's stdin, but only once
1977+
* what it already sent has been handed over. The read above is
1978+
* skipped while windowFull, so data can still be buffered here;
1979+
* closing early drops it and the next write lands on fd -1.
1980+
* peek reports the head channel, which is the shell channel, and
1981+
* reports WS_REKEYING without looking at the buffer at all. */
19711982
if (stdinPipe[1] != -1 && (!ptyReq || forcedCmd)) {
1983+
int peekRet = wolfSSH_stream_peek(ssh, NULL, 1);
19721984
WOLFSSH_CHANNEL* current;
19731985

19741986
current = wolfSSH_ChannelFind(ssh, shellChannelId,
19751987
WS_CHANNEL_ID_SELF);
1976-
if (current != NULL && wolfSSH_ChannelGetEof(current)) {
1988+
/* A channel that is gone reports EOF: wolfSSH_ChannelGetEof()
1989+
* returns 1 for NULL by design, and DoChannelClose() can have
1990+
* retired it already. Testing for NULL here would leave the
1991+
* child waiting on a stdin that never closes. */
1992+
if (wolfSSH_ChannelGetEof(current)
1993+
&& peekRet != WS_REKEYING && peekRet <= 0) {
19771994
/* SSH is done, close stdin pipe to child process */
19781995
close(stdinPipe[1]);
19791996
stdinPipe[1] = -1;

‎examples/echoserver/echoserver.c‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,14 @@ static int ssh_worker(thread_ctx_t* threadCtx)
10961096
continue;
10971097
}
10981098
else if (rc == WS_EOF) {
1099+
/* The peer has finished sending. Do NOT answer with an
1100+
* EOF of our own here: that latches eofTxd and
1101+
* SendChannelData() then rejects every later send, so
1102+
* the child's remaining output would be dropped. That
1103+
* auto-echo is the bug this half-close support exists
1104+
* to remove. An application sends its own EOF with
1105+
* wolfSSH_ChannelSendEof() once it has finished
1106+
* producing output for the channel. */
10991107
continue;
11001108
}
11011109
else if (rc != WS_WANT_READ) {
@@ -1438,8 +1446,15 @@ static int sftp_worker(thread_ctx_t* threadCtx)
14381446
}
14391447

14401448
/* Drain what is buffered before leaving on the EOF. */
1441-
if (error == WS_EOF && wolfSSH_stream_peek(ssh, NULL, 1) <= 0) {
1442-
break;
1449+
if (error == WS_EOF) {
1450+
/* A rekey is not a drained channel. */
1451+
int peekRet = wolfSSH_stream_peek(ssh, NULL, 1);
1452+
1453+
if (peekRet != WS_REKEYING && peekRet <= 0) {
1454+
/* An ordinary session end, not a failure. */
1455+
ret = 0;
1456+
break;
1457+
}
14431458
}
14441459
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_EOF) {
14451460
#ifdef WOLFSSH_TEST_BLOCK
@@ -1477,8 +1492,10 @@ static int sftp_worker(thread_ctx_t* threadCtx)
14771492
error == WS_CHAN_RXD || error == WS_REKEYING ||
14781493
error == WS_WINDOW_FULL)
14791494
ret = error;
1480-
if (error == WS_EOF)
1495+
if (error == WS_EOF) {
1496+
ret = 0;
14811497
break;
1498+
}
14821499
continue;
14831500
}
14841501
else if (ret == WS_REKEYING) {
@@ -1648,6 +1665,15 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs)
16481665
ret = 0;
16491666
}
16501667

1668+
/* The channel was already retired by the peer's close, which is a
1669+
* completed shutdown rather than a failure. Without this any session
1670+
* whose channel is gone before the shutdown call leaves ret at
1671+
* WS_CHANNEL_CLOSED, and the non-zero return sets quit, taking the
1672+
* whole server down after one session. */
1673+
if (ret == WS_CHANNEL_CLOSED) {
1674+
ret = 0;
1675+
}
1676+
16511677
error = wolfSSH_get_error(threadCtx->ssh);
16521678
if (error != WS_SOCKET_ERROR_E &&
16531679
(error == WS_WANT_READ || error == WS_WANT_WRITE)) {

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,14 @@ static int ssh_worker(thread_ctx_t* threadCtx)
10831083
continue;
10841084
}
10851085
else if (rc == WS_EOF) {
1086+
/* The peer has finished sending. Do NOT answer with an
1087+
* EOF of our own here: that latches eofTxd and
1088+
* SendChannelData() then rejects every later send, so
1089+
* the child's remaining output would be dropped. That
1090+
* auto-echo is the bug this half-close support exists
1091+
* to remove. An application sends its own EOF with
1092+
* wolfSSH_ChannelSendEof() once it has finished
1093+
* producing output for the channel. */
10861094
continue;
10871095
}
10881096
else if (rc != WS_WANT_READ) {
@@ -1409,8 +1417,15 @@ static int sftp_worker(thread_ctx_t* threadCtx)
14091417
}
14101418

14111419
/* Drain what is buffered before leaving on the EOF. */
1412-
if (error == WS_EOF && wolfSSH_stream_peek(ssh, NULL, 1) <= 0) {
1413-
break;
1420+
if (error == WS_EOF) {
1421+
/* A rekey is not a drained channel. */
1422+
int peekRet = wolfSSH_stream_peek(ssh, NULL, 1);
1423+
1424+
if (peekRet != WS_REKEYING && peekRet <= 0) {
1425+
/* An ordinary session end, not a failure. */
1426+
ret = 0;
1427+
break;
1428+
}
14141429
}
14151430
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_EOF) {
14161431
if (ret == WS_WANT_WRITE) {
@@ -1435,8 +1450,10 @@ static int sftp_worker(thread_ctx_t* threadCtx)
14351450
error == WS_CHAN_RXD || error == WS_REKEYING ||
14361451
error == WS_WINDOW_FULL)
14371452
ret = error;
1438-
if (error == WS_EOF)
1453+
if (error == WS_EOF) {
1454+
ret = 0;
14391455
break;
1456+
}
14401457
continue;
14411458
}
14421459
else if (ret == WS_REKEYING) {
@@ -1578,6 +1595,15 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs)
15781595
ret = 0;
15791596
}
15801597

1598+
/* The channel was already retired by the peer's close, which is a
1599+
* completed shutdown rather than a failure. Without this any session
1600+
* whose channel is gone before the shutdown call leaves ret at
1601+
* WS_CHANNEL_CLOSED, and the non-zero return sets quit, taking the
1602+
* whole server down after one session. */
1603+
if (ret == WS_CHANNEL_CLOSED) {
1604+
ret = 0;
1605+
}
1606+
15811607
error = wolfSSH_get_error(threadCtx->ssh);
15821608
if (error != WS_SOCKET_ERROR_E &&
15831609
(error == WS_WANT_READ || error == WS_WANT_WRITE)) {

‎src/internal.c‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11331,11 +11331,14 @@ static int DoChannelClose(WOLFSSH* ssh,
1133111331

1133211332
if (removeRet != WS_SUCCESS)
1133311333
ret = removeRet;
11334-
}
11335-
11336-
if (ret == WS_SUCCESS) {
11337-
ret = WS_CHANNEL_CLOSED;
11338-
ssh->lastRxId = channelId;
11334+
else {
11335+
/* The channel is closed and named either way. A back-pressured
11336+
* flush is a property of the output buffer, not of the channel,
11337+
* so reporting WS_WANT_WRITE here would cost the caller its
11338+
* graceful-close signal and leave it to time the teardown out. */
11339+
ret = WS_CHANNEL_CLOSED;
11340+
ssh->lastRxId = channelId;
11341+
}
1133911342
}
1134011343

1134111344
WLOG(WS_LOG_DEBUG, "Leaving DoChannelClose(), ret = %d", ret);

‎src/ssh.c‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,13 +1230,16 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz)
12301230
{
12311231
int ret = WS_SUCCESS;
12321232
WOLFSSH_BUFFER* inputBuffer;
1233+
word32 headId;
12331234

12341235
WLOG(WS_LOG_DEBUG, "Entering wolfSSH_stream_read()");
12351236

12361237
if (ssh == NULL || buf == NULL || bufSz == 0 || ssh->channelList == NULL)
12371238
return WS_BAD_ARGUMENT;
12381239

12391240
inputBuffer = &ssh->channelList->inputBuffer;
1241+
/* inputBuffer belongs to this channel; DoReceive() can retire it. */
1242+
headId = ssh->channelList->channel;
12401243

12411244
/* Report the EOF only once the buffered data is drained. */
12421245
if (inputBuffer->length - inputBuffer->idx == 0
@@ -1269,12 +1272,15 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz)
12691272
- ssh->channelList->inputBuffer.idx == 0))
12701273
ret = WS_EOF;
12711274
if (ret == WS_EOF && ssh->channelList != NULL
1272-
&& ssh->lastRxId != ssh->channelList->channel) {
1275+
&& ssh->channelList->channel == headId
1276+
&& ssh->lastRxId != headId) {
12731277
/* An EOF on another channel is not this read's EOF: the head
12741278
* is still open and still has nothing buffered. Reporting it
12751279
* would be indistinguishable from the head's own EOF, so keep
12761280
* waiting. Multi-channel apps see it on the channel EOF
1277-
* callback or as WS_EOF from wolfSSH_worker(). */
1281+
* callback or as WS_EOF from wolfSSH_worker(). Only loop while
1282+
* the head is unchanged: DoChannelClose() can free it, and
1283+
* inputBuffer still points into it. */
12781284
continue;
12791285
}
12801286
if (ret == WS_EXTDATA &&

0 commit comments

Comments
 (0)