ssh.c: don't fail a channel read whose window credit is deferred - #1192
ssh.c: don't fail a channel read whose window credit is deferred#1192yosuke-wolfssl wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request fixes a correctness issue in wolfSSH’s channel read paths where a deferred CHANNEL_WINDOW_ADJUST send (e.g., WS_WANT_WRITE on non-blocking sockets) could cause the read API to report failure even after bytes were already delivered/consumed, leading callers to prematurely tear down connections.
Changes:
- Update
wolfSSH_stream_read()and the internal_ChannelRead()path (used bywolfSSH_ChannelIdRead()/wolfSSH_ChannelRead()) to always return the number of bytes copied/consumed, decoupling that from the window-adjust send result. - Record non-success window-adjust send results in
ssh->error(and log hard failures), matching the “bytes delivered vs. credit flushed” split already used by_ChannelReadExt(). - Add targeted unit tests covering both affected read entry points under a
WS_WANT_WRITEsend scenario.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/ssh.c |
Ensures channel/stdout read APIs report bytes read even when window-adjust send is deferred, while still surfacing the deferred/failed credit via ssh->error. |
tests/unit.c |
Adds unit tests validating correct byte reporting, payload integrity, local window crediting, and ssh->error behavior under deferred window-adjust sends. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1192
Scan targets checked: wolfssh-bugs, wolfssh-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
- wolfSSH_stream_read() advances inputBuffer->idx before _UpdateChannelWindow() and returns the byte count, recording a non-success adjust result in ssh->error and logging anything other than WS_WANT_WRITE. - _ChannelRead() returns the bytes copied, records the adjust result the same way, and clears a stale WS_WANT_WRITE when the adjust goes out with the output buffer drained. - The wolfSSH_stream_read() block comment and a new note above its wolfssh/ssh.h declaration state that wolfSSH_get_error() carries the window-adjust status on a successful read. - tests/unit.c adds test_stream_read_deferredWindowAdjust() and test_ChannelIdRead_deferredWindowAdjust(), each reading a full window through an IO send that reports WS_CBIO_ERR_WANT_WRITE and then through one that fails, checking the byte count, the payload, ssh->error, the window credit and the drained input buffer. - The wolfSSH_ChannelIdRead() test ends on a credit that sends cleanly, asserting ssh->error returns to WS_SUCCESS and no credit stays owed.
8352ae7 to
ca90ef2
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1192
Scan targets checked: wolfssh-bugs, wolfssh-src
Fenrir result: Approved ✅
No new issues found in the changed files.
Advisory only — this automated result does not count as a GitHub approval.
Fenrir's latest completed scan found no issues; clearing the prior automated change request.
Problem
src/ssh.chas three channel read paths._ChannelReadExt()already separates"bytes were delivered" from "the window-adjust packet made it onto the wire".
The other two conflate them.
_ChannelRead()— behindwolfSSH_ChannelIdRead()andwolfSSH_ChannelRead()—consumes the bytes, then returns the send result:
_UpdateChannelWindow()returnsWS_WANT_WRITEwhenever the adjust would block —routine on a non-blocking socket, not an error. The caller is told the read failed
after the data has already left the input buffer, so those bytes are gone, and a
caller using the usual
if (cnt_r <= 0) break;shape tears the connection down(
src/wolfscp.c:1762is one).wolfSSH_stream_read()has the same shape without theloss — it skips
inputBuffer->idx += n, so the data is re-delivered — but stillreports a failure for a read that copied
nbytes.SendPacket()setsssh->erroronly forWS_WANT_WRITE, so a hard transport failureduring the adjust was invisible on both paths.
Fix (
src/ssh.c)Both paths now report the bytes copied and keep the send result out of band, matching
_ChannelReadExt():_ChannelRead()returnsbufSzunconditionally;wolfSSH_stream_read()advances
inputBuffer->idxbefore the adjust, then returnsn.ssh->errorandWLOGatWS_LOG_ERRORfor anything other thanWS_WANT_WRITE._ChannelRead()also clears a staleWS_WANT_WRITEonce the adjust does go outwith the output buffer drained — its entry points, unlike
wolfSSH_stream_read(),do not reset
ssh->error.A deferred credit still goes out:
wolfSSH_worker()and the nextSendChannelData()flush pending output before doing anything else.
API note:
wolfSSH_stream_read(),wolfSSH_ChannelRead()andwolfSSH_ChannelIdRead()now return the byte count where a deferred or failed windowadjust previously produced a negative return. Callers that relied on that negative
return must check
wolfSSH_get_error()after a successful read. Documented inwolfssh/ssh.hand thewolfSSH_stream_read()block comment.Tests (
tests/unit.c)One harness per path, each putting a full window of channel data and reading it back:
WS_CBIO_ERR_WANT_WRITEssh->error, credited window, consumed bufferWS_CBIO_ERR_GENERALssh->error == WS_SOCKET_ERROR_E, credit left owedwolfSSH_ChannelIdRead()only)ssh->errorback toWS_SUCCESS, no credit owedVerification
unit.test,api.test,testsuite.testandscripts/{sftp,scp,fwd,get-put}.testall pass;
unit.testalso clean under ASan + UBSan.-Werrorwith gcc-13 across 6 configurations (enable-all, Zephyrdefines, sftp-only, scp-only, default, small-stack).
ssh->errorrecording fails both hard-failureassertions; reverting the stale-
WS_WANT_WRITEclearing fails the third phase.The echo server's worker loop has its own partial-write problems in this area. Those
are a separate rework PR that builds on this one — deliberately not in scope here.