Skip to content

Fix: windowAdjust sends after CHANNEL_CLOSE#1483

Open
yurynix wants to merge 1 commit intomscdex:masterfrom
yurynix:dont-send-window-after-close
Open

Fix: windowAdjust sends after CHANNEL_CLOSE#1483
yurynix wants to merge 1 commit intomscdex:masterfrom
yurynix:dont-send-window-after-close

Conversation

@yurynix
Copy link

@yurynix yurynix commented Mar 6, 2026

Hi,
Everything here (except this line) was written by Claude, however, I really have this problem in my project, and the solution really solves it, please consider it 🙃

Problem

windowAdjust() in lib/Channel.js only guards against outgoing.state === 'closed' but not 'closing'. After close() sets the state to 'closing' and sends SSH_MSG_CHANNEL_CLOSE (message type 97), a pending _read() callback can still invoke windowAdjust(), which sends SSH_MSG_CHANNEL_WINDOW_ADJUST (message type 93) after SSH_MSG_CHANNEL_CLOSE.

This violates RFC 4254 Section 5.3:

"Once a party has sent a CHANNEL_CLOSE message, it MUST NOT send further messages for the channel."

The window between close() and receiving the remote's CHANNEL_CLOSE response leaves outgoing.state at 'closing' — a state that windowAdjust() did not check.

Fix

One-line change in lib/Channel.js:280:

-  if (self.outgoing.state === 'closed')
+  if (self.outgoing.state === 'closed' || self.outgoing.state === 'closing')

Tests

test/test-channel-window-adjust.js contains four test cases:

  1. closing state — Creates a mock channel with outgoing.state = 'closing' and verifies windowAdjust() does not call channelWindowAdjust. This is the bug reproduction: it fails without the fix and passes with it.

  2. open state (sanity check) — Verifies windowAdjust() works normally when outgoing.state = 'open', confirming the fix doesn't break the happy path.

  3. closed state — Verifies the existing guard against outgoing.state = 'closed' still works.

  4. integration test — Sets up a real client/server connection via setupSimple, executes a command, monkeypatches channelWindowAdjust to detect post-close calls, destroys the stream (triggering close()'closing'), then manually invokes _read() to simulate a pending read callback. Asserts no window adjust was sent after close.

`windowAdjust()` in `lib/Channel.js` only guards against `outgoing.state === 'closed'` but not `'closing'`. After `close()` sets the state to `'closing'` and sends `SSH_MSG_CHANNEL_CLOSE` (message type 97), a pending `_read()` callback can still invoke `windowAdjust()`, which sends `SSH_MSG_CHANNEL_WINDOW_ADJUST` (message type 93) after `SSH_MSG_CHANNEL_CLOSE`.

This violates RFC 4254 Section 5.3:
> "Once a party has sent a CHANNEL_CLOSE message, it MUST NOT send further messages for the channel."
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.

1 participant