Skip to content

update: sync bssh-russh-sftp with upstream russh-sftp 2.4.0 - #272

Merged
inureyes merged 2 commits into
mainfrom
update/russh-sftp-2.4.0
Aug 24, 2026
Merged

update: sync bssh-russh-sftp with upstream russh-sftp 2.4.0#272
inureyes merged 2 commits into
mainfrom
update/russh-sftp-2.4.0

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

Syncs the vendored bssh-russh-sftp fork with upstream russh-sftp 2.4.0, bringing in every upstream change released since the fork's 2.3.0 base. Upstream src/ is identical between 2.4.0 and current master, so this covers all of it.

Upstream changes vendored

The behavior fix: FileAttributes::default() used to return dummy attributes (every field Some(0)) while empty() returned omitted ones, making Default the opposite of what a caller would assume (upstream #89). Upstream now derives Default as the omitted form, makes empty() an alias for it, and moves the zero-filled placeholder to an explicit FileAttributes::dummy(), which protocol::File::new now calls so its wire output is unchanged.

bssh is unaffected: src/server/sftp.rs builds every FileAttributes and protocol::File with fully specified struct literals and never relies on Default (no ..Default::default() anywhere in that file). Verified by inspection and by SFTP round trips against both servers.

Also vendored:

  • File::close(), plus documentation that dropping a handle closes it without awaiting the reply (so pending write errors and the close status are discarded).
  • Client support for the expand-path@openssh.com extension: ExpandPathExtension, features.expand_path, RawSftpSession::expand_path, SftpSession::expand_path.
  • Doc link fixes.

Fork maintenance hazard fixed

Found while doing the sync, and worth flagging on its own.

src/server/mod.rs carried the request read-ahead intake queue and sequential-write coalescing from #227, roughly 750 changed lines, with no patch file. sync-upstream.sh deletes src/**/*.rs before copying upstream over it and then re-applies only patches/*.patch, so the next sync would have silently deleted that work. create-patch.sh would not have caught it either, because it only ever diffed client/fs/file.rs.

Fixed three ways:

  1. The change is captured in patches/server-readahead-write-coalescing.patch.
  2. create-patch.sh now regenerates one patch per entry in a PATCH_TARGETS list, and warns about any other vendored file that drifts from upstream without an entry.
  3. The README documents the second patch and the deletion hazard.

Both patch files were verified to apply cleanly onto a pristine upstream 2.4.0 checkout, and create-patch.sh reproduces both byte-identically.

Merge method

Rather than run the destructive sync-upstream.sh, the five files with no local drift were copied verbatim from upstream 2.4.0 and the upstream client/fs/file.rs delta was applied as a patch onto the forked copy. The result is upstream 2.4.0 plus exactly two additive fork changes, with no upstream line lost: diff against pristine 2.4.0 shows only the fork's own hunks.

Validation

  • cargo check --workspace --all-targets, cargo clippy --workspace --all-targets --all-features -- -D warnings, cargo fmt --all -- --check: clean.
  • Full workspace test suite with --no-fail-fast: bssh lib 1407 passed, bssh-russh-sftp 8 passed, all other binaries green. The only failures are the three integration_test cases that CI already skips, which fail on a stale localhost entry in the developer's ~/.ssh/known_hosts and reproduce on main.
  • End-to-end SFTP re-verified after the sync: round trip against bssh-server, plus 300 KB and 3 MB round trips against OpenSSH 10.3p1, all with matching SHA-256. The 3 MB case exercises both pipelined helpers across many chunks.

Refs #227

Brings in every upstream change released since the fork's 2.3.0 base. Upstream `src/` is identical between 2.4.0 and current master, so this covers all of it.

The behavior fix is `FileAttributes::default()`: it used to return *dummy* attributes (every field `Some(0)`) while `empty()` returned omitted ones, which made `Default` the opposite of what a caller would assume. Upstream now derives `Default` as the omitted form, makes `empty()` an alias for it, and moves the zero-filled placeholder to an explicit `FileAttributes::dummy()`, which `protocol::File::new` now calls so its wire output is unchanged. bssh is unaffected: `src/server/sftp.rs` builds every `FileAttributes` and `protocol::File` with fully specified struct literals and never uses `Default`, verified by inspection and by an SFTP round trip against both servers.

Also vendored: `File::close()` plus documentation that dropping a handle closes it without awaiting the reply, and client support for the `expand-path@openssh.com` extension (`ExpandPathExtension`, `features.expand_path`, `RawSftpSession::expand_path`, `SftpSession::expand_path`).

Fixes a fork maintenance hazard found while doing the sync. `src/server/mod.rs` carried the request read-ahead intake queue and sequential-write coalescing from issue #227, roughly 750 changed lines, with no patch file. Since `sync-upstream.sh` deletes `src/**/*.rs` before copying upstream over it and then re-applies only `patches/*.patch`, the next sync would have silently deleted that work; `create-patch.sh` would not have noticed because it only ever diffed `client/fs/file.rs`. That change is now captured in `patches/server-readahead-write-coalescing.patch`, `create-patch.sh` regenerates a patch per entry in a `PATCH_TARGETS` list and warns about any other vendored file that drifts from upstream without an entry, and the README documents both the second patch and the deletion hazard.

Both patch files were verified to apply cleanly onto a pristine upstream 2.4.0 checkout, and `create-patch.sh` reproduces both byte-identically.

Validation: `cargo check --workspace --all-targets`, `cargo clippy --workspace --all-targets --all-features -- -D warnings`, and `cargo fmt --all -- --check` clean. Full workspace test suite green (bssh lib 1407 passed, bssh-russh-sftp 8 passed) except the three `integration_test` cases that CI already skips and that fail on a stale localhost entry in the developer's `~/.ssh/known_hosts`. End-to-end SFTP re-verified after the sync: round trip against bssh-server, and 300 KB plus 3 MB round trips against OpenSSH 10.3p1, all with matching SHA-256.

Refs #227
@inureyes inureyes added type:dependency Dependency updates priority:medium Medium priority issue status:review Under review labels Aug 24, 2026
Running `./sync-upstream.sh 2.4.0` for real, instead of trusting it, surfaced two defects that between them made the documented sync workflow destructive.

Version resolution never worked. russh-sftp publishes no git tags at all, so `git checkout v2.4.0 || git checkout 2.4.0` failed both ways and `set -e` aborted the script; the command in the README could not have run. Upstream marks releases with a `bump to <version>` commit, so both scripts now resolve a version argument to that commit. An unresolvable version is a hard error that lists the available release commits rather than a silent fall back to the default branch, which would vendor unreleased code while stamping Cargo.toml with the requested version. Resolution runs before anything is copied, so a bad version leaves the tree untouched.

Patch-state detection was inverted-safe in name only. Apple's bundled `patch` (2.0-12u11) silently auto-corrects direction: with no tty it answers "yes" to `Unreversed (or previously applied) patch detected! Ignore -R?` and exits 0 for a forward patch, a reverse patch, an applied patch and an unapplied one alike. The obsolete-detection probe was `patch -p1 -R --dry-run`, so it succeeded unconditionally and every fork patch was classified "already present in upstream" and skipped. The observable result: a sync wiped both fork changes and the fork's test count silently dropped from 8 to 0, because the tests live inside the patched files. Detection now uses `git apply --check`, which never prompts and distinguishes the three real states (applies, already present, conflicts).

The post-sync guard added alongside the 2.4.0 sync was subject to the same flaw, since it skipped anything the broken probe had marked obsolete and used the same `patch -R` probe itself. It now reverse-checks with `git apply` too, and the script additionally runs the fork tests, so a lost change fails the sync instead of passing quietly.

Verified end to end rather than by inspection: `./sync-upstream.sh 2.4.0` now applies both patches, reports both present, passes 8 fork tests, and leaves `git diff` on the vendored tree completely empty, so the script reproduces the committed source byte for byte. `./create-patch.sh 2.4.0` resolves to the same commit and regenerates both patches byte-identically. Both scripts exit 1 on an unresolvable version without modifying the tree.

Refs #227
@inureyes

Copy link
Copy Markdown
Member Author

Pushed b87089d. While checking that this PR left the fork in a maintainable state for the next sync, I ran ./sync-upstream.sh 2.4.0 for real instead of trusting it, and it turned out the sync workflow was destructive in two independent ways.

Version resolution never worked. russh-sftp publishes no git tags at all, so git checkout v2.4.0 || git checkout 2.4.0 failed both ways and set -e aborted the script. The command documented in the README could not have run. Both scripts now resolve a version argument to upstream's bump to <version> release commit, and treat an unresolvable version as a hard error listing the available release commits rather than falling back to the default branch (which would vendor unreleased code while stamping Cargo.toml with the requested version). Resolution happens before anything is copied, so a bad version leaves the tree untouched.

Patch-state detection was broken in a way that silently deleted the fork. Apple's bundled patch (2.0-12u11) auto-corrects direction: with no tty it answers "yes" to Unreversed (or previously applied) patch detected! Ignore -R? and exits 0 for a forward patch, a reverse patch, an applied patch and an unapplied one alike. The obsolete-detection probe was patch -p1 -R --dry-run, so it succeeded unconditionally and classified every fork patch as "already present in upstream", skipping it. The observable result of the real run:

[INFO] Skipping pipelined-file-io.patch — already present in upstream
[INFO] Skipping server-readahead-write-coalescing.patch — already present in upstream
...
running 0 tests

Both fork changes were gone from the working tree, and the fork's test count had dropped from 8 to 0 without failing anything, because the tests live inside the patched files. Detection now uses git apply --check, which never prompts and distinguishes the three real states (applies / already present / conflicts).

This also means the post-sync guard added in 840fb64 was useless: it skipped whatever the broken probe had marked obsolete, and used the same patch -R probe itself. It now reverse-checks with git apply, and the script runs the fork tests as well, so a lost change fails the sync loudly.

Verified by running it, not by reading it: ./sync-upstream.sh 2.4.0 applies both patches, reports both present, passes 8 fork tests, and leaves git diff on the vendored tree completely empty, so the script reproduces the committed source byte for byte. ./create-patch.sh 2.4.0 resolves to the same commit (6606701) and regenerates both patches byte-identically. Both scripts exit 1 on an unresolvable version without touching the tree.

@inureyes inureyes self-assigned this Aug 24, 2026
@inureyes
inureyes merged commit 65516df into main Aug 24, 2026
3 checks passed
@inureyes
inureyes deleted the update/russh-sftp-2.4.0 branch August 24, 2026 05:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority:medium Medium priority issue status:review Under review type:dependency Dependency updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant