Report the disconnect with no channel to drop

wolfSSH_shutdown() set ssh->error to WS_DISCONNECT only inside the
channel branch, so a flush that emptied the buffer with the channel
already retired left behind the WS_WANT_WRITE that queued it. echoserver
and sftpclient read that error and burn ten wolfSSH_worker() calls on a
write that is already done. TestShutdownFlushesWithNoChannel asserts it.

Issue: F-8837
pull/1192/head
John Safranek 2026-08-26 14:34:49 -07:00 committed by philljj
parent a7fda8f6a3
commit d05978f00d
2 changed files with 10 additions and 4 deletions

View File

@ -1161,13 +1161,16 @@ int wolfSSH_shutdown(WOLFSSH* ssh)
* and the wait for a close that will not come. RFC 4253 section 11.1. */
if (channel != NULL && ssh->disconnected) {
WLOG(WS_LOG_DEBUG, "Session already disconnected, nothing to send");
/* An unfinished flush owns ssh->error. Callers gate their retry on
* WS_WANT_WRITE, so overwriting it strands the queued disconnect. */
if (flushRet == WS_SUCCESS)
ssh->error = WS_DISCONNECT;
channel = NULL;
}
/* Report the dead session with or without a channel to drop: callers
* gate their retry on ssh->error, and the flush above may have just
* emptied the output buffer they would be retrying for. An unfinished
* flush owns the error instead, since that retry is still owed. */
if (ssh != NULL && ssh->disconnected && flushRet == WS_SUCCESS)
ssh->error = WS_DISCONNECT;
/* if channel close was not already sent then send it */
if (channel != NULL && !channel->closeTxd) {
if (ret == WS_SUCCESS) {

View File

@ -3583,6 +3583,9 @@ static void TestShutdownFlushesWithNoChannel(void)
AssertIntEQ(ret, WS_CHANNEL_CLOSED);
AssertFalse(wolfSSH_OutputPending(ssh));
AssertIntEQ(out[LENGTH_SZ + 1], MSGID_DISCONNECT);
/* The flush finished, so the WS_WANT_WRITE that queued it is stale.
* Leaving it sends the caller back for a write that is already done. */
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);