-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Fix PTY shell output and publish bssh-russh crate #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Conversation
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
- macOS: /Users/{username} with /bin/zsh
- Linux: /home/{username} with /bin/sh
- Windows: C:\Users\{username} with cmd.exe
Also fixes macOS TIOCSCTTY type casting issue in shell.rs
The shell_request handler was returning immediately after starting the shell, while the I/O tasks continued running in the background. This caused handle.data() calls to succeed but data was not reaching the SSH client. Changes: - Add run_until_exit() method to ShellSession that waits for the shell process and I/O tasks to complete - Store I/O task handles (JoinHandle) in ShellSession - Add shell_data_tx and shell_pty fields to ChannelState for data/resize handlers to access while shell_request is waiting - Update shell_request to wait for shell completion before returning, similar to how exec_request waits for command completion - Send exit_status, eof, and close channel when shell exits
The russh library doesn't process Handle messages while a handler future is being awaited. The previous approach spawned I/O tasks that called handle.data(), but those messages were queued and never sent because the handler was blocked waiting. This fix runs the I/O loop directly in the handler's async context using tokio::select! to multiplex: - Reading from PTY (shell output) and sending to SSH - Receiving SSH data and writing to PTY - Checking if the shell process exited Key changes: - Remove tokio::spawn for I/O tasks in ShellSession - New run() method that runs the full I/O loop - Simplified ChannelState (removed shell_session field) - Handler calls shell_session.run() directly This ensures each I/O iteration yields control back to russh's event loop, allowing data to actually be transmitted over the SSH channel.
…elect The previous implementation had try_wait() wrapped in an async block inside a biased tokio::select!. Since try_wait() is synchronous, it completed immediately on every iteration, causing a busy loop that starved the PTY read branch. Fixed by moving the child exit check outside the select! block, running it once at the start of each loop iteration. The select! now only contains truly async operations (PTY read and SSH data receive), ensuring fair polling. Also fixes clippy warnings in password.rs (abs_diff) and sftp.rs (io_other_error).
The previous approach blocked the russh handler while running the I/O loop, preventing the session from processing outgoing Handle::data() messages. Now ShellSession::run() spawns a separate tokio task for the I/O loop and returns immediately. This allows: 1. The handler to return, unblocking the russh event loop 2. The session to process queued outgoing messages 3. Bidirectional data flow between PTY and SSH channel The spawned task handles exit_status_request, eof, and channel close when the shell process terminates.
Added logging before and after handle.data().await to confirm: 1. PTY read data is being read 2. handle.data() call is being made 3. handle.data().await is completing (or blocking) Also refactored ShellSession to expose helper methods for spawned task approach: - spawn_shell_process() - spawn shell before taking resources - take_data_receiver() - get receiver for I/O loop - take_child() - get child process for I/O loop - channel_id() - get channel ID Removed unused run() method that took Session reference.
Switch from Handle::data() to ChannelStream for shell output, following the same pattern used by SFTP. This avoids message queue coordination issues that caused shell output to not reach SSH clients. Changes: - shell_request now takes channel ownership and creates ChannelStream - run_shell_io_loop uses ChannelStream's AsyncRead/AsyncWrite - Added session_notify mechanism to russh's ChannelTx for reliable cross-task message delivery
- Internalize russh as bssh-russh crate for customization - Change PTY lock from Mutex to RwLock to allow concurrent read/write - Add proper SSH channel closure sequence (exit_status, eof, close) - Add timeout on PTY reads to prevent lock starvation The PTY deadlock occurred because Mutex required exclusive access, but PTY read() and write() only need shared reference. The output task held the lock while waiting for data, blocking the main loop from writing user input. RwLock allows concurrent operations since both only need &self.
Remove internalized bssh-cryptovec and bssh-russh-util crates since they had no code modifications beyond import renames. Only bssh-russh needs to remain internalized as it contains the PTY fix in server/session.rs. - Remove crates/bssh-cryptovec directory - Remove crates/bssh-russh-util directory - Update bssh-russh to depend on public russh-cryptovec and russh-util - Revert import names to original russh_* naming
Document the issue, root cause, and proposed fix for Handle::data() messages not being processed from spawned tasks. This prepares for potential upstream contribution to russh.
Limit the number of messages processed per batch (64) before yielding to select! to check for client input. This ensures that during high-throughput output (e.g., `yes` command), client signals like Ctrl+C are handled promptly instead of being delayed until all pending output messages are processed.
Add batch limit explanation and update code example to reflect the improved implementation that prevents input starvation.
- Add package metadata (author, description, license, etc.) - Create README.md explaining the fork purpose - Add sync-upstream.sh for tracking upstream releases - Add create-patch.sh for patch management - Add patches/handle-data-fix.patch
- Local development uses path (crates/bssh-russh) - Publishing uses crates.io version
- handler tests: session_id is now assigned at creation time - timing test: increase tolerance to 100ms for CI environments
Timing tests are inherently flaky in shared CI environments. Run locally with: cargo test test_password_verifier_timing_attack_mitigation --lib -- --ignored
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
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.
Summary
bssh-russhcrate with the fix appliedbssh-russh v0.56.0to crates.io for public useProblem
When implementing SSH servers with PTY support, messages sent via
Handle::data()from spawned tasks may not be delivered to the client. Thetokio::select!in russh's server session loop doesn't always wake up promptly for messages sent through the internal mpsc channel.Solution
Added a
try_recv()batch processing loop beforeselect!inserver/session.rsto drain pending messages, with a limit of 64 messages per batch to maintain input responsiveness (e.g., Ctrl+C).Changes
bssh-russh crate
crates/bssh-russh/PTY Shell improvements
MutextoRwLockfor PTY operations to allow concurrent read/writeOther
russh-cryptovecandrussh-utilcrates (no modifications needed)Upstream Status
PR branch prepared at: https://github.com/inureyes/russh/tree/fix/handle-data-from-spawned-tasks
Test Plan
seq 1 500,yes | head -100)yescommand)