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
This commit is contained in:
committed by
Nick Craig-Wood
parent
060b997595
commit
7804c1b315
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+32
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user