Skip to content

ssh: report the owed write when the worker leaves output queued - #1252

Merged
ejohnstown merged 2 commits into
wolfSSL:masterfrom
yosuke-wolfssl:fix/shutdown-owed-write
Sep 17, 2026
Merged

ejohnstown merged 2 commits into
wolfSSL:masterfrom
yosuke-wolfssl:fix/shutdown-owed-write

Conversation

@yosuke-wolfssl

Copy link
Copy Markdown
Contributor

Problem

wolfSSH_shutdown() reads for the peer's close reply, then reports what the
read left queued:

if (ret == WS_SUCCESS && wolfSSH_OutputPending(ssh)) {
    int sendErr = wolfSSH_get_error(ssh);

    if (sendErr == WS_WANT_WRITE || sendErr == WS_WANT_READ
            || sendErr == WS_SUCCESS)
        ret = WS_WANT_WRITE;
    else
        ret = sendErr;          /* cannot be a send error */
}

The else cannot do what it says. A hard flush failure in wolfSSH_worker()
puts its code in the return, so this block is not entered; a short flush leaves
WS_WANT_WRITE; a successful one drains the buffer and fails the guard. And
DoReceive() overwrites ssh->error with the dispatch status, so what the arm
reads is the event, not a send result.

What it can do is leak a per-channel event out of a teardown. The worker's
flush is gated on !ssh->disconnected, and that flag can be set mid-pass:
DoChannelEof() discards its callback's return, so a channelEofCb that calls
wolfSSH_SendDisconnect() on a blocked socket leaves the DISCONNECT queued
with the flag up. The flush is skipped, the pass returns WS_EOF, and
wolfSSH_shutdown() returns WS_EOF (-1031) for a session that owes a write.

No in-tree consumer installs channelEofCb, so this is a contract fix rather
than a reported failure.

Fix (src/ssh.c)

ret = WS_WANT_WRITE unconditionally — output is pending by the guard.
sendErr and its two vestigial tolerances go with it.

WS_WANT_WRITE is already the answer for a disconnected session with our own
DISCONNECT queued:

Site Returns
wolfSSH_shutdown() top flush WS_WANT_WRITE (TestShutdownKeepsFlushWantWrite)
wolfSSH_SendDisconnect() WS_WANT_WRITE
wolfSSH_accept() WS_FATAL_ERROR / WS_DISCONNECT

FlushQueuedOutput() bails only on disconnected && !disconnectTxd, so once
our DISCONNECT is bundled the flush is still wanted and the retry puts it on
the wire. A genuine transport error is not lost — it takes the return on the
next call.

wolfssh/ssh.h gains three doc corrections: WS_FATAL_ERROR keeps the
wolfSSH_worker() return alongside WS_CHANNEL_CLOSED; only WS_SUCCESS and
WS_CHAN_RXD are displaced by a reported WS_REKEYING; and the
wolfSSH_shutdown() block notes the owed write above. The first two describe
behaviour already covered by test_WorkerHardRecvErrorOutranksFlush,
test_WorkerReportsExtDataChannelKeying and
test_WorkerReportsEofChannelKeying.

Tests

Test Asserts
test_ShutdownOwedWriteAfterEofDisconnect WS_WANT_WRITE with output pending, then WS_SUCCESS with the buffer drained on the retry
test_ShutdownHardEofDisconnectSurfacesOnRetry same first call; the refused send's WS_SOCKET_ERROR_E takes the return on the retry

Both fail against the unmodified src/ssh.c, the first returning WS_EOF.

Verification

  • Lint plus six gcc-13 -Werror configs clean.
  • 168 unit tests pass (2 new), 0 failures; regress and testsuite clean;
    scp/sshclient/get-put/sftp scripts green.
  • ASan + UBSan clean on unit and regress.

- wolfSSH_shutdown() sets ret to WS_WANT_WRITE whenever the
  wolfSSH_worker() call it makes ends with output pending. The
  sendErr local and the arm that returned wolfSSH_get_error() are
  gone.
- tests/unit.c gains EofDisconnectCb(), which sends a disconnect
  from the channel EOF dispatch, and two tests driving it:
  test_ShutdownOwedWriteAfterEofDisconnect() asserts
  wolfSSH_shutdown() returns WS_WANT_WRITE with output pending and
  WS_SUCCESS with the buffer drained on the retry;
  test_ShutdownHardEofDisconnectSurfacesOnRetry() refuses the
  disconnect's send outright and asserts the retry returns
  WS_SOCKET_ERROR_E.
…rite

- The wolfSSH_worker() send-failure sentence names WS_CHANNEL_CLOSED
  and WS_FATAL_ERROR as the statuses that keep the return.
- Its channelId sentence narrows the rekey case to a WS_REKEYING
  that displaced WS_SUCCESS or WS_CHAN_RXD.
- The wolfSSH_shutdown() block adds that the read for the peer's
  close reply can leave output queued, which reports WS_WANT_WRITE,
  and that a send which failed outright there takes the return on
  the next call.
@yosuke-wolfssl yosuke-wolfssl self-assigned this Sep 15, 2026
Copilot AI lite review requested due to automatic review settings September 15, 2026 02:46

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.

🔵 Needs a closer look

A moderate shutdown path issue remains unresolved for queued disconnect output.

Pull request overview

Updates wolfSSH_shutdown() to report queued teardown writes correctly, with documentation updates and regression tests.

Changes:

  • Simplifies pending-output handling.
  • Clarifies worker and shutdown status documentation.
  • Adds retry and transport-error tests.
File summaries
File Summary
wolfssh/ssh.h Documents updated status and shutdown behavior.
tests/unit.c Adds shutdown retry regression coverage.
src/ssh.c Handles owed writes, but WS_CHANNEL_CLOSED teardown can still bypass pending-write handling.
Review details

Suppressed comments (1)

src/ssh.c:1296

  • This still treats queued output as a write want only when the worker returned WS_SUCCESS. A public channelCloseCb can call wolfSSH_SendDisconnect() while the socket is blocked; DoChannelClose() then removes the channel and wolfSSH_worker() returns WS_CHANNEL_CLOSED with that disconnect still in outputBuffer. The branch is skipped, and the later empty-channel handling preserves the event, so wolfSSH_shutdown() returns WS_CHANNEL_CLOSED and a caller can stop without flushing the owed disconnect. Apply the pending-write precedence to this teardown event too, while preventing the later channel-list assignment from overwriting WS_WANT_WRITE.
            ret = WS_WANT_WRITE;
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

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

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

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 changed the title Fix/shutdown owed write ssh: report the owed write when the worker leaves output queued Sep 15, 2026
@ejohnstown
ejohnstown merged commit 47cea34 into wolfSSL:master Sep 17, 2026
185 checks passed
@yosuke-wolfssl
yosuke-wolfssl deleted the fix/shutdown-owed-write branch September 17, 2026 22:57
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.

5 participants