Skip to content

examples/echoserver: hold what a channel send did not take - #1261

Open
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_10544_rework
Open

yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_10544_rework

Conversation

@yosuke-wolfssl

@yosuke-wolfssl yosuke-wolfssl commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Problem

ssh_worker() treated every positive write(), send() and wolfSSH_ChannelIdSend()
as a complete transfer. SendChannelData() clamps each send to
min(peerWindowSz, peerMaxPacketSz), so short sends are routine and seven sites
discarded the unsent suffix — the forwarding path reset its buffer index on any
positive return. Not confined to small windows: a 4 MB echo at the default window
loses 98 KiB. Closes f-10544 (High).

Fix (examples/echoserver/echoserver.c)

WS_AppCtx gains bufferIdx/bufferOff, so each direction keeps the tail its sink
would not take.

  • app_drain_to_channel() advances bufferOff by what the send took and holds the
    rest on WS_WANT_WRITE, WS_WINDOW_FULL, WS_REKEYING, WS_CHANNEL_NOT_CONF and
    WS_CHAN_RXD — restoring the entry error code for the latter four, so an "owed"
    status is not read as fatal on the next pass.
  • app_echo_pump() reads and echoes the shell channel, replacing the EOF drain
    (eofBuffer, eofOff, eofRead are gone). It runs every pass, since data already
    buffered inside the library draws no second data report.
  • app_write_all() writes whole buffers to the pty, agent and forward descriptors.

A descriptor leaves the read set while its buffer is non-empty, and a context clears
its staging as it enters a connection. Holds are retired by existing wakes:
wolfSSH_OutputPending() arms the write set, and inbound traffic arrives on a socket
already in the read set.

Verification

Manual, since the tests are deferred. Same client and constrained window, master vs
this branch:

Case master branch
Echo 4 MiB, default window 98 KiB lost complete
Forward, 2 MB target→client truncated at 3000 byte-identical
pty shell, 1500 lines, slow reader 1478 lines lost none lost

make check 13/13 (incl. fwd.test, fwd-bulk.test); GCC -Werror clean on 6
configurations; ASan + UBSan clean; Zephyr qemu_x86 3/3.

Not in this PR

  • Regression tests. Withdrawn: they force a rekey mid-transfer in small-highwater
    builds, hitting a separate defect that reproduces on master — the echo path treats
    a rekey-blocked send as fatal. They land with that fix.
  • dump_stats() has the same flaw: a short wolfSSH_stream_send() truncates the
    statistics blob. It is left alone on purpose. A plain retry is worse than the
    truncation — the second send finds the window exhausted, returns WS_WINDOW_FULL,
    and the caller's <= 0 check ends the session. Doing it properly means staging the
    blob and resuming across passes, which is out of proportion for debug output behind
    a control byte.
  • The ESP-IDF copy, deferred: its ssh_worker() has diverged by ~580 lines and has
    no select() write set, which this fix depends on.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Sep 18, 2026
Copilot AI lite review requested due to automatic review settings September 18, 2026 00:53

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.

🟡 Changes recommended

Critical EOF flushing and forwarding teardown issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Reworks echo-server buffering to preserve unsent data during short channel and descriptor writes, with regression tests for flow control and half-close behavior.

Changes:

  • Adds persistent staging and reliable drain helpers.
  • Updates echo, agent, and forwarding paths.
  • Adds 8,000-byte round-trip regression tests.
File summaries
File Summary
tests/api.c Adds flow-control, refill, and half-close tests.
examples/echoserver/echoserver.c Preserves partial transfers across worker passes.
Review details

Suppressed comments (1)

examples/echoserver/echoserver.c:1760

  • When the local forwarding socket reaches EOF, this path changes fwdCtx to APP_STATE_LISTEN and continues without resetting bufferIdx/bufferOff. Any staged tail left by an earlier channel send therefore survives into the next accepted connection and is sent on its channel. Reset the staging fields on this EOF/reset teardown as well (and do the same for the analogous agent-socket close paths).
                    if (cnt_r == 0) {
                        /* Read zero-returned. Socket is closed. Go back
                           to listening. */
                        WCLOSESOCKET(fwdFd);
                        fwdFd = -1;
                        threadCtx->fwdCtx.appFd = -1;
                        if (threadCtx->fwdCbCtx.hostName != NULL) {
                            WFREE(threadCtx->fwdCbCtx.hostName, NULL, 0);
                            threadCtx->fwdCbCtx.hostName = NULL;
                        }
                        threadCtx->fwdCtx.state = APP_STATE_LISTEN;
                        continue;
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread examples/echoserver/echoserver.c
Comment thread examples/echoserver/echoserver.c Outdated

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fenrir Automated Review — PR #1261

Scan targets checked: wolfssh-src, wolfssh-bugs

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Comment thread tests/api.c Outdated
@yosuke-wolfssl yosuke-wolfssl changed the title Fix/f 10544 rework examples/echoserver: hold what a channel send did not take Sep 18, 2026
@wolfSSL-Fenrir-bot
wolfSSL-Fenrir-bot dismissed their stale review September 18, 2026 01:58

Fenrir's latest completed scan found no issues; clearing the prior automated change request.

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fenrir Automated Review — PR #1261

Scan targets checked: wolfssh-src, wolfssh-bugs

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

@yosuke-wolfssl
yosuke-wolfssl force-pushed the fix/f_10544_rework branch 3 times, most recently from ea034be to 44d9b8b Compare September 18, 2026 06:26

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fenrir Automated Review — PR #1261

Scan targets checked: wolfssh-src, wolfssh-bugs

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Comment thread examples/echoserver/echoserver.c Outdated
- WS_AppCtx gains bufferIdx and bufferOff; app_staged() reports the
  bytes its buffer still owes the channel.
- app_drain_to_channel() advances bufferOff by what
  wolfSSH_ChannelIdSend() took, keeps the rest on WS_WANT_WRITE,
  WS_WINDOW_FULL, WS_REKEYING, WS_CHANNEL_NOT_CONF and WS_CHAN_RXD,
  and raises wantWrite for the first of those. For the other four it
  puts back the error code it found on entry, so the status of a send
  that is only owed does not outlive the call. A send reporting more
  than it was given, or zero, ends the session.
- The shell, agent and forward reads stage into their own WS_AppCtx
  buffer and drain through it; their descriptors leave the select
  read set while bufferOff is short of bufferIdx.
- app_echo_pump() reads the shell channel into shellCtx.buffer, hands
  each chunk to process_bytes() and drains it back. It reports the
  channel dry on a zero read alone, and a negative one stops the pump
  without ending the session. ssh_worker() runs it on every pass
  rather than on a data report.
- process_bytes() sees every echoed chunk, including the backlog that
  goes out behind the peer's EOF, and only ever clears ChildRunning.
- The EOF reply takes its drained state from app_echo_pump();
  eofBuffer, eofOff and eofRead are dropped.
- app_write_all() writes a whole buffer to the pty, agent and forward
  descriptors, retrying an interrupted write, in place of the single
  write() and send() calls on the channel-to-descriptor paths.
- The agent and forward contexts clear their staging as they enter a
  connection; the forward's recv() fills the buffer from the start,
  and fwdBufferIdx and cnt_w are dropped.

Issue: F-10544
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.

4 participants