Carry the EOF status through SFTP and SCP

wolfSSH_SFTP_buffer_send() and ScpStreamSend() keep driving the worker when the
peer half-closes. Both return any negative status, so a WS_EOF would have
aborted a transfer that is still perfectly able to finish: the peer closed its
sending direction, not ours.

- wolfSSH_SFTP_buffer_read() reports every negative peek but a rekey instead of
  spending a receive on it. A drained channel at EOF, a dead session and a
  channel that is gone all mean no more data can arrive, and the poll only
  overwrites the latched cause with WS_WANT_READ or blocks on a peer that has
  hung up.
- A rekey is not a drained channel: peek reports it before it looks at the
  buffer at all, so that one still needs the poll.
- DoScpRequest() reads its own EOF case the same way as the rest.
pull/1218/head
John Safranek 2026-08-27 13:08:57 -07:00 committed by philljj
parent 74098863ad
commit 2bfdbac2b3
3 changed files with 95 additions and 11 deletions

View File

@ -151,9 +151,10 @@ static int ScpStreamSend(WOLFSSH* ssh, byte* data, word32 sz)
if (err == WS_WANT_READ || err == WS_WANT_WRITE)
return err;
/* Only a rekey/window/channel-data status means "keep driving".
* Any other negative status is fatal and returned. */
* Any other negative status is fatal and returned. A peer EOF
* closes their direction only, and is raised once. */
if (ret < 0 && ret != WS_REKEYING && ret != WS_WINDOW_FULL
&& ret != WS_CHAN_RXD)
&& ret != WS_CHAN_RXD && ret != WS_EOF)
return ret;
/* otherwise loop and retry the send, which clears the status */
}
@ -944,15 +945,13 @@ int DoScpRequest(WOLFSSH* ssh)
/* Peer MUST send back a SSH_MSG_CHANNEL_CLOSE unless already
sent*/
ret = ScpStreamRead(ssh, buf, 1);
if (ret == WS_SOCKET_ERROR_E || ret == WS_CHANNEL_CLOSED) {
if (ret == WS_SOCKET_ERROR_E || ret == WS_CHANNEL_CLOSED
|| ret == WS_EOF) {
WLOG(WS_LOG_DEBUG, scpState, "Peer hung up, but SCP is done");
ret = WS_SUCCESS;
}
else if (ret != WS_EOF) {
WLOG(WS_LOG_DEBUG, scpState, "Did not receive EOF packet");
}
else {
ret = WS_SUCCESS;
WLOG(WS_LOG_DEBUG, scpState, "Did not receive EOF packet");
}
}
@ -1784,7 +1783,7 @@ int ReceiveScpMessage(WOLFSSH* ssh)
}
}
/* check if wolfSSH_worker returns 0 from handling a channel eof */
/* Already at EOF, and the worker had nothing else to report. */
if (err == 0) {
WOLFSSH_CHANNEL* channel;
channel = wolfSSH_ChannelFind(ssh, lastChannel, WS_CHANNEL_ID_SELF);

View File

@ -593,9 +593,9 @@ static int wolfSSH_SFTP_buffer_send(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer)
/* Only a rekey/window/channel-data status means "keep driving". Any
* other negative status (fatal error, or WS_WANT_READ/WS_WANT_WRITE on
* a non-blocking socket) is returned so a stalled or dead rekey cannot
* spin forever. */
* spin forever. A peer EOF ends their direction only, not ours. */
if (ret < 0 && ret != WS_REKEYING && ret != WS_WINDOW_FULL
&& ret != WS_CHAN_RXD) {
&& ret != WS_CHAN_RXD && ret != WS_EOF) {
return ret;
}
err = wolfSSH_get_error(ssh);
@ -720,6 +720,7 @@ static int wolfSSH_SFTP_buffer_read(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer,
{
int ret;
int polled;
int peekRet;
byte peekBuf[1];
if (buffer == NULL || ssh == NULL) {
@ -759,7 +760,18 @@ static int wolfSSH_SFTP_buffer_read(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer,
}
}
if (!wolfSSH_stream_peek(ssh, peekBuf, 1)) {
peekRet = wolfSSH_stream_peek(ssh, peekBuf, 1);
/* Every negative peek but a rekey means no more data can arrive:
* drained and at EOF, session gone, or channel gone. Polling would
* only overwrite the cause with WS_WANT_READ or block on a dead
* peer. A rekey is not a drained channel, so it still polls. */
if (peekRet < 0 && peekRet != WS_REKEYING) {
return WS_FATAL_ERROR;
}
/* Nothing buffered. Poll for the real status. */
if (peekRet <= 0) {
/* poll more data off the wire */
ret = wolfSSH_worker(ssh, NULL);
polled = 1;

View File

@ -6956,6 +6956,72 @@ done:
}
#ifdef WOLFSSH_SFTP
static int s_recvCalls = 0;
/* Counts the polls, so a test can prove the wire was never touched. */
static int CountingIoRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
{
WOLFSSH_UNUSED(ssh);
WOLFSSH_UNUSED(buf);
WOLFSSH_UNUSED(sz);
WOLFSSH_UNUSED(ctx);
s_recvCalls++;
return WS_CBIO_ERR_WANT_READ;
}
/* The SFTP read must not poll a channel that has latched its EOF and been
* drained. No further data can arrive, so the poll cannot finish the message
* and only overwrites the latched WS_EOF with WS_WANT_READ, which the SFTP
* loops read as "come back later" -- or blocks outright on a blocking
* socket. */
static int test_SftpReadEofNoPoll(void)
{
WOLFSSH_CTX* ctx = NULL;
WOLFSSH* ssh = NULL;
WOLFSSH_CHANNEL* ch = NULL;
int result = 0;
int ret;
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
if (ctx == NULL)
return -1620;
wolfSSH_SetIOSend(ctx, DiscardIoSend);
wolfSSH_SetIORecv(ctx, CountingIoRecv);
ssh = wolfSSH_new(ctx);
if (ssh == NULL) { result = -1621; goto done; }
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024);
if (ch == NULL) { result = -1622; goto done; }
if (ChannelAppend(ssh, ch) != WS_SUCCESS) {
ChannelDelete(ch, ssh->ctx->heap);
result = -1623;
goto done;
}
ch->openConfirmed = 1;
ch->peerWindowSz = 1024;
ch->peerMaxPacketSz = 1024;
/* The peer half-closed and everything it sent has been read. */
ch->eofRxd = 1;
s_recvCalls = 0;
ret = wolfSSH_SFTP_read(ssh);
if (ret != WS_FATAL_ERROR) { result = -1624; goto done; }
if (wolfSSH_get_error(ssh) != WS_EOF) { result = -1625; goto done; }
if (s_recvCalls != 0) { result = -1626; goto done; }
done:
s_recvCalls = 0;
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
return result;
}
#endif /* WOLFSSH_SFTP */
/* DoReceive() can retire the head channel mid-read: DoChannelClose() frees it
* while wolfSSH_stream_read() still holds its inputBuffer. If the next head
@ -20002,6 +20068,13 @@ int wolfSSH_UnitTest(int argc, char** argv)
(unitResult == 0 ? "SUCCESS" : "FAILED"));
testResult = testResult || unitResult;
#ifdef WOLFSSH_SFTP
unitResult = test_SftpReadEofNoPoll();
printf("SftpReadEofNoPoll: %s\n",
(unitResult == 0 ? "SUCCESS" : "FAILED"));
testResult = testResult || unitResult;
#endif
unitResult = test_AcceptSurvivesChannelEof();
printf("AcceptSurvivesChannelEof: %s\n",
(unitResult == 0 ? "SUCCESS" : "FAILED"));