Report a disconnect from stream_peek

wolfSSH_stream_peek() is how the shell loops decide whether a channel is
drained. It had no disconnect check, so a dead session looked exactly like
a drained one: zero bytes available, nothing to tell them apart.

- Report WS_DISCONNECT once the buffered data runs dry, the same shape
  wolfSSH_stream_read() uses. What is still buffered comes back first.
- ssh.h and internal.h name peek alongside the read call, and no longer
  claim the read side is ungated outright.
- regress.c: peek sees the buffered byte, then sees the disconnect.

Raised from the channel-eof branch, where peek becomes the drain gate for
the wolfsshd and echoserver shell loops.

Issue: F-8837
pull/1192/head
John Safranek 2026-08-21 15:20:33 -07:00 committed by philljj
parent 285e0409cf
commit bac3c8ba37
4 changed files with 29 additions and 8 deletions

View File

@ -1198,6 +1198,7 @@ int wolfSSH_TriggerKeyExchange(WOLFSSH* ssh)
int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz)
{
WOLFSSH_BUFFER* inputBuffer;
word32 avail;
WLOG(WS_LOG_DEBUG, "Entering wolfSSH_stream_peek()");
@ -1214,11 +1215,22 @@ int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz)
}
inputBuffer = &ssh->channelList->inputBuffer;
bufSz = min(bufSz, inputBuffer->length - inputBuffer->idx);
avail = inputBuffer->length - inputBuffer->idx;
/* Report the disconnect only once the buffered data is drained, the
* same way wolfSSH_stream_read() does. Callers use this to tell a
* drained channel from one with more to come, and a dead session is
* neither. */
if (avail == 0 && ssh->disconnected) {
ssh->error = WS_DISCONNECT;
return WS_FATAL_ERROR;
}
bufSz = min(bufSz, avail);
if (buf != NULL) {
WMEMCPY(buf, inputBuffer->buffer + inputBuffer->idx, bufSz);
}
return bufSz;
return (int)bufSz;
}

View File

@ -3004,12 +3004,21 @@ static void TestDisconnectDrainsBufferedData(void)
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
AssertTrue(ssh->disconnected);
/* Peek is the drain gate the shell loops use, so it has to tell a
* channel with data left from a session that is over. */
ret = wolfSSH_stream_peek(ssh, NULL, 1);
AssertIntEQ(ret, 1);
WMEMSET(data, 0, sizeof(data));
ret = wolfSSH_stream_read(ssh, data, sizeof(data));
AssertIntEQ(ret, (int)sizeof(payload));
AssertIntEQ(WMEMCMP(data, payload, sizeof(payload)), 0);
/* Buffer is dry now, so the disconnect is what is left to report. */
ret = wolfSSH_stream_peek(ssh, NULL, 1);
AssertIntEQ(ret, WS_FATAL_ERROR);
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
ret = wolfSSH_stream_read(ssh, data, sizeof(data));
AssertIntEQ(ret, WS_FATAL_ERROR);
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);

View File

@ -1103,10 +1103,10 @@ struct WOLFSSH {
byte connReset;
byte isClosed;
/* Set when a DISCONNECT is sent or received. Gates every public send
* call, so
* nothing more goes out. The read calls are not gated: data that
* arrived before the disconnect can still be drained. wolfSSH_worker()
* is not gated either, since the shutdown paths still pump it. */
* call, so nothing more goes out. Reads still hand back what arrived
* before the disconnect; the head-of-list reads report it once their
* buffer runs dry. wolfSSH_worker() is not gated, the shutdown paths
* pump it. */
byte disconnected;
byte clientOpenSSH;

View File

@ -562,8 +562,8 @@ WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh);
* every send call in this header, above this comment and below it,
* reports WS_DISCONNECT from then on. Reads are not
* gated, so channel data that arrived before the disconnect can still be
* drained; wolfSSH_stream_read() reports WS_DISCONNECT once its buffer
* runs dry. RFC 4253 section 11.1. */
* drained; wolfSSH_stream_read() and wolfSSH_stream_peek() report
* WS_DISCONNECT once their buffer runs dry. RFC 4253 section 11.1. */
WOLFSSH_API int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz);
WOLFSSH_API int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz);
WOLFSSH_API int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz);