Skip to content

Commit dd6e7e3

Browse files
authored
feat: vendor russh-sftp with serde_bytes perf fix (#188)
Vendor russh-sftp as bssh-russh-sftp and route SFTP Write/Data payloads through serde_bytes bulk byte handling. Review follow-up hardens packet length handling with checked u32 conversions, removes an extra byte-buffer copy, adds wire-format tests, narrows the lockfile delta, fixes clippy, and makes Keychain-backed tests skip cleanly when local authorization is unavailable. Verified with cargo test -p bssh-russh-sftp --locked, cargo test --lib --locked, cargo test --tests --locked -- --skip integration_test, cargo fmt --check, and cargo clippy --workspace --locked -- -D warnings.
1 parent 60e1bec commit dd6e7e3

56 files changed

Lines changed: 4770 additions & 26 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Internal fork of `russh-sftp` as `crates/bssh-russh-sftp` with a `serde_bytes` performance fix for `SSH_FXP_WRITE` and `SSH_FXP_DATA` packets. The upstream serde derive routes `Vec<u8>` through `deserialize_seq` (byte-by-byte), accounting for ~42% of server CPU during 1 GiB SFTP uploads in `perf` profiling. Annotating the `data` fields with `#[serde(with = "serde_bytes")]` and implementing wire-compatible `serialize_bytes` on the SFTP `Serializer` routes through the existing bulk `deserialize_byte_buf`/`try_get_bytes` path. Measured impact on a CPU-bound host (Xeon Silver 4214): 1 GiB SFTP upload throughput improves from 74.8 MiB/s to 96.4 MiB/s (+29%), closing the gap to OpenSSH `sftp-server` from ~26% to ~5%.
12+
13+
### Changed
14+
- Switched the top-level `russh-sftp` dependency from crates.io `russh-sftp = "2.1.1"` to `russh-sftp = { package = "bssh-russh-sftp", version = "2.1.1", path = "crates/bssh-russh-sftp" }`. All existing `use russh_sftp::...` imports continue to work unchanged.
15+
1016
## [2.1.2] - 2026-04-27
1117

1218
### Fixed

Cargo.lock

Lines changed: 28 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
members = [
33
".",
44
"crates/bssh-russh",
5+
"crates/bssh-russh-sftp",
56
]
67

78
[package]
@@ -23,7 +24,8 @@ tokio = { version = "1.52.1", features = ["full"] }
2324
# - Development: uses local path (crates/bssh-russh)
2425
# - Publishing: uses crates.io version (path ignored)
2526
russh = { package = "bssh-russh", version = "0.60.1", path = "crates/bssh-russh" }
26-
russh-sftp = "2.1.1"
27+
# Use our internal russh-sftp fork with a serde_bytes perf fix
28+
russh-sftp = { package = "bssh-russh-sftp", version = "2.1.1", path = "crates/bssh-russh-sftp" }
2729
clap = { version = "4.6.1", features = ["derive", "env"] }
2830
anyhow = "1.0.102"
2931
thiserror = "2.0.18"

crates/bssh-russh-sftp/Cargo.toml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "bssh-russh-sftp"
3+
version = "2.1.1"
4+
authors = ["Jeongkyu Shin <inureyes@gmail.com>"]
5+
description = "Temporary fork of russh-sftp with a serde_bytes performance fix for SFTP Write/Data packets"
6+
documentation = "https://docs.rs/bssh-russh-sftp"
7+
edition = "2021"
8+
homepage = "https://github.com/lablup/bssh"
9+
keywords = ["russh", "sftp", "ssh2", "server", "client"]
10+
license = "Apache-2.0"
11+
readme = "README.md"
12+
repository = "https://github.com/lablup/bssh"
13+
14+
[dependencies]
15+
tokio = { version = "1", default-features = false, features = [
16+
"io-util",
17+
"rt",
18+
"sync",
19+
"time",
20+
"macros",
21+
] }
22+
tokio-util = "0.7"
23+
serde = { version = "1.0", features = ["derive"] }
24+
serde_bytes = "0.11"
25+
bitflags = { version = "2.9", features = ["serde"] }
26+
async-trait = { version = "0.1", optional = true }
27+
28+
thiserror = "2.0"
29+
chrono = "0.4"
30+
bytes = "1.10"
31+
log = "0.4"
32+
flurry = "0.5"
33+
34+
[features]
35+
async-trait = ["dep:async-trait"]

crates/bssh-russh-sftp/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# bssh-russh-sftp
2+
3+
Temporary fork of [russh-sftp](https://crates.io/crates/russh-sftp) with a `serde_bytes` performance fix for SFTP `Write` and `Data` packets.
4+
5+
This crate exists so bssh can ship the packet serialization fix independently while keeping the public crate name usable through Cargo's `package = "bssh-russh-sftp"` dependency alias.
6+
7+
## The Problem
8+
9+
`russh-sftp` 2.1.1 derives serde for `Vec<u8>` fields in `SSH_FXP_WRITE` and `SSH_FXP_DATA`. With the crate's custom deserializer, that routes through `deserialize_seq` and reads payload bytes one at a time. Large transfers spend substantial CPU in serde's generic `VecVisitor` path.
10+
11+
## The Fix
12+
13+
The fork annotates the binary payload fields with `#[serde(with = "serde_bytes")]` and implements compatible `serialize_bytes` framing in the SFTP serializer. The wire format remains `u32 length + bytes`, but deserialization uses the existing bulk byte-buffer path.
14+
15+
## Sync with Upstream
16+
17+
```bash
18+
cd crates/bssh-russh-sftp
19+
./sync-upstream.sh 2.1.1
20+
```
21+
22+
Local changes are kept as patch files under `patches/`.
23+
24+
## License
25+
26+
Apache-2.0 (same as russh-sftp)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
# create-patch.sh
3+
# Creates a patch file from the current bssh-russh-sftp changes compared to upstream russh-sftp.
4+
#
5+
# Usage: ./create-patch.sh
6+
7+
set -e
8+
9+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10+
BSSH_ROOT="$SCRIPT_DIR/../.."
11+
UPSTREAM_DIR="$BSSH_ROOT/references/russh-sftp/src"
12+
CURRENT_DIR="$SCRIPT_DIR/src"
13+
PATCH_DIR="$SCRIPT_DIR/patches"
14+
PATCH_FILE="$PATCH_DIR/sftp-serde-bytes-perf.patch"
15+
16+
GREEN='\033[0;32m'
17+
YELLOW='\033[1;33m'
18+
NC='\033[0m'
19+
20+
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
21+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
22+
23+
if [ ! -d "$UPSTREAM_DIR" ]; then
24+
echo "Error: Upstream russh-sftp not found at $UPSTREAM_DIR"
25+
echo "Please ensure references/russh-sftp exists with the upstream source."
26+
exit 1
27+
fi
28+
29+
mkdir -p "$PATCH_DIR"
30+
31+
log_info "Creating patch from differences..."
32+
33+
/usr/bin/diff -urN "$UPSTREAM_DIR" "$CURRENT_DIR" \
34+
| sed "s|$UPSTREAM_DIR|a/src|g" \
35+
| sed "s|$CURRENT_DIR|b/src|g" \
36+
> "$PATCH_FILE" || true
37+
38+
if [ -s "$PATCH_FILE" ]; then
39+
LINES=$(wc -l < "$PATCH_FILE" | tr -d ' ')
40+
log_info "Patch created: $PATCH_FILE ($LINES lines)"
41+
42+
echo ""
43+
echo "Patch summary:"
44+
echo "=============="
45+
grep -E "^@@|^\+\+\+|^---" "$PATCH_FILE" | head -20
46+
else
47+
log_warn "No differences found - patch file is empty"
48+
fi

0 commit comments

Comments
 (0)