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.
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.
- 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() takes the WOLFSSH from channel->ssh, 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. - wolfssh/ssh.h states the window-adjust and ssh->error contract above wolfSSH_stream_read(), wolfSSH_ChannelRead() and wolfSSH_ChannelIdRead(), and names the wolfSSH_worker() results that do not flush; the src/ssh.c block comments match. - wolfSSH_SFTP_Close() checks NoticeError() only when SendPacketType() did not succeed. - tests/unit.c adds test_stream_read_deferredWindowAdjust() and test_ChannelIdRead_deferredWindowAdjust(), each reading a full window through an IO send that defers, then one that fails, then one that succeeds, checking the byte count, the payload, the status through wolfSSH_get_error(), the credit owed and the drained input buffer. The stream_read case counts the adjust that reaches the wire.
ca90ef2 to
20a8747
Compare
There was a problem hiding this comment.
The flush advice added to three public prototypes in wolfssh/ssh.h is wrong in the case that matters.
Details
Proved with a harness against libwolfssh_test.a: after a deferred adjust, five wolfSSH_worker() calls on an idle socket produce zero IO sends and leave the 24-byte WINDOW_ADJUST sitting in the output buffer, because GetInputData() returns WS_FATAL_ERROR with WS_WANT_READ and that is outside worker’s flush gate. That is exactly the state stalled channel is in. The alternative remedy is unreachable: wolfSSH_SendPacket and wolfSSH_OutputPending are WOLFSSH_LOCAL, so a read-only application has no public flush at all. Not created by this PR, but this PR promotes the wrong advice into the public API documentation. (This is the same underlying gap as the existing worker-idle-flush note.)
|
One other thing. Make the PR title match your one commit's title. Thanks! |
Problem
Two defects in the same three lines.
1. A read that copied bytes reports failure.
_ChannelReadExt()alreadyseparates "bytes were delivered" from "the window-adjust packet made it onto the
wire". The other two paths conflate them.
_ChannelRead()— behindwolfSSH_ChannelIdRead()andwolfSSH_ChannelRead()— consumes the bytes, thenreturns 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 session down(
src/wolfscp.c,apps/wolfsshd/wolfsshd.c).SendPacket()setsssh->erroronlyfor
WS_WANT_WRITE, so a hard transport failure during the adjust was invisible onboth paths.
2. A channel that never gets its window back.
wolfSSH_stream_read()called_UpdateChannelWindow()before advancinginputBuffer->idx, so it creditedbytesToAdd = inputBuffer->idx— the previous read's bytes. On the first read of asession
idxis 0,ChannelCreditWindow()returns early ontotal == 0, and noSSH_MSG_CHANNEL_WINDOW_ADJUSTis sent at all. If that read drained the window,channel->windowSzstays 0 and neither side can move. Every later read credited oneread late.
_ChannelRead()was never affected — it already advancedidxfirst.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. That reordering isalso the fix for defect 2.
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, flushed by the next send or by
wolfSSH_worker(),with one caveat now stated in the header:
wolfSSH_worker()flushes only when itsreceive returned
WS_SUCCESS,WS_WANT_READorWS_CHAN_RXD, so aWS_EXTDATAorWS_REKEYINGturn does not.Also (
src/wolfsftp.c):wolfSSH_SFTP_Close()'sSTATE_CLOSE_SENDarm was theonly
NoticeError()check in the file ungated byret. A parkedWS_WANT_WRITE—routine under the new contract — could make it return
WS_FATAL_ERRORafter asuccessful send and re-send
SSH_FXP_CLOSEon every application retry. Now gated likeits siblings.
API compatibility
wolfSSH_stream_read(),wolfSSH_ChannelRead()andwolfSSH_ChannelIdRead()nowreturn the byte count where a deferred or failed window adjust previously produced a
negative return; the adjust status moves to
wolfSSH_get_error().wolfSSH_ChannelReadExt()already had this shape (50ee1b61). All three prototypesin
wolfssh/ssh.hand the block comments insrc/ssh.cdocument it. This needs aChangeLog.mdline at release prep — not added here, since this repo only editsChangeLog.mdat release.One thing gets marginally worse: a hard transport failure during the adjust no longer
breaks the caller's read loop immediately. Every such loop calls
wolfSSH_worker()onthe next iteration and fails there, so it is a one-iteration delay, not a lost error.
Tests (
tests/unit.c)One harness per path, each putting a full window of channel data and reading it back:
WS_CBIO_ERR_WANT_WRITEwolfSSH_get_error(), credited window, consumed bufferWS_CBIO_ERR_GENERALWS_SOCKET_ERROR_E, credit left owedstream_read) / full send (ChannelIdRead)WS_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 retire assertion;restoring the old
idx/credit ordering sends 0 window adjusts where the fix sends 1.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.