From 90308de5d1fb3f360e832bcda26fe515fe141fb5 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Sun, 24 May 2026 14:34:59 +0100 Subject: [PATCH] serve sftp: fix truncate request being silently ignored The SFTP serve handler ignored the size attribute of SETSTAT/FSETSTAT requests, only acting on the modification time. This meant a client asking to truncate a file (eg setting the final size of an upload, or an explicit truncate) had no effect at all. This respects the size attribute (if present) by truncating the file to the requested size. --- cmd/serve/sftp/handler.go | 13 ++++++++++++- cmd/serve/sftp/handler_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/cmd/serve/sftp/handler.go b/cmd/serve/sftp/handler.go index 985b5998a..559580064 100644 --- a/cmd/serve/sftp/handler.go +++ b/cmd/serve/sftp/handler.go @@ -64,7 +64,18 @@ func (v vfsHandler) Filecmd(r *sftp.Request) error { switch r.Method { case "Setstat": attr := r.Attributes() - if attr.Mtime != 0 { + flags := r.AttrFlags() + // A size attribute is a request to truncate the file + if flags.Size { + node, err := v.Stat(r.Filepath) + if err != nil { + return err + } + if err := node.Truncate(int64(attr.Size)); err != nil { + return err + } + } + if flags.Acmodtime { modTime := time.Unix(int64(attr.Mtime), 0) err := v.Chtimes(r.Filepath, modTime, modTime) if err != nil { diff --git a/cmd/serve/sftp/handler_test.go b/cmd/serve/sftp/handler_test.go index 1b91c8dc0..b41d09cc9 100644 --- a/cmd/serve/sftp/handler_test.go +++ b/cmd/serve/sftp/handler_test.go @@ -138,6 +138,36 @@ func TestFilewriteTruncate(t *testing.T) { assert.Equal(t, newContents, string(got)) } +// Test that a SETSTAT request with a size attribute truncates the file, rather +// than being silently ignored. +func TestSetstatTruncate(t *testing.T) { + vfsOpt := vfscommon.Opt + vfsOpt.CacheMode = vfscommon.CacheModeWrites + client := startTestServer(t, &vfsOpt) + + const fileName = "file.bin" + + // Write a file and then truncate it via FSETSTAT (a size attribute) on the + // open handle, the way a client setting the final size of an upload does. + f, err := client.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) + require.NoError(t, err) + _, err = f.Write([]byte(strings.Repeat("A", 1024))) + require.NoError(t, err) + require.NoError(t, f.Truncate(10)) + require.NoError(t, f.Close()) + + fi, err := client.Stat(fileName) + require.NoError(t, err) + assert.Equal(t, int64(10), fi.Size(), "file not truncated to requested size") + + rd, err := client.Open(fileName) + require.NoError(t, err) + got, err := io.ReadAll(rd) + require.NoError(t, err) + require.NoError(t, rd.Close()) + assert.Equal(t, strings.Repeat("A", 10), string(got)) +} + // writeFile writes contents to fileName via the client truncating any existing // data, the way a normal upload does. func writeFile(client *sftp.Client, fileName, contents string) error {