Skip to content

wolfSSH_stream_read consumes before crediting and always reports the byte count. - #1188

Open
yosuke-wolfssl wants to merge 2 commits into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_10544
Open

wolfSSH_stream_read consumes before crediting and always reports the byte count.#1188
yosuke-wolfssl wants to merge 2 commits into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_10544

Conversation

@yosuke-wolfssl

Copy link
Copy Markdown
Contributor

Problem

ssh_worker() in the echo server treats every positive write(), send(), and
wolfSSH_ChannelIdSend() return as a complete transfer. SendChannelData() clamps
each send to min(peerWindowSz, peerMaxPacketSz, maxPacketSz) and returns that
clamped 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
fwdBufferIdx to 0 on any positive result — and the unsent suffix is lost
silently. Reachable through the shell, agent, and forwarding paths.

Separately, wolfSSH_stream_read() credits inputBuffer->idx bytes but advanced
idx only afterwards, so a read never credits its own bytes. A single read that
drains 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:

  • Local descriptors (pty, agent and forwarding sockets) stall only until the fd
    drains, so app_write_all() advances past each partial transfer and returns
    WS_FATAL_ERROR if it cannot complete.
  • SSH channels stall until the peer's window reopens, which needs the loop to
    process inbound packets. Each direction stages into its WS_AppCtx buffer; the
    flush subtracts what was sent and WMEMMOVEs the remainder for the next pass.
Send result Handling
> 0 subtract, keep the remainder
WS_CHANNEL_NOT_CONF, WS_CHAN_RXD, WS_WINDOW_FULL, WS_REKEYING hold, retry on inbound data
WS_WANT_WRITE hold, and add sshFd to a write set so select() wakes on writability

A 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() already
follows, so returning an error after consuming cannot drop the caller's bytes.

Tests

  • test_wolfSSH_stream_read_WindowCredit() (tests/api.c) round-trips 2000 bytes
    through a 1024-byte client receive window against the echo server.
  • test_stream_read_deferredWindowAdjust() (tests/unit.c) reads a full window with
    an IO send that reports WS_CBIO_ERR_WANT_WRITE.

Verification

  • Full suite green: unit, api, testsuite, regress, kex, auth, and the
    scripts/ tests including fwd.test.
  • Clean under -Werror across six GCC configurations; ASan + UBSan clean.
  • Both tests fail with either fix reverted; the unit test also fails against an
    intermediate variant that consumed the bytes but returned the error.

- 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
@yosuke-wolfssl yosuke-wolfssl self-assigned this Aug 20, 2026
Copilot AI lite review requested due to automatic review settings August 20, 2026 23:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in ssh->error.
  • Rework examples/echoserver/echoserver.c I/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.

Comment thread src/ssh.c
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants