Handle the EOF status in apps and examples

Every in-tree caller of wolfSSH_worker() now recognises a peer half-close.
wolfsshd's shell loop and both echoservers need it: all three ladders end in
"else if (rc != WS_WANT_READ) break", and wolfsshd's reaches
kill(childPid, SIGKILL), so without it a client half-close kills the command
it just finished feeding.

- wolfsshd closes the child's stdin off the channel's own EOF state instead of
  off a worker return of zero, which no longer happens on a half-close.
- The echoservers answer the half-close off wolfSSH_ChannelGetEof() rather
  than the WS_EOF status: the flush inside wolfSSH_worker() can supersede that
  status, and it is raised once. They hand back the backlog first, finish a
  short send, and only send the EOF once the channel is empty. Answering is
  not conditional on the shell build, where an echo session is the default.
- The SFTP loops peek before leaving, so a half-close with requests still
  buffered is served rather than dropped, and they report an ordinary session
  end as success.
- The clients -- examples/client, scpclient, sftpclient, apps/wolfssh -- treat
  it as the graceful case instead of an error. apps/wolfssh counts it as a
  finished flush as well, since one worker pass can drain the queue and
  consume the peer's EOF together.
- portfwd relays it to the local socket with shutdown(SHUT_WR) so a local
  reader waiting on end-of-input returns, once the backlog has genuinely been
  handed over: a read cut short by a rekey leaves the half-close for a later
  pass.
- The Windows half of wolfsshd does not answer with an EOF of its own. That
  latches eofTxd and the child's remaining output would be refused, which is
  the defect this series removes from the library.
- The mplabx port drains before tearing down, the way its SFTP read path
  already did; its worker arm was unreachable for a half-close until now.
pull/1218/head
John Safranek 2026-08-27 13:10:17 -07:00 committed by philljj
parent 2bfdbac2b3
commit ff59c723ec
11 changed files with 432 additions and 39 deletions

View File

@ -338,7 +338,7 @@ static int FlushQueuedSend(WOLFSSH* ssh, wolfSSL_Mutex* lock)
* conversation is for the reader to sort out. A rekey started on the way
* through is the reader's as well, the send itself went out. */
if (ret == WS_WANT_READ || ret == WS_CHAN_RXD || ret == WS_EXTDATA
|| ret == WS_REKEYING) {
|| ret == WS_REKEYING || ret == WS_EOF) {
ret = WS_SUCCESS;
}
@ -1362,14 +1362,17 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args)
ret = WS_SUCCESS;
}
}
else if (ret != WS_CHANNEL_CLOSED && ret != WS_WANT_READ) {
else if (ret != WS_CHANNEL_CLOSED && ret != WS_WANT_READ
&& ret != WS_EOF) {
WLOG(WS_LOG_DEBUG, "Sending the shutdown messages failed.");
}
if (ret == WS_CHANNEL_CLOSED || ret == WS_WANT_READ) {
/* Shutting down. The channel closing isn't a fail, and neither
* is the peer having nothing ready on this non-blocking socket;
* either way there is nothing left to wait for. */
if (ret == WS_CHANNEL_CLOSED || ret == WS_WANT_READ
|| ret == WS_EOF) {
/* Shutting down. The channel closing or the peer's EOF isn't a
* fail, and neither is the peer having nothing ready on this
* non-blocking socket; either way there is nothing left to wait
* for. */
ret = WS_SUCCESS;
}
else if (ret != WS_SUCCESS) {

View File

@ -11,6 +11,7 @@ test_cases=(
"sshd_sftp_idle_cpu_test.sh"
"sshd_scp_fail.sh"
"sshd_term_close_test.sh"
"sshd_stdin_eof_test.sh"
"ssh_kex_algos.sh"
)

View File

@ -0,0 +1,132 @@
#!/bin/bash
# bash, unlike the rest of this directory: the option array and PIPESTATUS
# below both need it.
# A client that half-closes its stdin sends SSH_MSG_CHANNEL_EOF and waits for
# the command to finish. wolfSSHd must close the write end of the child's stdin
# pipe so a command reading to end-of-input returns, and it must hand over
# everything the peer sent before it does.
#
# Needs the OpenSSH client; the wolfSSH example client does not half-close.
if [ -z "$1" ] || [ -z "$2" ]; then
echo "expecting host and port as arguments"
echo "./sshd_stdin_eof_test.sh 127.0.0.1 22222"
exit 1
fi
HOST="$1"
PORT="$2"
USER_NAME="${3:-`whoami`}"
command -v ssh >/dev/null 2>&1 || {
echo "ssh not found, skipping"
exit 77
}
# The RESULT==124 assertions below are the point of the test.
command -v timeout >/dev/null 2>&1 || {
echo "timeout not found, skipping"
exit 77
}
# ssh refuses a group/world readable identity file.
KEYDIR=`mktemp -d 2>/dev/null` || KEYDIR=`mktemp -d -t sshdeof`
if [ -z "$KEYDIR" ] || [ ! -d "$KEYDIR" ]; then
echo "could not create temp dir"
exit 1
fi
trap 'rm -rf "$KEYDIR"' EXIT
cp ../../../keys/hansel-key-ecc.pem "$KEYDIR/id_ecdsa" || exit 1
chmod 600 "$KEYDIR/id_ecdsa"
SSH_OPTS=(-i "$KEYDIR/id_ecdsa" -p "$PORT"
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
-o PreferredAuthentications=publickey -o PasswordAuthentication=no
-o BatchMode=yes -o ConnectTimeout=5 -o LogLevel=ERROR)
# An identity the client cannot load, or a host it cannot reach, is not this
# test's subject. sshd_exec_test.sh runs ahead of this one and owns a server
# that cannot run commands at all.
if ! timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" true >/dev/null 2>&1
then
echo "no session with these options, skipping"
exit 77
fi
# Case 1: a small input through 'sort'. 'sort' emits nothing until
# end-of-input, so a missed EOF is the timeout and a killed or hung child is
# empty output -- neither can pass by winning a race the way a streaming 'cat'
# can. The input is unsorted so the comparison also proves the remote command
# ran rather than the input being echoed back.
printf 'charlie\nalpha\nbravo\n' > "$KEYDIR/in.txt"
# wolfsshd runs the user's login shell, so its startup files can print on
# either stream: stderr goes to a file, and stdout is filtered to the lines
# the command itself produced.
OUT=`timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" 'sort' \
< "$KEYDIR/in.txt" 2> "$KEYDIR/in.err" \
| grep -E '^(alpha|bravo|charlie)$'`
RESULT=${PIPESTATUS[0]}
if [ "$RESULT" == 124 ]; then
echo "session did not end after the client half-closed its stdin"
cat "$KEYDIR/in.err"
exit 1
fi
if [ "$RESULT" != 0 ]; then
echo "ssh failed with $RESULT"
cat "$KEYDIR/in.err"
exit 1
fi
if [ "$OUT" != "`sort "$KEYDIR/in.txt"`" ]; then
echo "unexpected output from the remote command"
echo "$OUT"
cat "$KEYDIR/in.err"
exit 1
fi
# Case 2: the same half-close with the send window full. The reader below
# stalls, so the client stops draining, the server's window to the peer fills
# while the client is still sending, and the EOF arrives with channel data
# still buffered on the server. Data held back that way has to be handed to
# the child before its stdin closes: dropping it truncates the output, and
# never handing it over leaves 'cat' waiting on a stdin that never closes.
awk 'BEGIN { for (i = 0; i < 200000; i++)
printf "%08d one two three four five six seven\n", (i * 48271) % 99991
}' > "$KEYDIR/big.txt"
timeout 90 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" 'cat' \
< "$KEYDIR/big.txt" 2> "$KEYDIR/big.err" \
| { sleep 5; cat; } > "$KEYDIR/big.out"
RESULT=${PIPESTATUS[0]}
if [ "$RESULT" == 124 ]; then
echo "session did not end after a half-close with the window full"
cat "$KEYDIR/big.err"
exit 1
fi
if [ "$RESULT" != 0 ]; then
echo "ssh failed with $RESULT"
cat "$KEYDIR/big.err"
exit 1
fi
SENT=`wc -c < "$KEYDIR/big.txt"`
GOT=`wc -c < "$KEYDIR/big.out"`
if [ "$GOT" -lt "$SENT" ] \
|| ! tail -c "$SENT" "$KEYDIR/big.out" | cmp -s - "$KEYDIR/big.txt"
then
echo "the remote command did not see all of the input"
echo "sent $SENT bytes, got $GOT bytes"
cat "$KEYDIR/big.err"
exit 1
fi
exit 0

View File

@ -908,6 +908,8 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
* if there is still pending sends */
}
if (error == WS_EOF) {
/* An ordinary session end, not a failure. */
ret = 0;
break;
}
}
@ -934,10 +936,19 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
continue;
}
/* Drain what is buffered first. A rekey is not a drained
* channel: peek reports it without looking. */
if (error == WS_EOF) {
break;
int peekRet = wolfSSH_stream_peek(ssh, NULL, 1);
if (peekRet != WS_REKEYING && peekRet <= 0) {
/* An ordinary session end, not a failure. */
ret = 0;
break;
}
}
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD) {
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD
&& ret != WS_EOF) {
/* If not successful and no channel data, leave. */
break;
}
@ -954,8 +965,11 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
error == WS_CHAN_RXD || error == WS_REKEYING ||
error == WS_WINDOW_FULL)
ret = error;
if (error == WS_EOF)
if (error == WS_EOF) {
/* An ordinary session end, not a failure. */
ret = 0;
break;
}
continue;
}
else if (ret == WS_REKEYING) {
@ -964,8 +978,11 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
}
else if (ret < 0) {
error = wolfSSH_get_error(ssh);
if (error == WS_EOF)
if (error == WS_EOF) {
/* An ordinary session end, not a failure. */
ret = 0;
break;
}
}
else {
/* Channel is live with nothing buffered. Let the next select
@ -1331,6 +1348,19 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
else if (rc == WS_CHANNEL_CLOSED) {
continue;
}
else if (rc == WS_EOF) {
/* The peer is done sending. No EOF of ours here: it
* latches eofTxd and the child's remaining console
* output would then be refused, which both send sites
* below treat as fatal. wolfSSH_shutdown() sends it at
* teardown, as the POSIX copy relies on. Closing the
* write end of the child's stdin is still owed on this
* platform, and so is the per-pass drain: ptyIn is the
* terminal-resize context, and this copy reads into
* shellBuffer, which the windowFull resend owes the
* peer. Both want fixing where they can be tested. */
continue;
}
else if (rc != WS_WANT_READ) {
break;
}
@ -1524,8 +1554,10 @@ static int SHELL_FlushOut(WOLFSSH* ssh, WS_SOCKET_T sshFd, word32 channelId,
if (wolfSSH_worker(ssh, NULL) < 0) {
int err = wolfSSH_get_error(ssh);
/* A peer EOF during the final flush is expected. */
if (err != WS_WANT_READ && err != WS_WANT_WRITE &&
err != WS_CHAN_RXD && err != WS_REKEYING) {
err != WS_CHAN_RXD && err != WS_REKEYING &&
err != WS_EOF) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Issue draining connection on final flush");
return -1;
@ -1830,11 +1862,9 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
word32 shellChannelId = 0;
WOLFSSH_CHANNEL* shellChannel;
/* Name the session channel off the channel list rather than trusting
* DEFAULT_NEXT_CHANNEL to be 0, which a build can override. It is the
* only channel open at this point; the agent channel comes later. The
* loop below closes the child's stdin off this channel, so a wrong id
* there drops the peer's input instead of handing it over. */
/* Off the channel list, not DEFAULT_NEXT_CHANNEL, which a build can
* override. The session channel is the only one open here. lastRxId is
* no use: no request path sets it. A wrong id drops the peer's input. */
shellChannel = wolfSSH_ChannelNext(ssh, NULL);
if (shellChannel == NULL || wolfSSH_ChannelGetId(shellChannel,
&shellChannelId, WS_CHANNEL_ID_SELF) != WS_SUCCESS) {
@ -1966,6 +1996,9 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
peerConnected = 0;
continue;
}
else if (rc == WS_EOF) {
/* Half-close, handled below. */
}
else if (rc == WS_WANT_WRITE) {
wantWrite = 1;
continue;
@ -2722,7 +2755,7 @@ static void* HandleConnection(void* arg)
error = wolfSSH_get_error(ssh);
/* peer successfully closed down gracefully */
if (ret == WS_CHANNEL_CLOSED) {
if (ret == WS_CHANNEL_CLOSED || ret == WS_EOF) {
ret = 0;
break;
}

View File

@ -1179,7 +1179,7 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args)
if (ret <= 0) {
ret = wolfSSH_get_error(ssh);
if (ret != WS_WANT_READ && ret != WS_WANT_WRITE &&
ret != WS_CHAN_RXD) {
ret != WS_CHAN_RXD && ret != WS_EOF) {
ClientFreeBuffers(pubKeyName, privKeyName, NULL);
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
@ -1198,7 +1198,9 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args)
#endif
}
ret = wolfSSH_shutdown(ssh);
/* do not continue on with shutdown process if peer already disconnected */
/* do not continue on with shutdown process if peer already disconnected.
* A peer EOF is not a disconnect: the channel is still open and its close
* is still owed, so the drain below is exactly what is wanted. */
if (ret != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E
&& wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) {
if (ret != WS_SUCCESS) {
@ -1209,7 +1211,7 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args)
}
ret = wolfSSH_worker(ssh, NULL);
if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E &&
ret != WS_CHANNEL_CLOSED) {
ret != WS_CHANNEL_CLOSED && ret != WS_EOF) {
ClientFreeBuffers(pubKeyName, privKeyName, NULL);
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
@ -1226,7 +1228,7 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args)
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E &&
ret != WS_CHANNEL_CLOSED) {
ret != WS_CHANNEL_CLOSED && ret != WS_EOF) {
err_sys("Closing client stream failed");
}

View File

@ -805,6 +805,10 @@ static int ssh_worker(thread_ctx_t* threadCtx)
WOLFSSH* ssh;
WS_SOCKET_T sshFd;
int rc = 0;
int eofAnswered = 0;
/* Without a shell there is no child to outlive the peer's EOF, and the
* read path echoes unconditionally. */
int echoOnly = 1;
#ifdef WOLFSSH_SHELL
const char *userName;
struct passwd *p_passwd;
@ -822,6 +826,10 @@ static int ssh_worker(thread_ctx_t* threadCtx)
if (ssh == NULL)
return WS_FATAL_ERROR;
#ifdef WOLFSSH_SHELL
echoOnly = threadCtx->echo;
#endif
sshFd = wolfSSH_get_fd(ssh);
#if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ)
@ -997,6 +1005,57 @@ static int ssh_worker(thread_ctx_t* threadCtx)
channel. The additional channel is only used with the
agent. */
cnt_r = wolfSSH_worker(ssh, &lastChannel);
/* The peer is done sending: hand back the backlog and answer
* its EOF, or a client that half-closed waits on a server
* that never finishes -- the library no longer answers for
* us. Off the channel's own state, not the WS_EOF status: the
* flush inside wolfSSH_worker() can supersede that, and it is
* raised once. Echo mode only; a shell child on a pty is
* still producing, so its EOF waits for the child to exit. */
if (!eofAnswered && echoOnly) {
WOLFSSH_CHANNEL* eofChannel;
eofChannel = wolfSSH_ChannelFind(ssh,
threadCtx->shellCtx.channelId, WS_CHANNEL_ID_SELF);
if (eofChannel != NULL
&& wolfSSH_ChannelGetEof(eofChannel)) {
int eofRead;
int eofSent;
int eofOff;
do {
eofRead = wolfSSH_ChannelIdRead(ssh,
threadCtx->shellCtx.channelId,
threadCtx->channelBuffer,
sizeof threadCtx->channelBuffer);
eofOff = 0;
/* A send is bounded by the peer's window and
* packet size, so a short one is normal and the
* rest of the chunk is still owed. */
while (eofOff < eofRead) {
eofSent = wolfSSH_ChannelIdSend(ssh,
threadCtx->shellCtx.channelId,
threadCtx->channelBuffer + eofOff,
eofRead - eofOff);
if (eofSent <= 0)
break;
eofOff += eofSent;
}
if (eofOff < eofRead)
break;
} while (eofRead > 0);
/* Only an emptied channel earns the EOF; anything
* else is retried on a later pass. */
if (eofRead == 0) {
wolfSSH_ChannelSendEof(eofChannel);
eofAnswered = 1;
ChildRunning = 0;
}
}
}
if (cnt_r < 0) {
rc = wolfSSH_get_error(ssh);
/* wolfSSH_worker() reports WS_REKEYING in place of
@ -1117,6 +1176,11 @@ static int ssh_worker(thread_ctx_t* threadCtx)
#endif
continue;
}
else if (rc == WS_EOF) {
/* The half-close is answered by the durable check
* above, which has already run this pass. */
continue;
}
else if (rc != WS_WANT_READ) {
#ifdef SHELL_DEBUG
printf("Break:read sshFd returns %d: errno =%x\n",
@ -1439,6 +1503,8 @@ static int sftp_worker(thread_ctx_t* threadCtx)
* if there is still pending sends */
}
if (error == WS_EOF) {
/* An ordinary session end, not a failure. */
ret = 0;
break;
}
}
@ -1465,10 +1531,18 @@ static int sftp_worker(thread_ctx_t* threadCtx)
ret = error;
}
/* Drain what is buffered before leaving on the EOF. */
if (error == WS_EOF) {
break;
/* A rekey is not a drained channel. */
int peekRet = wolfSSH_stream_peek(ssh, NULL, 1);
if (peekRet != WS_REKEYING && peekRet <= 0) {
/* An ordinary session end, not a failure. */
ret = 0;
break;
}
}
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD) {
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_EOF) {
#ifdef WOLFSSH_TEST_BLOCK
if (error == WS_WANT_READ) {
while (error == WS_WANT_READ) {
@ -1504,8 +1578,10 @@ static int sftp_worker(thread_ctx_t* threadCtx)
error == WS_CHAN_RXD || error == WS_REKEYING ||
error == WS_WINDOW_FULL)
ret = error;
if (error == WS_EOF)
if (error == WS_EOF) {
ret = 0;
break;
}
continue;
}
else if (ret == WS_REKEYING) {
@ -1675,6 +1751,13 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs)
ret = 0;
}
/* The peer's close already retired the channel: a completed
* shutdown, not a failure. Left non-zero it sets quit, taking the
* server down after one session. */
if (ret == WS_CHANNEL_CLOSED) {
ret = 0;
}
error = wolfSSH_get_error(threadCtx->ssh);
if (error != WS_SOCKET_ERROR_E &&
(error == WS_WANT_READ || error == WS_WANT_WRITE)) {
@ -1686,7 +1769,7 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs)
error = wolfSSH_get_error(threadCtx->ssh);
/* peer successfully closed down gracefully */
if (ret == WS_CHANNEL_CLOSED) {
if (ret == WS_CHANNEL_CLOSED || ret == WS_EOF) {
ret = 0;
break;
}

View File

@ -431,6 +431,7 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args)
int ret;
int ch;
int appFdSet = 0;
int appFdHalfClosed = 0;
int reverse = 0;
int fwdFromPortSet = 0;
PortfwdState fwdState;
@ -641,7 +642,8 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args)
ret = wolfSSH_worker(ssh, NULL);
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD &&
ret != WS_WANT_READ && ret != WS_WANT_WRITE &&
ret != WS_WINDOW_FULL && ret != WS_REKEYING)
ret != WS_WINDOW_FULL && ret != WS_REKEYING &&
ret != WS_EOF)
err_sys("Couldn't get the remote forward reply.");
}
if (!fwdState.replied)
@ -765,6 +767,44 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args)
break;
}
/* Relay the half-close so a local reader waiting on end-of-input
* returns; nothing else relays it. Driven off the latched channel
* state, not the WS_EOF status: the flush inside wolfSSH_worker()
* can supersede that, and it is raised only once. Only the channel
* appFd is wired to, since half-closing the wrong socket truncates
* a live transfer. */
if (appFdSet && fwdChannel != NULL && !appFdHalfClosed
&& wolfSSH_ChannelGetEof(fwdChannel)) {
int drained;
/* Hand over the backlog first, or the local reader sees a
* clean end-of-input short of what the peer sent. A negative
* read is a rekey or a stalled channel, not a drained one, so
* only an empty read earns the half-close; the latch stays
* clear and a later pass tries again. */
do {
drained = wolfSSH_ChannelRead(fwdChannel, sshBuffer,
sshBufferSz);
if (drained > 0) {
if ((int)send(appFd, sshBuffer, drained, 0) != drained)
break;
}
} while (drained > 0);
if (drained == 0) {
appFdHalfClosed = 1;
#ifdef SHUT_WR
shutdown(appFd, SHUT_WR);
#elif defined(SD_SEND)
shutdown(appFd, SD_SEND);
#else
printf("No way to half-close the local socket, "
"the local reader may wait for input that is "
"not coming.\n");
#endif
}
}
if (ret == WS_CHAN_RXD) {
WOLFSSH_CHANNEL* readChannel;

View File

@ -317,7 +317,9 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args)
}
ret = wolfSSH_shutdown(ssh);
/* do not continue on with shutdown process if peer already disconnected */
/* do not continue on with shutdown process if peer already disconnected.
* A peer EOF is not a disconnect: the channel is still open and its close
* is still owed, so the drain below is exactly what is wanted. */
if (ret != WS_CHANNEL_CLOSED && ret != WS_SOCKET_ERROR_E &&
wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E &&
wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) {
@ -326,7 +328,8 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args)
}
else {
ret = wolfSSH_worker(ssh, NULL);
if (ret != WS_SUCCESS && ret != WS_CHANNEL_CLOSED) {
if (ret != WS_SUCCESS && ret != WS_CHANNEL_CLOSED
&& ret != WS_EOF) {
WLOG(WS_LOG_DEBUG,
"Failed to listen for close messages from the peer.");
}
@ -336,7 +339,7 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args)
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E &&
ret != WS_CHANNEL_CLOSED) {
ret != WS_CHANNEL_CLOSED && ret != WS_EOF) {
WLOG(WS_LOG_DEBUG,
"Closing scp stream failed. Connection could have been closed by peer");
}
@ -346,7 +349,8 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args)
wc_ecc_fp_free(); /* free per thread cache */
#endif
if ((ret != WS_SUCCESS) && (ret != WS_CHANNEL_CLOSED))
if ((ret != WS_SUCCESS) && (ret != WS_CHANNEL_CLOSED)
&& (ret != WS_EOF))
((func_args*)args)->return_code = 1;
return 0;
}

View File

@ -1501,7 +1501,7 @@ static int doAutopilot(int cmd, char* local, char* remote)
/* wolfSSH_worker returns WS_FATAL_ERROR when the socket
* would block (DoReceive -> GetInputData -> WS_WANT_READ),
* so check ssh->error rather than ret for blocking conditions. */
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD &&
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_EOF &&
wolfSSH_get_error(ssh) != WS_WANT_READ &&
wolfSSH_get_error(ssh) != WS_WANT_WRITE)
break;
@ -1815,7 +1815,9 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args)
int err;
ret = wolfSSH_shutdown(ssh);
/* peer hung up or channel already closed, stop trying */
/* peer hung up or channel already closed, stop trying.
* wolfSSH_shutdown() folds a peer EOF into WS_SUCCESS itself, so
* there is no WS_EOF to test for here. */
if (ret == WS_SOCKET_ERROR_E || ret == WS_ERROR ||
ret == WS_CHANNEL_CLOSED) {
ret = 0;
@ -1832,7 +1834,7 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args)
err = wolfSSH_get_error(ssh);
/* peer successfully closed down gracefully */
if (ret == WS_CHANNEL_CLOSED) {
if (ret == WS_CHANNEL_CLOSED || ret == WS_EOF) {
ret = 0;
break;
}

View File

@ -793,6 +793,10 @@ static int ssh_worker(thread_ctx_t* threadCtx)
WOLFSSH* ssh;
WS_SOCKET_T sshFd;
int rc = 0;
int eofAnswered = 0;
/* Without a shell there is no child to outlive the peer's EOF, and the
* read path echoes unconditionally. */
int echoOnly = 1;
#ifdef WOLFSSH_SHELL
const char *userName;
struct passwd *p_passwd;
@ -810,6 +814,10 @@ static int ssh_worker(thread_ctx_t* threadCtx)
if (ssh == NULL)
return WS_FATAL_ERROR;
#ifdef WOLFSSH_SHELL
echoOnly = threadCtx->echo;
#endif
sshFd = wolfSSH_get_fd(ssh);
#if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ)
@ -981,6 +989,57 @@ static int ssh_worker(thread_ctx_t* threadCtx)
channel. The additional channel is only used with the
agent. */
cnt_r = wolfSSH_worker(ssh, &lastChannel);
/* The peer is done sending: hand back the backlog and answer
* its EOF, or a client that half-closed waits on a server
* that never finishes -- the library no longer answers for
* us. Off the channel's own state, not the WS_EOF status: the
* flush inside wolfSSH_worker() can supersede that, and it is
* raised once. Echo mode only; a shell child on a pty is
* still producing, so its EOF waits for the child to exit. */
if (!eofAnswered && echoOnly) {
WOLFSSH_CHANNEL* eofChannel;
eofChannel = wolfSSH_ChannelFind(ssh, shellChannelId,
WS_CHANNEL_ID_SELF);
if (eofChannel != NULL
&& wolfSSH_ChannelGetEof(eofChannel)) {
int eofRead;
int eofSent;
int eofOff;
do {
eofRead = wolfSSH_ChannelIdRead(ssh,
shellChannelId,
threadCtx->channelBuffer,
sizeof threadCtx->channelBuffer);
eofOff = 0;
/* A send is bounded by the peer's window and
* packet size, so a short one is normal and the
* rest of the chunk is still owed. */
while (eofOff < eofRead) {
eofSent = wolfSSH_ChannelIdSend(ssh,
shellChannelId,
threadCtx->channelBuffer + eofOff,
eofRead - eofOff);
if (eofSent <= 0)
break;
eofOff += eofSent;
}
if (eofOff < eofRead)
break;
} while (eofRead > 0);
/* Only an emptied channel earns the EOF; anything
* else is retried on a later pass. */
if (eofRead == 0) {
wolfSSH_ChannelSendEof(eofChannel);
eofAnswered = 1;
ChildRunning = 0;
}
}
}
if (cnt_r < 0) {
rc = wolfSSH_get_error(ssh);
if (rc == WS_CHAN_RXD) {
@ -1082,6 +1141,11 @@ static int ssh_worker(thread_ctx_t* threadCtx)
#endif
continue;
}
else if (rc == WS_EOF) {
/* The half-close is answered by the durable check
* above, which has already run this pass. */
continue;
}
else if (rc != WS_WANT_READ) {
#ifdef SHELL_DEBUG
printf("Break:read sshFd returns %d: errno =%x\n",
@ -1379,6 +1443,8 @@ static int sftp_worker(thread_ctx_t* threadCtx)
* if there is still pending sends */
}
if (error == WS_EOF) {
/* An ordinary session end, not a failure. */
ret = 0;
break;
}
}
@ -1405,10 +1471,18 @@ static int sftp_worker(thread_ctx_t* threadCtx)
ret = error;
}
/* Drain what is buffered before leaving on the EOF. */
if (error == WS_EOF) {
break;
/* A rekey is not a drained channel. */
int peekRet = wolfSSH_stream_peek(ssh, NULL, 1);
if (peekRet != WS_REKEYING && peekRet <= 0) {
/* An ordinary session end, not a failure. */
ret = 0;
break;
}
}
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD) {
if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_EOF) {
if (ret == WS_WANT_WRITE) {
/* recall wolfSSH_worker here because is likely our custom
* highwater callback that returned up a WS_WANT_WRITE */
@ -1431,8 +1505,10 @@ static int sftp_worker(thread_ctx_t* threadCtx)
error == WS_CHAN_RXD || error == WS_REKEYING ||
error == WS_WINDOW_FULL)
ret = error;
if (error == WS_EOF)
if (error == WS_EOF) {
ret = 0;
break;
}
continue;
}
else if (ret == WS_REKEYING) {
@ -1441,8 +1517,11 @@ static int sftp_worker(thread_ctx_t* threadCtx)
}
else if (ret < 0) {
error = wolfSSH_get_error(ssh);
if (error == WS_EOF)
if (error == WS_EOF) {
/* shutdown is happening, clear peek error */
ret = 0;
break;
}
}
if (ret == WS_FATAL_ERROR && error == 0) {
@ -1574,6 +1653,13 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs)
ret = 0;
}
/* The peer's close already retired the channel: a completed
* shutdown, not a failure. Left non-zero it sets quit, taking the
* server down after one session. */
if (ret == WS_CHANNEL_CLOSED) {
ret = 0;
}
error = wolfSSH_get_error(threadCtx->ssh);
if (error != WS_SOCKET_ERROR_E &&
(error == WS_WANT_READ || error == WS_WANT_WRITE)) {
@ -1585,7 +1671,7 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs)
error = wolfSSH_get_error(threadCtx->ssh);
/* peer succesfully closed down gracefully */
if (ret == WS_CHANNEL_CLOSED) {
if (ret == WS_CHANNEL_CLOSED || ret == WS_EOF) {
ret = 0;
break;
}

View File

@ -841,8 +841,15 @@ void APP_Tasks ( void )
break;
}
/* Drain what the peer sent before tearing down. A rekey is not
* a drained channel: peek reports it without looking. */
if (error == WS_EOF) {
appData.state = APP_SSH_CLEANUP;
int peekRet = wolfSSH_stream_peek(ssh, peek_buf,
sizeof(peek_buf));
if (peekRet != WS_REKEYING && peekRet <= 0) {
appData.state = APP_SSH_CLEANUP;
}
break;
}