wolfsftp: keep the remote file when resuming a put - #1191
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a data-corruption bug in wolfSSH_SFTP_Put() when resuming an upload (reput): the destination file was previously opened with WOLFSSH_FXF_TRUNC unconditionally, which discarded the already-uploaded prefix and could silently produce a hole + tail.
Changes:
- Add a new
STATE_PUT_STAT_REMOTEstep to validate that the remote destination size matches the saved resume offset; otherwise reset the offset and restart from 0. - Make remote open truncation conditional on starting offset (
TRUNConly when offset is 0). - Add
test_wolfSSH_SFTP_PutResume()to validate correct behavior across resume and non-resume scenarios against the in-process echoserver.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/wolfsftp.c | Adds remote stat validation for resume offsets and makes truncation conditional on starting at offset 0 to prevent resumed-upload corruption. |
| tests/api.c | Adds a new API-level regression test covering resumed put behavior and ensuring correct remote file contents. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
1e832cf to
83fd93f
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1191
Scan targets checked: wolfssh-bugs, wolfssh-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
83fd93f to
6c18bd3
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1191
Scan targets checked: wolfssh-bugs, wolfssh-src
Fenrir result: Approved ✅
No new issues found in the changed files.
Advisory only — this automated result does not count as a GitHub approval.
Fenrir's latest completed scan found no issues; clearing the prior automated change request.
- wolfSSH_SFTP_Put() adds WOLFSSH_FXF_TRUNC to the destination open only when the write offset is zero. - STATE_PUT_LOOKUP_OFFSET clears a saved offset when the local file is no larger than it. - A new STATE_PUT_STAT_REMOTE stats the destination when the saved offset is nonzero, and clears the offset unless the reported size matches it exactly, or the stat returns WS_SFTP_STATUS_NOT_OK or WS_PERMISSIONS. Other stat failures re-save the offset and move to STATE_PUT_CLEANUP; a want-read or want-write keeps the state. WS_SFTP_PUT_STATE carries the attributes both states read. - The Windows server open maps WOLFSSH_FXF_CREAT to OPEN_ALWAYS and reserves CREATE_ALWAYS for an open that also asked for WOLFSSH_FXF_TRUNC; the disabled TRUNCATE_EXISTING mapping is dropped. - tests/api.c adds test_wolfSSH_SFTP_PutResume(), five cases over the resume paths, built where the hosted file wrappers are available. Issue: F-11659
6c18bd3 to
93548d8
Compare
Problem
Two defects, both leaving a corrupted file at the destination.
Client (
wolfSSH_SFTP_Put, f-11659): resume mode restores the saved offsetand seeks the local source forward, but still opens the destination with
WOLFSSH_FXF_TRUNC. Truncation is the open's job (draft-ietf-secsh-filexfer-02section 6.3), so the prefix already uploaded is discarded; the first write then
lands at the nonzero offset, which section 6.4 defines as zero-filling the gap.
A
reputyields a hole followed by the tail, with no error returned.Windows server (surfaced by the new test):
creationDisp |= ...combineddwCreationDispositionvalues thatCreateFileAdocuments as mutuallyexclusive.
WRITE|CREATbecameCREATE_ALWAYSand truncated a destination theclient asked to keep;
READ|WRITE|CREAT[|TRUNC]becameOPEN_EXISTING | CREATE_ALWAYS == OPEN_EXISTING, so it failed when the file wasabsent and never truncated when asked to. The POSIX server maps each flag
separately and is unaffected.
Fix (
src/wolfsftp.c)TRUNCis added only when the write offset is zero. Dropping it alone wouldstill hole a missing or short destination, and would newly allow stale trailing
bytes on a longer one, so a new
STATE_PUT_STAT_REMOTEvalidates first - thesame shape as OpenSSH's
sftp_upload()resume branch, which stats thedestination and never sends
TRUNCon a resume. It runs before the local open,where the offset is consumed (
WFSEEK, or theOVERLAPPEDseed on Windows),and does nothing unless the saved offset is nonzero, so a plain
putcosts noextra round trip.
wolfSSH_SFTP_STAT()on the destinationTRUNCTRUNCWS_SFTP_STATUS_NOT_OK,WS_PERMISSIONSTRUNCWS_WANT_READ/WS_WANT_WRITESTATE_PUT_LOOKUP_OFFSETalso drops the offset when the local source is nolarger than it - OpenSSH's "destination file same size or larger" case - which
otherwise resumes into a zero-byte transfer that reports success. It uses the
SFTP_GetAttributes()available on every port, so no extra round trip.The Windows disposition is now assigned, not OR'd:
WRITE|CREAT|APPEND, which OpenSSH'ssftpsends forreput, now maps toOPEN_ALWAYS, so a Windows wolfSSH server no longer destroys the destinationfor an OpenSSH resume.
WS_SFTP_PUT_STATEcarries the returned attributes; it is defined only inwolfsftp.c, so there is no header or ABI change.Tests (
tests/api.c)test_wolfSSH_SFTP_PutResume()is the first coverage of the resume path;scripts/get-put.testalready drives a plain put. Five cases, each checking thedestination byte-for-byte: resume onto a matching prefix, resume with the
destination deleted, resume with an offset that undershoots it, a plain put over
a longer destination, and a resume whose source is shorter than the offset. The
resume case stages a prefix unlike the source and expects it back untouched, so
a silent full re-upload fails the test. Excluded on Zephyr, which lacks the
hosted wrappers needed to stage the local source.
Verification
-Werrorsweep over 6 configs (default, sftp-only, scp-only,enable-all, smallstack, Zephyr defines): clean, no warnings.
make check, macOS--enable-all: 11 pass, 1 skip, 0 fail(
scripts/sftp.testandscripts/scp.testneed the serial re-run; theirparallel failure is a harness
counterbug, unrelated to this change).source-too-short case fails.
Not in this PR
SFTP_STAT()does not surface the SSH_FX status, soWS_SFTP_STATUS_NOT_OKalso covers
SSH_FX_FAILUREandSSH_FX_OP_UNSUPPORTED. The restart branchis commented to say so; distinguishing them needs a new
SFTP_STAT()return.SSH_FXF_EXCLandSSH_FXF_APPENDare still unmapped (pre-existing,inside the same
#if 0).uses a blocking socket. Likewise the stat's permission-denied restart and its
hard-error abort, both of which need a server that can fail a stat on demand.