ssh: report the owed write when the worker leaves output queued

- wolfSSH_shutdown() sets ret to WS_WANT_WRITE whenever the
  wolfSSH_worker() call it makes ends with output pending. The
  sendErr local and the arm that returned wolfSSH_get_error() are
  gone.
- tests/unit.c gains EofDisconnectCb(), which sends a disconnect
  from the channel EOF dispatch, and two tests driving it:
  test_ShutdownOwedWriteAfterEofDisconnect() asserts
  wolfSSH_shutdown() returns WS_WANT_WRITE with output pending and
  WS_SUCCESS with the buffer drained on the retry;
  test_ShutdownHardEofDisconnectSurfacesOnRetry() refuses the
  disconnect's send outright and asserts the retry returns
  WS_SOCKET_ERROR_E.
pull/1252/head
Yosuke Shimizu 2026-09-15 11:20:03 +09:00
parent 399c83b1b6
commit c88b2582b8
2 changed files with 156 additions and 8 deletions

View File

@ -1291,15 +1291,9 @@ int wolfSSH_shutdown(WOLFSSH* ssh)
/* received response */
ret = WS_SUCCESS;
}
/* Report a write still owed, or the send's own error if it failed. */
/* The worker left output queued, so a write is still owed. */
if (ret == WS_SUCCESS && wolfSSH_OutputPending(ssh)) {
int sendErr = wolfSSH_get_error(ssh);
if (sendErr == WS_WANT_WRITE || sendErr == WS_WANT_READ
|| sendErr == WS_SUCCESS)
ret = WS_WANT_WRITE;
else
ret = sendErr;
ret = WS_WANT_WRITE;
}
}

View File

@ -9326,6 +9326,150 @@ static int test_ChannelEofCallback(void)
if (ret == WS_EOF) { result = -1718; goto done; }
if (s_eofCbCalls != 1) { result = -1719; goto done; }
done:
s_recvPkt = NULL;
s_recvPktSz = 0;
s_recvPktOff = 0;
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
return result;
}
/* Disconnects from inside the EOF dispatch. DoChannelEof() discards what
* this returns, so the refused send's status is not an error to report. */
static int EofDisconnectCb(WOLFSSH_CHANNEL* channel, void* ctx)
{
WOLFSSH* ssh = (WOLFSSH*)ctx;
(void)channel;
(void)wolfSSH_SendDisconnect(ssh, WOLFSSH_DISCONNECT_BY_APPLICATION);
return WS_SUCCESS;
}
/* The queued DISCONNECT sets ssh->disconnected, so wolfSSH_worker() skips its
* flush and the pass returns WS_EOF with output still owed. The write outranks
* that event: the teardown reports WS_WANT_WRITE and the retry sends it. */
static int test_ShutdownOwedWriteAfterEofDisconnect(void)
{
WOLFSSH_CTX* ctx = NULL;
WOLFSSH* ssh = NULL;
WOLFSSH_CHANNEL* ch = NULL;
int result = 0;
int ret;
byte pkt[16];
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
if (ctx == NULL)
return -1900;
wolfSSH_SetIOSend(ctx, WantWriteIoSend);
wolfSSH_SetIORecv(ctx, PacketIoRecv);
if (wolfSSH_CTX_SetChannelEofCb(ctx, EofDisconnectCb) != WS_SUCCESS) {
result = -1901;
goto done;
}
ssh = wolfSSH_new(ctx);
if (ssh == NULL) { result = -1902; goto done; }
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
if (wolfSSH_SetChannelEofCtx(ssh, ssh) != WS_SUCCESS) {
result = -1903;
goto done;
}
ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024);
if (ch == NULL) { result = -1904; goto done; }
if (ChannelAppend(ssh, ch) != WS_SUCCESS) {
ChannelDelete(ch, ssh->ctx->heap);
result = -1905;
goto done;
}
ch->openConfirmed = 1;
ch->peerWindowSz = 1024;
ch->peerMaxPacketSz = 1024;
/* The teardown sends are already done, so the read is all that is left
* and the output buffer is empty going into it. */
ch->eofTxd = 1;
ch->closeTxd = 1;
s_recvPkt = pkt;
s_recvPktSz = BuildChannelEofPacket(pkt, ch->channel);
s_recvPktOff = 0;
ret = wolfSSH_shutdown(ssh);
if (ret != WS_WANT_WRITE) { result = -1906; goto done; }
if (!wolfSSH_OutputPending(ssh)) { result = -1907; goto done; }
/* And the disconnect really does go out once the socket takes bytes. */
wolfSSH_SetIOSend(ctx, DiscardIoSend);
ret = wolfSSH_shutdown(ssh);
if (ret != WS_SUCCESS) { result = -1908; goto done; }
if (wolfSSH_OutputPending(ssh)) { result = -1909; goto done; }
done:
s_recvPkt = NULL;
s_recvPktSz = 0;
s_recvPktOff = 0;
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
return result;
}
/* The owed write is what the first call reports even when the disconnect's
* send failed outright, and the retry's flush surfaces the transport error. */
static int test_ShutdownHardEofDisconnectSurfacesOnRetry(void)
{
WOLFSSH_CTX* ctx = NULL;
WOLFSSH* ssh = NULL;
WOLFSSH_CHANNEL* ch = NULL;
int result = 0;
int ret;
byte pkt[16];
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
if (ctx == NULL)
return -1910;
wolfSSH_SetIOSend(ctx, ConnResetIoSend);
wolfSSH_SetIORecv(ctx, PacketIoRecv);
if (wolfSSH_CTX_SetChannelEofCb(ctx, EofDisconnectCb) != WS_SUCCESS) {
result = -1911;
goto done;
}
ssh = wolfSSH_new(ctx);
if (ssh == NULL) { result = -1912; goto done; }
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
if (wolfSSH_SetChannelEofCtx(ssh, ssh) != WS_SUCCESS) {
result = -1913;
goto done;
}
ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024);
if (ch == NULL) { result = -1914; goto done; }
if (ChannelAppend(ssh, ch) != WS_SUCCESS) {
ChannelDelete(ch, ssh->ctx->heap);
result = -1915;
goto done;
}
ch->openConfirmed = 1;
ch->peerWindowSz = 1024;
ch->peerMaxPacketSz = 1024;
ch->eofTxd = 1;
ch->closeTxd = 1;
s_recvPkt = pkt;
s_recvPktSz = BuildChannelEofPacket(pkt, ch->channel);
s_recvPktOff = 0;
ret = wolfSSH_shutdown(ssh);
if (ret != WS_WANT_WRITE) { result = -1916; goto done; }
if (!wolfSSH_OutputPending(ssh)) { result = -1917; goto done; }
/* The reset is not lost: it takes the return one call later. */
ret = wolfSSH_shutdown(ssh);
if (ret != WS_SOCKET_ERROR_E) { result = -1918; goto done; }
done:
s_recvPkt = NULL;
s_recvPktSz = 0;
@ -22852,6 +22996,16 @@ int wolfSSH_UnitTest(int argc, char** argv)
printf("ChannelEofCallback: %s\n",
(unitResult == 0 ? "SUCCESS" : "FAILED"));
testResult = testResult || unitResult;
unitResult = test_ShutdownOwedWriteAfterEofDisconnect();
printf("ShutdownOwedWriteAfterEofDisconnect: %s\n",
(unitResult == 0 ? "SUCCESS" : "FAILED"));
testResult = testResult || unitResult;
unitResult = test_ShutdownHardEofDisconnectSurfacesOnRetry();
printf("ShutdownHardEofDisconnectSurfacesOnRetry: %s\n",
(unitResult == 0 ? "SUCCESS" : "FAILED"));
testResult = testResult || unitResult;
#endif /* NO_WOLFSSH_SERVER */
#ifndef NO_WOLFSSH_SERVER