diff --git a/cmd/serve/nfs/filesystem.go b/cmd/serve/nfs/filesystem.go index c0989a660..2ee8eca5c 100644 --- a/cmd/serve/nfs/filesystem.go +++ b/cmd/serve/nfs/filesystem.go @@ -186,16 +186,7 @@ func (f *FS) Readlink(link string) (result string, err error) { func (f *FS) Chmod(name string, mode os.FileMode) (err error) { name = f.fullPath(name) defer log.Trace(name, "mode=%v", mode)("err=%v", &err) - file, err := f.vfs.Open(name) - if err != nil { - return err - } - defer func() { - if err := file.Close(); err != nil { - fs.Logf(f, "Error while closing file: %e", err) - } - }() - err = file.Chmod(mode) + err = f.vfs.Chmod(name, mode) // Mask Chmod not implemented if err == vfs.ENOSYS { err = nil @@ -213,16 +204,12 @@ func (f *FS) Lchown(name string, uid, gid int) (err error) { func (f *FS) Chown(name string, uid, gid int) (err error) { name = f.fullPath(name) defer log.Trace(name, "uid=%d, gid=%d", uid, gid)("err=%v", &err) - file, err := f.vfs.Open(name) - if err != nil { - return err + err = f.vfs.Chown(name, uid, gid) + // Mask Chown not implemented + if err == vfs.ENOSYS { + err = nil } - defer func() { - if err := file.Close(); err != nil { - fs.Logf(f, "Error while closing file: %e", err) - } - }() - return file.Chown(uid, gid) + return err } // Chtimes changes the access time and modified time diff --git a/cmd/serve/nfs/filesystem_test.go b/cmd/serve/nfs/filesystem_test.go new file mode 100644 index 000000000..45596bfab --- /dev/null +++ b/cmd/serve/nfs/filesystem_test.go @@ -0,0 +1,41 @@ +//go:build unix + +package nfs + +import ( + "testing" + + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfscommon" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Chmod/Chown arrive as plain SETATTR calls on the link path after a +// SYMLINK RPC, so they must not follow symlinks - a freshly created +// symlink usually dangles and following it would fail with ENOENT, +// which the NFS layer surfaces as an IO error. See #9627. +func TestChmodDanglingSymlink(t *testing.T) { + ctx := t.Context() + f, err := fs.NewFs(ctx, t.TempDir()) + require.NoError(t, err) + opt := vfscommon.Opt + opt.Links = true + opt.CacheMode = vfscommon.CacheModeWrites + v := vfs.New(ctx, f, &opt) + defer v.Shutdown() + bfs := &FS{vfs: v} + + // Create a symlink pointing at a target which doesn't exist yet + require.NoError(t, bfs.Symlink("does-not-exist", "link")) + + // SETATTR after SYMLINK must not fail + assert.NoError(t, bfs.Chmod("link", 0777)) + assert.NoError(t, bfs.Chown("link", 1000, 1000)) + assert.NoError(t, bfs.Lchown("link", 1000, 1000)) + + // A genuinely missing node must still report ENOENT + assert.ErrorIs(t, bfs.Chmod("missing", 0777), vfs.ENOENT) + assert.ErrorIs(t, bfs.Chown("missing", 1000, 1000), vfs.ENOENT) +} diff --git a/vfs/vfs.go b/vfs/vfs.go index e1a118982..8f576d5d5 100644 --- a/vfs/vfs.go +++ b/vfs/vfs.go @@ -745,6 +745,38 @@ func (vfs *VFS) Chtimes(name string, atime time.Time, mtime time.Time) error { return nil } +// Chmod changes the mode of the named file. +// +// If name is a symlink the mode of the link itself is changed, not +// its target (like lchmod). It does not follow the link, so it works +// on symlinks whose target doesn't exist. +// +// The VFS doesn't store file permissions so currently this returns +// ENOSYS if the file exists and ENOENT if it doesn't. +func (vfs *VFS) Chmod(name string, mode os.FileMode) error { + _, err := vfs.Stat(name) + if err != nil { + return err + } + return ENOSYS +} + +// Chown changes the uid and gid of the named file. +// +// If name is a symlink the ownership of the link itself is changed, +// not its target (like lchown). It does not follow the link, so it +// works on symlinks whose target doesn't exist. +// +// The VFS doesn't store file ownership so currently this returns +// ENOSYS if the file exists and ENOENT if it doesn't. +func (vfs *VFS) Chown(name string, uid, gid int) error { + _, err := vfs.Stat(name) + if err != nil { + return err + } + return ENOSYS +} + // mkdir creates a new directory with the specified name and permission bits // (before umask) returning the new directory node. func (vfs *VFS) mkdir(name string, perm os.FileMode) (*Dir, error) {