fix(sftp): send channel EOF+CLOSE when the SFTP session ends (fixes #215, sshj/Cyberduck hang)#216
Closed
Yaminyam wants to merge 2 commits into
Closed
Conversation
Both reasons the fork existed are now upstreamed: - high-frequency PTY output fix (Handle::data from spawned tasks): upstream #731, released in russh 0.62.0 - SHA-1 MAC exclusion from Preferred::DEFAULT: upstream #690, released in russh 0.60.2 So crates/bssh-russh is no longer needed. Switch the dependency to the crates.io russh 0.62.1 and remove the vendored fork (66 files). - Cargo.toml: russh = "0.62.1" (was package = bssh-russh, path fork) - ssh-key pin bumped =0.7.0-rc.10 -> =0.7.0-rc.11 to match russh 0.62.x - handler: adapt channel_open_session to the new russh 0.62 signature (ChannelOpenHandle reply + Result<()> instead of Result<bool>); accept the channel via reply.accept() - audit.toml: reword the RUSTSEC-2023-0071 note (no more vendored fork) Verified: cargo build, clippy, fmt clean, and full test suite green (1233 lib tests, PTY stress 10/10, streaming high-throughput 28/28).
7a4dc44 to
2ddd396
Compare
The SFTP subsystem ran `russh_sftp::server::run()`, which spawned the SFTP request loop on a detached task and returned nothing, so the handler had no completion hook and never sent the server-side channel EOF/CLOSE after the session finished (unlike the shell/exec/SCP paths, which send exit_status + eof + close). 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 ~30s before timing out. OpenSSH `sftp`/FileZilla tolerate the missing CLOSE, so it only showed up with sshj-based clients. - bssh-russh-sftp: `run`/`run_with_config` now return the spawned task's `JoinHandle<()>` (resolves on client EOF) so callers can observe when the session ends. Backward compatible — dropping the handle keeps the old fire-and-forget behaviour. Mirrors the upstream proposal AspectUnk/russh-sftp#93. - handler: await that handle on a detached task and then 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, `SFTPClient.close()` timed out after 30s; after, the session completes and closes in ~1s. OpenSSH `sftp` and paramiko are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2ddd396 to
dbc7ec7
Compare
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Fixes #215. sshj/JSch-based SFTP clients (Cyberduck, PyCharm/IntelliJ) hang ~30s then fail against
bssh-server, while OpenSSHsftp/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 the shell/exec/SCP paths). OpenSSH/FileZilla tolerate the missing CLOSE; sshj/JSch strictly wait for the server'sSSH_MSG_CHANNEL_CLOSE, soSFTPClient.close()blocks until its 30s timeout.Fix
bssh-russh-sftp:run_with_configruns the loop inline instead oftokio::spawn-ing internally, so the caller can observe completion. (run()is only called from the server handler.)handle.eof()+handle.close()(mirroring shell/exec/SCP). Also close on the user-info error paths.Verification
Minimal sshj 0.38 client (
canonicalize→stat→ls→close): ~30s → ~1s. Regression: OpenSSHsftp(ls/get/put), paramiko,cargo test -p bssh-russh-sftpall fine.