Skip to content

fix(sftp): send channel EOF+CLOSE when the SFTP session ends (fixes sshj/Cyberduck hang)#1

Closed
Yaminyam wants to merge 1 commit into
chore/drop-bssh-russh-forkfrom
fix/sftp-send-channel-close-on-session-end
Closed

fix(sftp): send channel EOF+CLOSE when the SFTP session ends (fixes sshj/Cyberduck hang)#1
Yaminyam wants to merge 1 commit into
chore/drop-bssh-russh-forkfrom
fix/sftp-send-channel-close-on-session-end

Conversation

@Yaminyam

@Yaminyam Yaminyam commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Problem

Fixes lablup#215. sshj/JSch-based SFTP clients (Cyberduck, PyCharm/IntelliJ) hang ~30s then fail against bssh-server, while OpenSSH sftp/FileZilla work fine.

Root cause: the SFTP subsystem ran russh_sftp::server::run(), which spawned the request loop on a detached task and returned immediately — so when the session ended, the handler never sent the server-side channel EOF/CLOSE (unlike shell/exec/SCP). OpenSSH/FileZilla tolerate the missing CLOSE; sshj/JSch strictly wait for the server's SSH_MSG_CHANNEL_CLOSE, so SFTPClient.close() blocks until its 30s timeout.

Fix

  • bssh-russh-sftp: run_with_config no longer tokio::spawns internally — runs the loop inline and returns on client EOF. run() is only called from the server handler.
  • handler: run the SFTP session on our own task and, on completion, send handle.eof() + handle.close() (mirroring shell/exec/SCP). Also close on user-info error paths.

Verification

Minimal sshj 0.38 client (canonicalizestatlsclose): ~30s → ~1s. Regression: OpenSSH sftp (ls/get/put fine), paramiko (fine), cargo test -p bssh-russh-sftp (passes).

Stacked on lablup#212 (russh 0.62 migration).

The SFTP subsystem ran `russh_sftp::server::run()` which spawned the SFTP
request loop on a detached task and returned immediately, so the handler
never sent the server-side channel EOF/CLOSE after the session finished
(unlike the shell/exec/SCP paths, which do). Clients that block on the
SSH channel-close handshake — notably sshj/JSch (Cyberduck, PyCharm,
IntelliJ) — then wait for a CLOSE that never arrives and hang for ~30s
before timing out. OpenSSH `sftp`/FileZilla tolerate the missing CLOSE,
which is why the problem only showed up with sshj-based clients.

- bssh-russh-sftp: `run_with_config` no longer spawns internally; it runs
  the request loop inline and returns on client EOF, so the caller can
  observe completion and clean up the channel.
- handler: run the SFTP session on our own task and, when it ends, send
  `handle.eof()` + `handle.close()` (mirroring the shell/exec/SCP paths).
  Also close the channel on the user-info error paths.

Reproduced with an sshj 0.38 client (`canonicalize` + `stat` + `ls` +
close): before, the `SFTPClient.close()` timed out after 30s; after, the
session completes and closes in ~1s. OpenSSH `sftp` and paramiko continue
to work unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Yaminyam

Yaminyam commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Re-targeting to lablup/bssh (upstream). See the new PR there.

@Yaminyam Yaminyam closed this Jul 6, 2026
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