Skip to content

ssh, internal: flush the worker's queued output on every call - #1217

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

ssh, internal: flush the worker's queued output on every call#1217
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/worker-deadlock

Conversation

@yosuke-wolfssl

@yosuke-wolfssl yosuke-wolfssl commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Problem

A read-only application on a non-blocking socket stalls permanently.

A channel read credits the window, ChannelCreditWindow() bundles a CHANNEL_WINDOW_ADJUST into ssh->outputBuffer, and the socket write blocks. The credit is not re-parked — the packet is already encrypted and sequenced — so wolfSSH_SendPacket() is the only thing that can discharge it. The peer has spent its window and goes silent waiting for that adjust.

The application calls wolfSSH_worker(), as wolfssh/ssh.h directs. The worker gated its flush on DoReceive()'s return, and an idle socket makes DoReceive() return WS_FATAL_ERROR — not one of the gated values. No write is attempted, on that call or any later one. The gate also listed WS_WANT_READ, which DoReceive() never returns; that dead arm is the bug.

Fix (src/ssh.c)

wolfSSH_worker() flushes whenever output is queued and the session is live:

if (ssh != NULL && !ssh->disconnected && ssh->outputBuffer.length != 0) {
  • !ssh->disconnected keeps bytes from going out after a DISCONNECT (RFC 4253 §11.1), which the old gate excluded only by accident.
  • ssh->error keeps the receive's code only when the receive itself failed. WS_CHAN_RXD, WS_EOF and WS_EXTDATA keep the flush's code, as ssh.h documents for WS_EOF.
  • Removes the second DoReceive(), its WS_WINDOW_FULL case, the WOLFSSH_TEST_BLOCK ordering fork, and the separate WS_CHANNEL_CLOSED flush — that block existed only because the gate skipped that status. DoChannelCloseWantWrite and DoChannelCloseFlushesReply pass unchanged without it.

Fix (src/internal.c)

SendPacketFlush() records its code in ssh->error on every transport failure path, not only WS_WANT_WRITE. Otherwise a hard send failure during the flush left the idle receive's WS_WANT_READ in place, and callers routing on wolfSSH_get_error() would select for read on a dead socket.

Fix (consumers)

The worker now reports WS_WANT_WRITE where the removed second DoReceive() left WS_WANT_READ. Two shell loops treated anything else as fatal and would end a live session on a briefly unwritable socket: examples/echoserver/echoserver.c:1208 and apps/wolfsshd/wolfsshd.c:1364 (WIN32). Every other wolfSSH_worker() call site already handles it.

No public API or header change.

Tests

Seven wolfSSH_worker() tests cover the flush on an idle receive, the owed flush across calls, and what ssh->error holds after a receive failure, a hard send failure, channel data alongside a failed send, a discarded buffer, and an out-of-bounds send. TestWorkerReportsDisconnect covers queued output staying unsent on the disconnect pass.

Verification

  • unit.test 149 passed / 0 failed; regress.test passed.
  • Clean under gcc-13 -Werror across 6 configurations, plus lint.
  • Network contention (-DWOLFSSH_TEST_BLOCK, scripts/sftp.test), the harness whose ordering fork this removes:
WOLFSSH_BLOCK_PROB Result Forced write blocks
70 pass, 47s 66
50 pass, 21s 34
30 pass, 11s 15
  • Each guard is backed by a negative control: reverting it makes a named test fail.

Known limitation, not addressed here

echoserver's ssh_worker() waits in select() on read fds with a NULL timeout and calls wolfSSH_worker() only when the socket is readable, so an owed flush is retried only if the peer sends. That stall predates this PR — on master the same path left WS_WANT_READ and also never flushed. Closing it needs a write fd_set in the example's event loop.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Aug 31, 2026
Copilot AI lite review requested due to automatic review settings August 31, 2026 05:55

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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 #1217

Scan targets checked: wolfssh-bugs, wolfssh-src

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 src/ssh.c Outdated
Comment thread src/ssh.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 #1217

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.

Comment thread src/ssh.c
Comment thread apps/wolfsshd/wolfsshd.c
Comment thread src/ssh.c
Comment thread apps/wolfsshd/wolfsshd.c

@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 #1217

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.

@wolfSSL-Fenrir-bot
wolfSSL-Fenrir-bot dismissed stale reviews from themself September 1, 2026 01:12

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

- wolfSSH_worker() calls wolfSSH_SendPacket() whenever
  ssh->outputBuffer holds bytes and the session is not
  disconnected, in place of doing so only for WS_SUCCESS,
  WS_WANT_READ, WS_CHAN_RXD or WS_EOF. A failed flush reaches the
  return only when the receive reported nothing, and ssh->error
  keeps the receive's code only when the receive failed. Drops the
  second DoReceive(), its WS_WINDOW_FULL case, the
  WOLFSSH_TEST_BLOCK fork, and the separate WS_CHANNEL_CLOSED
  flush.
- SendPacketFlush() records its code in ssh->error on every
  transport failure path, and wolfSSH_SendPacket() says so. The
  comments on _ChannelRead(), _ChannelReadExt() and
  test_ChannelReadExtHardFailureReported() drop the former
  contract.
- The echoserver shell loop and the Windows wolfsshd shell loop
  treat a WS_WANT_WRITE from wolfSSH_worker() as non-fatal.
- tests cover the flush on an idle receive, the owed flush across
  calls, what ssh->error holds after a receive failure, a hard
  send failure, channel data alongside a failed send, a discarded
  buffer and an out-of-bounds send, and queued output staying
  unsent on the disconnect pass. ConnResetIoSend moves to the
  shared doubles beside a new OobIoSend.
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