Skip to content

wolfsftp: keep the remote file when resuming a put - #1191

Merged
ejohnstown merged 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_11659
Aug 26, 2026
Merged

wolfsftp: keep the remote file when resuming a put#1191
ejohnstown merged 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_11659

Conversation

@yosuke-wolfssl

@yosuke-wolfssl yosuke-wolfssl commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Two defects, both leaving a corrupted file at the destination.

Client (wolfSSH_SFTP_Put, f-11659): resume mode restores the saved offset
and seeks the local source forward, but still opens the destination with
WOLFSSH_FXF_TRUNC. Truncation is the open's job (draft-ietf-secsh-filexfer-02
section 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 reput yields a hole followed by the tail, with no error returned.

Windows server (surfaced by the new test): creationDisp |= ... combined
dwCreationDisposition values that CreateFileA documents as mutually
exclusive. WRITE|CREAT became CREATE_ALWAYS and truncated a destination the
client asked to keep; READ|WRITE|CREAT[|TRUNC] became
OPEN_EXISTING | CREATE_ALWAYS == OPEN_EXISTING, so it failed when the file was
absent and never truncated when asked to. The POSIX server maps each flag
separately and is unaffected.

Fix (src/wolfsftp.c)

TRUNC is added only when the write offset is zero. Dropping it alone would
still hole a missing or short destination, and would newly allow stale trailing
bytes on a longer one, so a new STATE_PUT_STAT_REMOTE validates first - the
same shape as OpenSSH's sftp_upload() resume branch, which stats the
destination and never sends TRUNC on a resume. It runs before the local open,
where the offset is consumed (WFSEEK, or the OVERLAPPED seed on Windows),
and does nothing unless the saved offset is nonzero, so a plain put costs no
extra round trip.

wolfSSH_SFTP_STAT() on the destination Action
size equals the saved offset resume, no TRUNC
size differs, or no size attribute restart from 0 with TRUNC
WS_SFTP_STATUS_NOT_OK, WS_PERMISSIONS restart from 0 with TRUNC
WS_WANT_READ / WS_WANT_WRITE retry in place
any other failure re-save the offset, cleanup, error propagates

STATE_PUT_LOOKUP_OFFSET also drops the offset when the local source is no
larger 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:

if (reason & WOLFSSH_FXF_TRUNC)
    creationDisp = CREATE_ALWAYS;
else
    creationDisp = OPEN_ALWAYS;

WRITE|CREAT|APPEND, which OpenSSH's sftp sends for reput, now maps to
OPEN_ALWAYS, so a Windows wolfSSH server no longer destroys the destination
for an OpenSSH resume.

WS_SFTP_PUT_STATE carries the returned attributes; it is defined only in
wolfsftp.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.test already drives a plain put. Five cases, each checking the
destination 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

  • gcc-13 -Werror sweep 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.test and scripts/scp.test need the serial re-run; their
    parallel failure is a harness counter bug, unrelated to this change).
  • Negative control: with the offset-validation hunk reverted, the new
    source-too-short case fails.

Not in this PR

  • SFTP_STAT() does not surface the SSH_FX status, so WS_SFTP_STATUS_NOT_OK
    also covers SSH_FX_FAILURE and SSH_FX_OP_UNSUPPORTED. The restart branch
    is commented to say so; distinguishing them needs a new SFTP_STAT() return.
  • Windows SSH_FXF_EXCL and SSH_FXF_APPEND are still unmapped (pre-existing,
    inside the same #if 0).
  • The new state's want-read re-entry is not covered in-tree; the test client
    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.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Aug 24, 2026
Copilot AI lite review requested due to automatic review settings August 24, 2026 04:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_REMOTE step 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 (TRUNC only 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.

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/api.c Outdated
Comment thread tests/api.c Outdated

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@wolfSSL-Fenrir-bot
wolfSSL-Fenrir-bot dismissed their stale review August 24, 2026 05:47

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
@ejohnstown
ejohnstown merged commit faca3bc into wolfSSL:master Aug 26, 2026
165 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants