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
42 lines
1.2 KiB
Go
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)
|
|
}
|