Skip to content

Fix the channel EOF and close lifecycle, add a public send-EOF API - #1195

Open
ejohnstown wants to merge 4 commits into
wolfSSL:masterfrom
ejohnstown:channel-eof
Open

Fix the channel EOF and close lifecycle, add a public send-EOF API#1195
ejohnstown wants to merge 4 commits into
wolfSSL:masterfrom
ejohnstown:channel-eof

Conversation

@ejohnstown

Copy link
Copy Markdown
Contributor

Three related bugs in the channel EOF and close path, plus the public API the half-close needs. RFC 4254 section 5.3 closes each direction of a channel independently, but the library answered every received EOF with one of its own, which silenced the local send direction; it had no way for an application to send an EOF on its own; it could put a second EOF on the wire on a retry; and it freed a channel before the peer's close arrived. Each is fixed below, with the daemon and example call sites updated to match and unit coverage for every change.

Issues: F-1687, F-8826, F-8839

F-1687 -- EOF auto-echo defeats the half-close

  • DoChannelEof() no longer echoes a received EOF. Echoing set our eofTxd, and SendChannelData() refuses to send once that bit is set, so a peer that finished sending silenced us too.
  • The EOF is reported instead: WS_EOF from wolfSSH_worker() with the channel id, and the existing channel EOF callback. Raised once, on arrival.
  • New wolfSSH_ChannelSendEof() and wolfSSH_stream_send_eof() send an EOF on its own. Previously all three SendChannelEof() callers bundled it with exit/close and the symbol is WOLFSSH_LOCAL, so an application had no way to reply. Sends afterwards fail with WS_EOF; reads keep working.
  • wolfSSH_stream_read() and wolfSSH_stream_peek() deliver data buffered ahead of the EOF and report it only once drained.
  • WS_EOF now reaches wolfSSH_worker() callers, so it is handled at the call sites in the library, apps, examples and tests.
  • wolfSSHd SHELL_Subsystem() closes the write end of the child's stdin pipe on the peer's half-close, so a non-interactive command reading to end-of-input returns. It keys on the channel's latched EOF state rather than one worker return, which also fixes an older bug: the check used the last channel to receive rather than the shell channel, so with agent forwarding an EOF on the agent channel closed the shell's stdin.
  • The SFTP loops in wolfSSHd and the echoservers drain what is buffered before leaving on the EOF.

F-8826 -- a retry queues a second EOF

  • SendChannelEof() set eofTxd only on WS_SUCCESS, so an application retrying after WS_WANT_WRITE bundled a second EOF behind the first.
  • The bit is now set once the packet is bundled, since that is the point at which it is committed to the output buffer.
  • This matches what SendChannelClose() already does for closeTxd.

F-8839 -- the channel is freed before the peer's close

  • wolfSSH_ChannelExit() removed and freed the channel as soon as it had sent the close.
  • The peer's CHANNEL_CLOSE then matched nothing, so DoChannelClose() returned WS_INVALID_CHANID and DoReceive() raised it as a fatal error on an ordinary shutdown.
  • The WOLFSSH_CHANNEL* the caller passed in was freed on return, so an application walking wolfSSH_ChannelNext() and exiting channels read freed memory.
  • It now sends the EOF and the close and stops. DoChannelClose() retires the channel when the peer's close arrives, which is what wolfSSH_shutdown() already did. ssh.h states how long the channel and its pointer stay valid.

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

Scan targets checked: wolfssh-bugs, wolfssh-src

Findings: 4
4 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 tests/unit.c
Comment thread src/ssh.c
Comment thread src/internal.c Outdated
Comment thread src/internal.c Outdated
Comment thread tests/unit.c
Comment thread src/ssh.c
Comment thread src/internal.c Outdated
Comment thread src/internal.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 #1195

Scan targets checked: wolfssh-bugs, wolfssh-src

Findings: 4
4 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

A received SSH_MSG_CHANNEL_EOF is a notification, not a command to answer
in kind. The library reports it instead of echoing one back, so a peer
that has finished sending does not silence the local send direction.

- Add wolfSSH_ChannelSendEof() and wolfSSH_stream_send_eof() so an
  application can close its own sending direction.
- Report the channel id with WS_EOF from wolfSSH_worker() and pass the
  status through the rekey mask; it is raised once, on arrival.
- Deliver data buffered ahead of the EOF from wolfSSH_stream_read() and
  wolfSSH_stream_peek(), reporting the EOF only once drained.
- Set eofTxd when the EOF packet is bundled, so a WS_WANT_WRITE retry
  does not queue a second EOF.
- Handle WS_EOF at the wolfSSH_worker() call sites in the library, apps,
  examples and tests.
- Close the child's stdin in wolfsshd on the peer's half-close, keyed on
  the channel's EOF state, so a command reading to end-of-input returns.
- Drain the buffered channel data in the SFTP loops of wolfsshd and the
  echoservers before leaving on the EOF.
- Add unit tests for the half-close, the send API and the retry, and an
  sshd suite test for a client that half-closes its stdin.

Issue: F-1687, F-8826
wolfSSH_ChannelExit() removed and freed the channel as soon as it had sent
the close. The peer's CHANNEL_CLOSE then matched nothing, so DoChannelClose()
returned WS_INVALID_CHANID and DoReceive() raised it as a fatal error on an
ordinary shutdown, and the channel pointer the caller passed in was already
freed on return.

- Send the EOF and the close, then leave the channel on the list.
  DoChannelClose() retires it when the peer's close arrives, the same as
  wolfSSH_shutdown() already does.
- DoChannelClose() sends an EOF ahead of its close reply, which RFC 4254
  section 5.3 asks for. Without it the peer's wolfSSH_shutdown() reads a
  bare close as its first packet and reports WS_CHANNEL_CLOSED.
- ssh.h states how long the channel and its pointer stay valid.
- unit.c: exit a channel, check it survives with eofTxd and closeTxd set,
  then feed the peer's close and check that is what retires it.

Issue: F-8839
Three regressions the PR review turned up in the new EOF and close path.

- DoChannelClose() gated SendChannelClose() on the new SendChannelEof()
  returning WS_SUCCESS. DoPacket() consumes the peer's CHANNEL_CLOSE
  whatever DoChannelClose() returns, so a blocked flush lost the
  mandatory close reply for good. Send both unconditionally and retire
  the channel once the close is bundled; the flush status still reaches
  the caller.
- DoChannelEof() now returns WS_EOF for whichever channel the EOF lands
  on, and wolfSSH_stream_read() only checked the head, so an EOF on an
  agent or forwarded channel came back as the head channel's EOF while
  it was still open. Filter it the way the adjacent WS_EXTDATA case
  does and keep waiting.
- Move WantWriteIoSend() out of the NO_WOLFSSH_SERVER block in the unit
  tests. test_SendChannelEofWantWrite() sits outside it and calls the
  helper, which broke the build with NO_WOLFSSH_SERVER defined.

Add unit coverage for the first two; both fail without the fix.

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

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 examples/echoserver/echoserver.c Outdated
Comment thread examples/echoserver/echoserver.c Outdated
Findings from the second review round on the EOF and close changes.

- wolfSSH_stream_read() held inputBuffer across the new EOF retry. When
  DoChannelClose() retired the head inside DoReceive() and the next head
  already had its EOF latched and drained, the override forced WS_EOF
  while lastRxId still named the channel that had gone, so the retry
  looped forever on freed memory. Both messages that get there are
  peer-controlled. Capture the head's id and only loop while it is
  unchanged.
- The EOF drain guards read a peek of WS_REKEYING as a drained channel.
  wolfSSH_stream_peek() reports it before it looks at the buffer, and
  wolfSSH_worker() no longer masks WS_EOF during a rekey, so this is
  reachable. Distinguish it in wolfsshd and both echoservers.
- wolfsshd closed the child's stdin without checking that the buffered
  channel data had been handed over. The read above it is skipped while
  windowFull, so a later read would write to a closed pipe.
- DoChannelClose() reported WS_WANT_WRITE and left lastRxId unset when
  the flush was back-pressured, costing the caller the graceful-close
  signal for a channel that is already closed and already bundled. The
  pending flush belongs to the output buffer, not the channel.
- test_SendEofApi's idempotence check could not fail: DiscardIoSend
  flushes everything, so the output buffer length was 0 either way.
  Count what reaches the transport instead.

Add coverage for the retry loop and the deferred flush; both fail
without the fix.
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