examples/echoserver: hold what a channel send did not take - #1261
Conversation
There was a problem hiding this comment.
🟡 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
fwdCtxtoAPP_STATE_LISTENand continues without resettingbufferIdx/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.
d939176 to
89337e0
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
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.
89337e0 to
8d87a43
Compare
Fenrir's latest completed scan found no issues; clearing the prior automated change request.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
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.
ea034be to
44d9b8b
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
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.
44d9b8b to
5df5245
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
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.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
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.
Fenrir's latest completed scan found no issues; clearing the prior automated change request.
f0f7131 to
f0f2d8a
Compare
ejohnstown
left a comment
There was a problem hiding this comment.
Two requests before this merges. Both are small.
1. Echo before process_bytes(), as master does
app_echo_pump() now runs process_bytes() on a chunk before app_drain_to_channel() sends it. On master the echo went out first. Sending abc\x05def\n through OpenSSH shows the change:
master:abc^Edef\n, then the statistics block.- this branch: the statistics block, then
abc^Edef\n.
Ctrl-F changes the same way. wolfSSH_TriggerKeyExchange() now fires before the drain, so the drain gets WS_REKEYING and the chunk's echo waits until the rekey finishes. Before, it went out ahead of the rekey. Nothing is lost, but the output changes and the PR doesn't mention it. Please drain first and process the chunk afterwards. app_drain_to_channel() doesn't modify buffer, so the bytes are still there after the send:
for (;;) {
word32 freshSz = 0;
if (app_staged(appCtx) == 0) {
cnt = wolfSSH_ChannelIdRead(ssh, appCtx->channelId,
appCtx->buffer, (word32)sizeof appCtx->buffer);
/* Only a zero read is dry; a negative one just stops the pump. */
if (cnt <= 0) {
*dry = (cnt == 0);
break;
}
#ifdef SHELL_DEBUG
buf_dump(appCtx->buffer, cnt);
#endif
appCtx->bufferIdx = (word32)cnt;
appCtx->bufferOff = 0;
freshSz = (word32)cnt;
}
ret = app_drain_to_channel(ssh, appCtx, appCtx->channelId, wantWrite);
if (ret < 0) {
return ret;
}
/* After the drain, so the echo precedes what the chunk asks for */
if (freshSz > 0 && process_bytes(threadCtx, appCtx->buffer, freshSz)) {
ChildRunning = 0;
break;
}
if (app_staged(appCtx) > 0) {
break;
}
}The break on a stop matches master, which echoed only the chunk that held the Ctrl-C and read nothing after it.
2. Explain why app_drain_to_channel() restores ssh->error
Please keep this restore. A review suggested removing it, since for three of the four codes it does nothing. I removed it and tested. With OpenSSH set to -o RekeyLimit=256K and a slow reader, every run ended at 16384 of 4 MiB, with and without -N. With the restore, every run was byte-identical.
Here's why. SendChannelData() stores WS_WINDOW_FULL in ssh->error. DoKexInit() returns any non-zero ssh->error (src/internal.c, the "Propagate potential want write case from SendKexInit" check). DoPacket() doesn't tolerate WS_WINDOW_FULL, so the peer's KEXINIT fails and the session ends. The current comment doesn't say this, so the next reader is likely to delete it. Please change it to something like:
/* Owed, not failed. Put back the entry code: SendChannelData()
* leaves WS_WINDOW_FULL in ssh->error, and DoKexInit() returns
* any nonzero ssh->error, so a stale one fails the peer's next
* KEXINIT and ends the session. */
ssh->error = savedError;The fix for the underlying problem belongs in the library and will be a separate PR. Once that lands, this restore can go.
69f84a6 to
c80ce5d
Compare
- WS_AppCtx stages each direction on its own: buffer, bufferIdx and bufferOff from appFd to the channel; fdBuffer, fdIdx and fdOff from the channel to appFd. thread_ctx_t drops channelBuffer and eofBuffer. - app_drain_to_channel() advances bufferOff by what wolfSSH_ChannelIdSend() took, and holds the rest on WS_WANT_WRITE, WS_WINDOW_FULL, WS_REKEYING, WS_CHANNEL_NOT_CONF and WS_CHAN_RXD, putting back the ssh->error from before that send for all but WS_WANT_WRITE. A zero or over-reported send ends the session. - app_pump_to_fd() reads the channel into fdBuffer and writes it to the pty or forward socket, both now nonblocking, keeping what would block. - ssh_worker() leaves appFd out of the read set while buffer holds bytes, puts it in the write set while fdBuffer does, and runs both pumps on every pass. - A pty read of zero ends the loop whatever errno holds. - app_echo_pump() echoes the shell channel through shellCtx.buffer and then hands each new chunk to process_bytes(). It replaces the EOF drain along with eofOff and eofRead. A negative read ends the session. - app_flush_fd() gives a connected forward up to APP_FLUSH_SECS without progress after the loop to take what fdBuffer still holds. - The agent keeps blocking writes, through app_write_all(), which retries an interrupted send. - A forward clears both of its buffers as it connects. - fwd-bulk.test phase 3 logs in with a password and echoes 32 MB through ssh -L while the client keeps sending but stops reading for 10 s. Phase 4 ends a forward while bytes are held for its target and checks the next forward's target gets only its own. Issue: F-10544
c80ce5d to
68330b9
Compare
|
Thank you @ejohnstown for reviewing.
Rekey with a slow reader ( |
|
@wolfSSL-Fenrir-bot review |
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1261
Scan targets checked: wolfssh-src, wolfssh-bugs
Coverage: 1 of 1 in-scope changed file(s) opened by the reviewer
Fenrir result: Approved ✅
No new issues found in the changed files.
Advisory only — this automated result does not count as a GitHub approval.
Review tier: Lite
Problem
ssh_worker()treated every positivewrite(),send()andwolfSSH_ChannelIdSend()as a complete transfer.
SendChannelData()clamps each send tomin(peerWindowSz, peerMaxPacketSz), so short sends are routine and seven sitesdiscarded 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)Each
WS_AppCtxstages both directions and keeps what its sink would not take:buffer/bufferIdx/bufferOfftoward the channel,fdBuffer/fdIdx/fdOfftowardthe descriptor.
app_drain_to_channel()advancesbufferOffby what the send took and holds therest on
WS_WANT_WRITE,WS_WINDOW_FULL,WS_REKEYING,WS_CHANNEL_NOT_CONFandWS_CHAN_RXD. For the latter four it restores the entryssh->error: a leftoverWS_WINDOW_FULLfailsDoKexInit()on the peer's next KEXINIT.app_pump_to_fd()moves channel data to the pty or forward socket, both nownonblocking, and keeps what would block. The descriptor sits in the
select()writeset while it owes bytes, so no write can stall the loop.
app_echo_pump()echoes the shell channel, then runsprocess_bytes(). Itreplaces the EOF drain.
app_flush_fd()gives a forward up to 5 s without progress after the session endsto take what it still owes.
app_write_all()retries interrupted sends.A descriptor leaves the read set while its channel-bound buffer is non-empty, both
pumps run every pass, and a forward clears both buffers as it connects.
Tests (
scripts/fwd-bulk.test)Both phases need
python3andssh, and skip without them. OpenSSH logs in with apassword, so no key algorithm has to be compiled in.
ssh -Lwhile the client keeps sending but stopsreading for 10 s. The first revision of this PR deadlocks there at ~4.2 MB, blocked in
send()to the forward; so does this one with the forward left blocking.checks the next forward's target gets only its own. Without the reset on connect, the
second target receives 69–4113 stale bytes (3/3 runs).
Verification
make check13/13; GCC-Werrorclean on 6 configurations; CI green.blocking and
-Nsockets): 18/18 on macOS, 18/18 on Linux (ubuntu:24.04).Not in this PR
channel, or the target half-closes, before the target has read them. Pre-existing:
a client that sends 600 KB and closes at once loses ~120 KB to a stalled target on
mastertoo, and ~5 KB more here (fdBuffer). OpenSSH andportfwddon't close thatway, and lost nothing in testing. Keeping them needs the library to hold a closed
channel's unread data.
tests/api.cregression tests. Withdrawn: they force a rekey mid-transfer insmall-highwater builds, hitting a separate defect that reproduces on
master— theecho path treats a rekey-blocked send as fatal. They land with that fix.
dump_stats()has the same flaw. A plain retry is worse than the truncation: thesecond send finds the window exhausted and the caller ends the session. Staging the
blob across passes is out of proportion for debug output behind a control byte.
ssh_worker()has diverged by ~580 lines and hasno
select()write set, which this fix depends on.