ssh.c: report the byte count from a channel read that defers its credit - #1192
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.
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! |
- wolfSSH_stream_read() advances inputBuffer->idx before crediting the window, and it and _ChannelRead() return the bytes copied with a non-success adjust left in ssh->error. - _ChannelRead() takes the WOLFSSH from channel->ssh, rejects an idx past inputBuffer->length, restores the entry ssh->error on a clean credit, and retires a stale WS_WANT_WRITE only when its own credit went out. - wolfSSH_SFTP_Close() checks NoticeError() only on a failed send. - The src/ssh.c block comments, and new wolfssh/ssh.h notes above wolfSSH_stream_read(), wolfSSH_ChannelRead() and wolfSSH_ChannelIdRead(), state the window-adjust and ssh->error contract. - tests/unit.c adds test_stream_read_deferredWindowAdjust() and test_ChannelIdRead_deferredWindowAdjust(): an adjust that defers, then fails, then succeeds, plus a read with nothing buffered against a seeded WS_WANT_WRITE.
20a8747 to
8c816b6
Compare
Problem
Two defects in the same three lines of
src/ssh.c.1. A read that copied bytes reports failure.
_ChannelRead()— behindwolfSSH_ChannelRead()andwolfSSH_ChannelIdRead()— consumes the bytes, thenreturns the window-adjust send result:
WS_WANT_WRITEis routine on a non-blocking socket, not an error. A caller using theusual
if (cnt_r <= 0) break;shape tears the session down after the data has alreadyleft the input buffer (
src/wolfscp.c,apps/wolfsshd/wolfsshd.c). A hard transportfailure during the adjust was invisible on both paths.
2. A channel that never gets its window back.
wolfSSH_stream_read()creditedbefore advancing
inputBuffer->idx, so it credited the previous read's bytes. On asession's first read
idxis 0 and noSSH_MSG_CHANNEL_WINDOW_ADJUSTis sent at all;if that read drained the window, neither side can move.
Fix (
src/ssh.c)Both paths report the bytes copied and keep the send result in
ssh->error, matching_ChannelReadExt().wolfSSH_stream_read()advancesidxbefore crediting, which isalso the fix for defect 2.
_ChannelRead()retires a staleWS_WANT_WRITEonly whenthis read's own credit went out, and gains its sibling's
idx > lengthguard andsavedErrorrestore.src/wolfsftp.c:wolfSSH_SFTP_Close()'sSTATE_CLOSE_SENDarm was the file'sonly ungated
NoticeError()check, so a parkedWS_WANT_WRITEmade it report afailed close. Now gated like its siblings.
API compatibility
The three read entry points now return the byte count where a deferred or failed
adjust produced a negative return; the status moves to
wolfSSH_get_error().wolfSSH_ChannelReadExt()already had this shape (50ee1b61). Documented on allthree prototypes. Needs a
ChangeLog.mdline at release prep.Tests (
tests/unit.c)One harness per path: a deferred adjust, then a hard failure, then a clean credit. The
ChannelIdReadharness adds an empty read, which must leave a seededWS_WANT_WRITEstanding.
Verification
make checkpasses, andunit.testis clean under ASan + UBSan and under-Werrorwith gcc-13 in 6 configurations. Negative controls: reverting the
ssh->errorrecording fails the hard-failure assertions, restoring the old ordering sends 0
adjusts where the fix sends 1, and removing the retire gate lets an empty read clear a
WS_WANT_WRITE. (scripts/sftp.testandscripts/scp.testneed a serial run on aloaded machine — a pre-existing 2 second ready-file timeout, unrelated to this change.)
Not in this PR
wolfSSH_worker()does not flush a parked adjust when its receive is idle, so aread-only application whose peer has exhausted its window can stall — and there is no
public flush, since
wolfSSH_SendPacket()andwolfSSH_OutputPending()areWOLFSSH_LOCAL. Closing it means restructuringwolfSSH_worker(), which iscaller-visible; that is on its own branch. This PR neither creates nor widens the gap,
and its header no longer promises a flush that does not happen.