Skip to content

ssh: stop driving a session after a disconnect - #1211

Open
ejohnstown wants to merge 2 commits into
wolfSSL:masterfrom
ejohnstown:disconnect-gate
Open

ssh: stop driving a session after a disconnect#1211
ejohnstown wants to merge 2 commits into
wolfSSL:masterfrom
ejohnstown:disconnect-gate

Conversation

@ejohnstown

@ejohnstown ejohnstown commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

RFC 4253 section 11.1 ends the session at the disconnect, but two paths still ran afterwards. wolfSSH_accept() and wolfSSH_connect() drove the handshake, and their pending-send block would flush a disconnect of ours left queued by a short send, counting it as the next handshake message. DoPacket() still dispatched every inbound message, so the handlers that answer -- a close draws an EOF and a close of ours, a request a success or failure, an open a confirmation, an unknown message an UNIMPLEMENTED -- put traffic on a session that is over.

  • both drivers gate on SendAfterDisconnect() ahead of the pending-send block; the shutdown paths own that flush
  • DoPacket() skips the whole dispatch once ssh->disconnected is set, for every message but a DISCONNECT, which still reaches DoDisconnect() and is what latches WS_DISCONNECT. The frame advance steps over the packet using the length DoReceive() already validated, so the stream stays in step with no payload bookkeeping of its own
  • wolfSSH_worker() stops reporting WS_REKEYING once the session is over. The NEWKEYS that clears isKeying is skipped from here on, so the flag would latch for the rest of the session and the SFTP and SCP drive loops would keep pumping a dead one
  • ssh.h and internal.h record that inbound traffic is dropped rather than buffered, and drop the note that the drivers are ungated

A received disconnect used to reach the error-state test in wolfSSH_accept() and report WS_INVALID_STATE_E; the gate answers WS_FATAL_ERROR first. Both driver tests pin that, alongside the local disconnect and the queued short send, one test per endpoint so a single-sided build keeps the coverage that applies to it.

This is the first of three branches re-cut from #1195, which had grown to 25 commits across three unrelated concerns. This strand owns all of the tests/regress.c churn and reads nothing the EOF work adds; its lineage is the merged "Block every send after a disconnect" series, not the EOF PR.

Merge order: #1211, #1212, #1195, then #1148. Nothing here conflicts with master's 14 commits since the fork point -- they touch no file this branch touches.

wolfSSH_accept() and wolfSSH_connect() drive the handshake only while the
session is live. Both gate on SendAfterDisconnect() ahead of the pending-send
block, which would otherwise flush a queued disconnect and count it as the
next handshake message; the shutdown paths own that flush.

- The prototype sits ahead of both drivers, since either can be the only one
  built.
- TestDisconnectGatesAccept() and TestDisconnectGatesConnect() cover a local
  disconnect, one from the peer, and a queued short send. One test per
  endpoint, so a single-sided build keeps the coverage that applies to it.
- A received disconnect used to reach the error-state test in wolfSSH_accept()
  and report WS_INVALID_STATE_E; the gate answers WS_FATAL_ERROR first, and
  both tests pin that.
- The contract comments in ssh.h and internal.h drop the ungated note.

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 aligns session behavior with RFC 4253 §11.1 by ensuring that once a DISCONNECT is sent or received, the library stops driving handshake progress and stops emitting any protocol replies triggered by later inbound messages.

Changes:

  • Gate wolfSSH_accept() and wolfSSH_connect() on SendAfterDisconnect() to prevent handshake driving (and pending-send flushing) after a disconnect.
  • Update DoPacket() to skip dispatching all post-disconnect inbound messages except MSGID_DISCONNECT, preventing post-termination replies and dropping late inbound traffic while keeping framing in sync.
  • Adjust wolfSSH_worker() to stop reporting WS_REKEYING after a disconnect so callers don’t keep pumping a dead session.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
wolfssh/ssh.h Updates public API documentation to reflect post-disconnect gating and inbound-drop behavior.
wolfssh/internal.h Clarifies internal state contract for disconnected and the fact that inbound dispatch is skipped post-disconnect.
src/ssh.c Adds disconnect gating to accept/connect drivers and prevents WS_REKEYING reporting after disconnect.
src/internal.c Skips inbound message dispatch after disconnect (except DISCONNECT) while still advancing the input frame safely.
tests/regress.c Adds regression coverage for the accept/connect gates and for silencing/dropping post-disconnect inbound handling.

💡 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 #1211

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 Outdated
* buffered stderr and its window credit. A disconnect outranks the
* rekey, the way the head-of-list reads have it: the NEWKEYS that
* clears isKeying is skipped from here on, so the flag latches. */
if (ssh->isKeying && !ssh->disconnected && ret != WS_EXTDATA) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New !ssh-disconnected rekey gate in wolfSSH_worker() has no test coverage · Missing edge-case coverage on a function the PR also changed

The PR adds !ssh->disconnected to the WS_REKEYING gate in wolfSSH_worker(), but none of the five new tests — nor any existing one — calls wolfSSH_worker() with both isKeying and disconnected set. Reverting the condition leaves the suite green.

Fix: Add a test that sets isKeying, takes a disconnect, then asserts wolfSSH_worker() no longer returns WS_REKEYING, mirroring TestDisconnectOutranksRekey().

Comment thread src/ssh.c Outdated
* buffered stderr and its window credit. A disconnect outranks the
* rekey, the way the head-of-list reads have it: the NEWKEYS that
* clears isKeying is skipped from here on, so the flag latches. */
if (ssh->isKeying && !ssh->disconnected && ret != WS_EXTDATA) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

wolfSSH_worker() reports success indefinitely on a disconnected session · SSH protocol violations

DoPacket() now returns WS_SUCCESS for every post-disconnect message, so DoReceive() sets ssh->error = WS_SUCCESS; adding !ssh->disconnected removes the last non-success return. After a locally-sent disconnect wolfSSH_worker() returns WS_SUCCESS with wolfSSH_get_error() 0 for unbounded peer traffic, so a worker-driven caller never tears the session down.

Fix: Have wolfSSH_worker() set ssh->error = WS_DISCONNECT and return WS_FATAL_ERROR once ssh->disconnected is set, as wolfSSH_accept() and wolfSSH_connect() now do.

DoPacket() skips the whole message dispatch once ssh->disconnected is set, for
every message but a DISCONNECT. The handlers that answer must not -- a close
draws an EOF and a close of ours, a request a success or failure, an open a
confirmation, an unknown message an UNIMPLEMENTED -- and what the rest would
record is of no use to a caller that can no longer send. RFC 4253 section 11.1.

- Inbound data from here on is dropped rather than buffered, so the read path
  hands back only what arrived before the disconnect. ssh.h and internal.h say
  so, beside the calls and beside the flag.
- A DISCONNECT still reaches DoDisconnect(), which sends nothing and is what
  latches WS_DISCONNECT; SendDisconnect() sets the flag too, so ours can be
  the one that raised it.
- The frame advance steps over the whole packet, so the stream stays in step
  with no payload bookkeeping of its own.
- wolfSSH_worker() gates on SendAfterDisconnect() the way wolfSSH_accept() and
  wolfSSH_connect() do. With the dispatch skipped there is no non-success left
  to return, so it would answer a healthy session for as long as the peer kept
  talking and the SFTP and SCP drive loops would keep pumping a dead one. The
  rekey test below the gate needs no disconnect of its own: the gate returns
  first, and a DISCONNECT arriving mid-pass leaves ret fatal.
  wolfSSH_shutdown() drops the channel on a disconnect before its own pump, so
  the gate does not cost it the read it does there.
- tests/regress.c pins all four: no reply goes out, the stream stays in step
  with a disconnect queued behind a skipped close, late channel data is
  dropped rather than queued, and the worker reports the disconnect on the
  pass that takes it and on every pass behind it.
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.

3 participants