Files
rclone/cmd/serve/nfs/filesystem_test.go
T
SillyZirandNick Craig-Wood 7804c1b315 serve nfs: fix EIO when creating symlinks with --vfs-links
The macOS NFS client sends SETATTR after SYMLINK, which arrives as
Chmod/Chown on the link path. These opened the target with vfs.Open,
which follows symlinks - a freshly created symlink usually dangles, so
the open failed with ENOENT, surfaced to the client as NFS3ERR_IO even
though the link was created.

Add path-based VFS.Chmod and VFS.Chown mirroring VFS.Chtimes. They do
not follow symlinks (lstat semantics, matching VFS.Stat) and return
ENOSYS when the node exists, since the VFS stores neither permissions
nor ownership; serve nfs calls them and masks ENOSYS as before.

Fixes #9627
2026-08-01 12:17:20 +01:00

42 lines
1.2 KiB
Go

//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)
}