wolfSSH_stream_read consumes before crediting and always reports the byte count. - #1188
Open
yosuke-wolfssl wants to merge 2 commits into
Open
wolfSSH_stream_read consumes before crediting and always reports the byte count.#1188yosuke-wolfssl wants to merge 2 commits into
yosuke-wolfssl wants to merge 2 commits into
Conversation
- app_write_all() hands a whole buffer to a local descriptor across partial transfers and returns WS_FATAL_ERROR otherwise; it waits on writability with a bounded try count and checks the select() result. The pty, agent and forwarding writes in ssh_worker() call it, and SOCKET_EAGAIN and SOCKET_EINTR join the socket error macros. - ssh_worker() carries shellBufferIdx and agentBufferIdx beside the existing fwdBufferIdx. A descriptor is read only while its staging buffer is empty and stays out of the read set otherwise; the forwarding recv() fills the buffer from its base. - Each buffer has a flush block, outside the descriptor-state guards, that subtracts what wolfSSH_ChannelIdSend() took and moves the remainder down. WS_CHANNEL_NOT_CONF, WS_CHAN_RXD, WS_WINDOW_FULL and WS_REKEYING hold the data for a later pass. - WS_WANT_WRITE also holds it and sets wantWrite, which adds sshFd to a write set passed to select(). - Echo mode reads the channel into shellCtx.buffer and shares the shell flush block; process_bytes() runs on that buffer. Issue: F-10544
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes data-loss and deadlock scenarios caused by partial writes in the echo server example and incorrect window-credit sequencing in wolfSSH_stream_read(). It also adds targeted regression tests to cover both the stream/window-credit behavior and the short-send retry logic in an integration-style scenario against the echo server.
Changes:
- Update
wolfSSH_stream_read()to consume bytes before window crediting and always return the bytes copied, recording window-adjust send failures inssh->error. - Rework
examples/echoserver/echoserver.cI/O loops to correctly handle partial sends/writes by buffering unsent data and retrying when the channel becomes writable/unblocked. - Add new unit/API tests to validate deferred window adjust behavior and window credit round-tripping with a small receive window.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/ssh.c |
Fixes stream read window-credit sequencing and return/error reporting behavior. |
examples/echoserver/echoserver.c |
Adds robust partial-write handling for shell/agent/forwarding paths and select() wakeups on WANT_WRITE. |
tests/unit.c |
Adds a unit regression test ensuring deferred window credit doesn’t “lose” consumed bytes. |
tests/api.c |
Adds an echoserver-gated API test that validates window credit recovery across a payload larger than the receive window. |
Suppressed comments (1)
examples/echoserver/echoserver.c:1125
- In the agent relay path, agentChannelId is initialized to (word32)-1 and (per a file-wide search) is never updated, but it is passed to wolfSSH_ChannelIdRead()/wolfSSH_ChannelIdSend(). Since those APIs look up channels by self ID, this will consistently fail with WS_INVALID_CHANID and prevent agent forwarding from working. The code likely needs to plumb the correct self channel ID for the auth-agent channel into agentChannelId (or another tracked field) before attempting reads/sends.
if (lastChannel == agentChannelId) {
cnt_r = wolfSSH_ChannelIdRead(ssh, agentChannelId,
threadCtx->channelBuffer,
sizeof threadCtx->channelBuffer);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- wolfSSH_stream_read() advances inputBuffer->idx before _UpdateChannelWindow(), records a non-success result in ssh->error and reports the byte count. - tests/api.c adds test_wolfSSH_stream_read_WindowCredit(), which round-trips 2000 bytes through a 1024-byte client receive window, with its own user-auth and host-key callbacks. - tests/unit.c adds test_stream_read_deferredWindowAdjust(), which puts a full window of channel data and reads it back with an IO send that reports WS_CBIO_ERR_WANT_WRITE, checking the byte count, the payload, ssh->error, the credited window and the consumed buffer.
yosuke-wolfssl
force-pushed
the
fix/f_10544
branch
from
August 21, 2026 00:23
18f189d to
bc65695
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ssh_worker()in the echo server treats every positivewrite(),send(), andwolfSSH_ChannelIdSend()return as a complete transfer.SendChannelData()clampseach send to
min(peerWindowSz, peerMaxPacketSz, maxPacketSz)and returns thatclamped count, so a send larger than the peer's window returns short. Every call
site then overwrites or discards its source buffer — the forwarding path resets
fwdBufferIdxto0on any positive result — and the unsent suffix is lostsilently. Reachable through the shell, agent, and forwarding paths.
Separately,
wolfSSH_stream_read()creditsinputBuffer->idxbytes but advancedidxonly afterwards, so a read never credits its own bytes. A single read thatdrains the entire receive window leaves the window at zero with nothing left to
trigger a credit, wedging the stream permanently. Latent at the default 128 KB
window; immediate with a smaller one.
Fix (
examples/echoserver/echoserver.c)Two mechanisms, split by which side owns the backpressure:
drains, so
app_write_all()advances past each partial transfer and returnsWS_FATAL_ERRORif it cannot complete.process inbound packets. Each direction stages into its
WS_AppCtxbuffer; theflush subtracts what was sent and
WMEMMOVEs the remainder for the next pass.> 0WS_CHANNEL_NOT_CONF,WS_CHAN_RXD,WS_WINDOW_FULL,WS_REKEYINGWS_WANT_WRITEsshFdto a write set soselect()wakes on writabilityA descriptor stays out of the read set while its buffer is pending. Closes f-10544.
Fix (
src/ssh.c)wolfSSH_stream_read()consumes before crediting and always reports the byte count,recording a failed adjust in
ssh->error— the contract_ChannelReadExt()alreadyfollows, so returning an error after consuming cannot drop the caller's bytes.
Tests
test_wolfSSH_stream_read_WindowCredit()(tests/api.c) round-trips 2000 bytesthrough a 1024-byte client receive window against the echo server.
test_stream_read_deferredWindowAdjust()(tests/unit.c) reads a full window withan IO send that reports
WS_CBIO_ERR_WANT_WRITE.Verification
unit,api,testsuite,regress,kex,auth, and thescripts/tests includingfwd.test.-Werroracross six GCC configurations; ASan + UBSan clean.intermediate variant that consumed the bytes but returned the error.