Files
rclone/vfs/vfs_test.go
Nick Craig-Wood 3739ad9c35 vfs: fix serve nfs path traversal by refusing unsafe file names GHSA-44pm-2q5p-fm63 CVE-PENDING
Dir.Create, Dir.Mkdir and Dir.Rename joined whatever name they were
given onto the directory's path. A name of "..", "." or one containing
a "/" isn't a child of the directory, and at the VFS root
path.Join("", "..") preserves the ".." so the new node's path pointed
outside the root. Backends which join the Fs root with the remote then
resolved it to a location above the directory the VFS was created on.

serve nfs was exploitable through this. The go-nfs library refuses a
name of exactly "." or ".." in MKDIR only, doesn't check the name in
CREATE, RENAME or SYMLINK, and never refuses a name containing a "/".

serve webdav was exploitable in the same way and has its own fix. The
other serve front-ends rely on their own or their protocol library's
path cleaning to keep such names out of the VFS.

Reject such names with EINVAL in the VFS itself, so that every
front-end and mount is protected regardless of what the layer above it
checks. This is the fix for serve nfs and defence in depth for the
others. The serve webdav check is kept as the front-end is the trust
boundary and gives the client a clear error. The source name of a
rename is only used to look the node up so it isn't checked.

VFS.Mkdir of the root, which previously went through Dir.Mkdir with an
empty name and added a nameless child node to the root, now makes the
root directory on the backend directly, still honouring --read-only.
serve webdav and serve ftp made directories through Dir.Mkdir rather
than VFS.Mkdir, so they are switched to VFS.Mkdir to keep "MKCOL /" and
"MKD /" working.
2026-10-08 22:49:33 +01:00

583 lines
15 KiB
Go

// Test suite for vfs
package vfs
import (
"context"
"errors"
"fmt"
"io"
"os"
"testing"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Some times used in the tests
var (
t1 = fstest.Time("2001-02-03T04:05:06.499999999Z")
t2 = fstest.Time("2011-12-25T12:59:59.123456789Z")
t3 = fstest.Time("2011-12-30T12:59:59.000000000Z")
)
// Constants uses in the tests
const (
writeBackDelay = fs.Duration(100 * time.Millisecond) // A short writeback delay for testing
waitForWritersDelay = 30 * time.Second // time to wait for existing writers
)
// TestMain drives the tests
func TestMain(m *testing.M) {
fstest.TestMain(m)
}
// Clean up a test VFS
func cleanupVFS(t *testing.T, vfs *VFS) {
vfs.WaitForWriters(waitForWritersDelay)
err := vfs.CleanUp()
require.NoError(t, err)
vfs.Shutdown()
}
// Create a new VFS
func newTestVFSOpt(t *testing.T, opt *vfscommon.Options) (r *fstest.Run, vfs *VFS) {
r = fstest.NewRun(t)
vfs = New(context.Background(), r.Fremote, opt)
t.Cleanup(func() {
cleanupVFS(t, vfs)
})
return r, vfs
}
// Create a new VFS with default options
func newTestVFS(t *testing.T) (r *fstest.Run, vfs *VFS) {
return newTestVFSOpt(t, nil)
}
// Check baseHandle performs as advertised
func TestVFSbaseHandle(t *testing.T) {
fh := baseHandle{}
err := fh.Chdir()
assert.Equal(t, ENOSYS, err)
err = fh.Chmod(0)
assert.Equal(t, ENOSYS, err)
err = fh.Chown(0, 0)
assert.Equal(t, ENOSYS, err)
err = fh.Close()
assert.Equal(t, ENOSYS, err)
fd := fh.Fd()
assert.Equal(t, uintptr(0), fd)
name := fh.Name()
assert.Equal(t, "", name)
_, err = fh.Read(nil)
assert.Equal(t, ENOSYS, err)
_, err = fh.ReadAt(nil, 0)
assert.Equal(t, ENOSYS, err)
_, err = fh.Readdir(0)
assert.Equal(t, ENOSYS, err)
_, err = fh.Readdirnames(0)
assert.Equal(t, ENOSYS, err)
_, err = fh.Seek(0, io.SeekStart)
assert.Equal(t, ENOSYS, err)
_, err = fh.Stat()
assert.Equal(t, ENOSYS, err)
err = fh.Sync()
assert.Equal(t, nil, err)
err = fh.Truncate(0)
assert.Equal(t, ENOSYS, err)
_, err = fh.Write(nil)
assert.Equal(t, ENOSYS, err)
_, err = fh.WriteAt(nil, 0)
assert.Equal(t, ENOSYS, err)
_, err = fh.WriteString("")
assert.Equal(t, ENOSYS, err)
err = fh.Flush()
assert.Equal(t, ENOSYS, err)
err = fh.Release()
assert.Equal(t, ENOSYS, err)
node := fh.Node()
assert.Nil(t, node)
}
// TestVFSNew sees if the New command works properly
func TestVFSNew(t *testing.T) {
// Check active cache has this many entries
checkActiveCacheEntries := func(i int) {
_, count := activeCacheEntries()
assert.Equal(t, i, count)
}
checkActiveCacheEntries(0)
r, vfs := newTestVFS(t)
// Check making a VFS with nil options
var defaultOpt = vfscommon.Opt
defaultOpt.Init(context.Background())
checkActiveCacheEntries(1)
// Check that we get the same VFS if we ask for it again with
// the same options
vfs2 := New(context.Background(), r.Fremote, nil)
assert.Equal(t, fmt.Sprintf("%p", vfs), fmt.Sprintf("%p", vfs2))
checkActiveCacheEntries(1)
// Shut the new VFS down and check the cache still has stuff in
vfs2.Shutdown()
checkActiveCacheEntries(1)
cleanupVFS(t, vfs)
checkActiveCacheEntries(0)
}
// TestVFSHold checks a held VFS isn't shut down until it is released
// and can't be held once shut down.
func TestVFSHold(t *testing.T) {
r := fstest.NewRun(t)
vfs := New(context.Background(), r.Fremote, nil)
require.True(t, vfs.Hold())
vfs.Shutdown()
assert.NoError(t, vfs.ctx.Err(), "VFS shut down while held")
vfs.Shutdown()
assert.Error(t, vfs.ctx.Err(), "VFS not shut down when released")
assert.False(t, vfs.Hold(), "VFS held after being shut down")
}
// TestVFSNewShuttingDown checks New doesn't reuse a VFS which is being
// shut down but hasn't yet removed itself from the active cache.
func TestVFSNewShuttingDown(t *testing.T) {
r := fstest.NewRun(t)
vfs := New(context.Background(), r.Fremote, nil)
// The state Shutdown leaves the VFS in while it waits for activeMu
vfs.inUse.Store(0)
vfs2 := New(context.Background(), r.Fremote, nil)
assert.NotSame(t, vfs, vfs2, "reused a VFS being shut down")
assert.Equal(t, int32(0), vfs.inUse.Load())
vfs.Shutdown()
vfs2.Shutdown()
}
// TestVFSBusy checks Busy reports files open for write and cached
// files which haven't been uploaded yet.
func TestVFSBusy(t *testing.T) {
opt := vfscommon.Opt
opt.CacheMode = vfscommon.CacheModeWrites
opt.WriteBack = 10 * writeBackDelay
_, vfs := newTestVFSOpt(t, &opt)
assert.False(t, vfs.Busy())
fh, err := vfs.Create("file.txt")
require.NoError(t, err)
assert.True(t, vfs.Busy(), "not busy with a file open for write")
_, err = fh.WriteString("hello")
require.NoError(t, err)
require.NoError(t, fh.Close())
assert.True(t, vfs.Busy(), "not busy with a file waiting to be uploaded")
vfs.WaitForWriters(waitForWritersDelay)
assert.False(t, vfs.Busy(), "busy after the file was uploaded")
}
// TestVFSNewWithOpts sees if the New command works properly
func TestVFSNewWithOpts(t *testing.T) {
var opt = vfscommon.Opt
opt.DirPerms = 0777
opt.FilePerms = 0666
opt.Umask = 0002
_, vfs := newTestVFSOpt(t, &opt)
assert.Equal(t, vfscommon.FileMode(0775)|vfscommon.FileMode(os.ModeDir), vfs.Opt.DirPerms)
assert.Equal(t, vfscommon.FileMode(0664), vfs.Opt.FilePerms)
}
// TestVFSRoot checks root directory is present and correct
func TestVFSRoot(t *testing.T) {
_, vfs := newTestVFS(t)
root, err := vfs.Root()
require.NoError(t, err)
assert.Equal(t, vfs.root, root)
assert.True(t, root.IsDir())
assert.Equal(t, os.FileMode(vfs.Opt.DirPerms).Perm(), root.Mode().Perm())
}
func TestVFSStat(t *testing.T) {
r, vfs := newTestVFS(t)
file1 := r.WriteObject(context.Background(), "file1", "file1 contents", t1)
file2 := r.WriteObject(context.Background(), "dir/file2", "file2 contents", t2)
r.CheckRemoteItems(t, file1, file2)
node, err := vfs.Stat("file1")
require.NoError(t, err)
assert.True(t, node.IsFile())
assert.Equal(t, "file1", node.Name())
node, err = vfs.Stat("dir")
require.NoError(t, err)
assert.True(t, node.IsDir())
assert.Equal(t, "dir", node.Name())
node, err = vfs.Stat("dir/file2")
require.NoError(t, err)
assert.True(t, node.IsFile())
assert.Equal(t, "file2", node.Name())
_, err = vfs.Stat("not found")
assert.Equal(t, os.ErrNotExist, err)
_, err = vfs.Stat("dir/not found")
assert.Equal(t, os.ErrNotExist, err)
_, err = vfs.Stat("not found/not found")
assert.Equal(t, os.ErrNotExist, err)
_, err = vfs.Stat("file1/under a file")
assert.Equal(t, os.ErrNotExist, err)
}
func TestVFSStatParent(t *testing.T) {
r, vfs := newTestVFS(t)
file1 := r.WriteObject(context.Background(), "file1", "file1 contents", t1)
file2 := r.WriteObject(context.Background(), "dir/file2", "file2 contents", t2)
r.CheckRemoteItems(t, file1, file2)
node, leaf, err := vfs.StatParent("file1")
require.NoError(t, err)
assert.True(t, node.IsDir())
assert.Equal(t, "/", node.Name())
assert.Equal(t, "file1", leaf)
node, leaf, err = vfs.StatParent("dir/file2")
require.NoError(t, err)
assert.True(t, node.IsDir())
assert.Equal(t, "dir", node.Name())
assert.Equal(t, "file2", leaf)
node, leaf, err = vfs.StatParent("not found")
require.NoError(t, err)
assert.True(t, node.IsDir())
assert.Equal(t, "/", node.Name())
assert.Equal(t, "not found", leaf)
_, _, err = vfs.StatParent("not found dir/not found")
assert.Equal(t, os.ErrNotExist, err)
_, _, err = vfs.StatParent("file1/under a file")
assert.Equal(t, os.ErrExist, err)
}
func TestVFSAddVirtual(t *testing.T) {
_, vfs := newTestVFS(t)
require.NoError(t, vfs.AddVirtual("file", 17, false))
node, err := vfs.Stat("file")
require.NoError(t, err)
assert.True(t, node.IsFile())
assert.Equal(t, int64(17), node.Size())
require.NoError(t, vfs.AddVirtual("dir/", 0, true))
node, err = vfs.Stat("dir")
require.NoError(t, err)
assert.True(t, node.IsDir())
require.NoError(t, vfs.AddVirtual("dir/file2", 18, false))
node, err = vfs.Stat("dir/file2")
require.NoError(t, err)
assert.True(t, node.IsFile())
assert.Equal(t, int64(18), node.Size())
}
func TestVFSOpenFile(t *testing.T) {
r, vfs := newTestVFS(t)
file1 := r.WriteObject(context.Background(), "file1", "file1 contents", t1)
file2 := r.WriteObject(context.Background(), "dir/file2", "file2 contents", t2)
r.CheckRemoteItems(t, file1, file2)
fd, err := vfs.OpenFile("file1", os.O_RDONLY, 0777)
require.NoError(t, err)
assert.NotNil(t, fd)
require.NoError(t, fd.Close())
fd, err = vfs.OpenFile("dir", os.O_RDONLY, 0777)
require.NoError(t, err)
assert.NotNil(t, fd)
require.NoError(t, fd.Close())
fd, err = vfs.OpenFile("dir/new_file.txt", os.O_RDONLY, 0777)
assert.Equal(t, os.ErrNotExist, err)
assert.Nil(t, fd)
fd, err = vfs.OpenFile("dir/new_file.txt", os.O_WRONLY|os.O_CREATE, 0777)
require.NoError(t, err)
assert.NotNil(t, fd)
err = fd.Close()
if !errors.Is(err, fs.ErrorCantUploadEmptyFiles) {
require.NoError(t, err)
}
fd, err = vfs.OpenFile("not found/new_file.txt", os.O_WRONLY|os.O_CREATE, 0777)
assert.Equal(t, os.ErrNotExist, err)
assert.Nil(t, fd)
}
func TestVFSRename(t *testing.T) {
r, vfs := newTestVFS(t)
features := r.Fremote.Features()
if features.Move == nil && features.Copy == nil {
t.Skip("skip as can't rename files")
}
file1 := r.WriteObject(context.Background(), "dir/file2", "file2 contents", t2)
r.CheckRemoteItems(t, file1)
err := vfs.Rename("dir/file2", "dir/file1")
require.NoError(t, err)
file1.Path = "dir/file1"
r.CheckRemoteItems(t, file1)
err = vfs.Rename("dir/file1", "file0")
require.NoError(t, err)
file1.Path = "file0"
r.CheckRemoteItems(t, file1)
err = vfs.Rename("not found/file0", "file0")
assert.Equal(t, os.ErrNotExist, err)
err = vfs.Rename("file0", "not found/file0")
assert.Equal(t, os.ErrNotExist, err)
}
func TestVFSStatfs(t *testing.T) {
r, vfs := newTestVFS(t)
// pre-conditions
assert.Nil(t, vfs.usage)
assert.True(t, vfs.usageTime.IsZero())
aboutSupported := r.Fremote.Features().About != nil
// read
total, used, free := vfs.Statfs()
if !aboutSupported {
assert.Equal(t, int64(unknownFreeBytes), total)
assert.Equal(t, int64(unknownFreeBytes), free)
assert.Equal(t, int64(0), used)
return // can't test anything else if About not supported
}
require.NotNil(t, vfs.usage)
assert.False(t, vfs.usageTime.IsZero())
if vfs.usage.Total != nil {
assert.Equal(t, *vfs.usage.Total, total)
} else {
assert.True(t, total >= int64(unknownFreeBytes))
}
if vfs.usage.Free != nil {
assert.Equal(t, *vfs.usage.Free, free)
} else {
if vfs.usage.Total != nil && vfs.usage.Used != nil {
assert.Equal(t, free, total-used)
} else {
assert.True(t, free >= int64(unknownFreeBytes))
}
}
if vfs.usage.Used != nil {
assert.Equal(t, *vfs.usage.Used, used)
} else {
assert.Equal(t, int64(0), used)
}
// read cached
oldUsage := vfs.usage
oldTime := vfs.usageTime
total2, used2, free2 := vfs.Statfs()
assert.Equal(t, oldUsage, vfs.usage)
assert.Equal(t, total, total2)
assert.Equal(t, used, used2)
assert.Equal(t, free, free2)
assert.Equal(t, oldTime, vfs.usageTime)
}
func TestVFSMkdir(t *testing.T) {
r, vfs := newTestVFS(t)
if !r.Fremote.Features().CanHaveEmptyDirectories {
return // can't test if can't have empty directories
}
r.CheckRemoteListing(t, nil, []string{})
// Try making the root
err := vfs.Mkdir("", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{})
// Try making a sub directory
err = vfs.Mkdir("a", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{"a"})
// Try making an existing directory
err = vfs.Mkdir("a", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{"a"})
// Try making a new directory
err = vfs.Mkdir("b/", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{"a", "b"})
// Try making a new directory
err = vfs.Mkdir("/c", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{"a", "b", "c"})
// Try making a new directory
err = vfs.Mkdir("/d/", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{"a", "b", "c", "d"})
vfs.Opt.ReadOnly = true
err = vfs.Mkdir("", 0777)
assert.Equal(t, EROFS, err)
err = vfs.Mkdir("e", 0777)
assert.Equal(t, EROFS, err)
}
func TestVFSMkdirAll(t *testing.T) {
r, vfs := newTestVFS(t)
if !r.Fremote.Features().CanHaveEmptyDirectories {
return // can't test if can't have empty directories
}
r.CheckRemoteListing(t, nil, []string{})
// Try making the root
err := vfs.MkdirAll("", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{})
// Try making a sub directory
err = vfs.MkdirAll("a/b/c/d", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{"a", "a/b", "a/b/c", "a/b/c/d"})
// Try making an existing directory
err = vfs.MkdirAll("a/b/c", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{"a", "a/b", "a/b/c", "a/b/c/d"})
// Try making an existing directory
err = vfs.MkdirAll("/a/b/c/", 0777)
require.NoError(t, err)
r.CheckRemoteListing(t, nil, []string{"a", "a/b", "a/b/c", "a/b/c/d"})
}
func TestFillInMissingSizes(t *testing.T) {
const unknownFree = 10
for _, test := range []struct {
total, free, used int64
wantTotal, wantUsed, wantFree int64
}{
{
total: 20, free: 5, used: 15,
wantTotal: 20, wantFree: 5, wantUsed: 15,
},
{
total: 20, free: 5, used: -1,
wantTotal: 20, wantFree: 5, wantUsed: 15,
},
{
total: 20, free: -1, used: 15,
wantTotal: 20, wantFree: 5, wantUsed: 15,
},
{
total: 20, free: -1, used: -1,
wantTotal: 20, wantFree: 20, wantUsed: 0,
},
{
total: -1, free: 5, used: 15,
wantTotal: 20, wantFree: 5, wantUsed: 15,
},
{
total: -1, free: 15, used: -1,
wantTotal: 15, wantFree: 15, wantUsed: 0,
},
{
total: -1, free: -1, used: 15,
wantTotal: 25, wantFree: 10, wantUsed: 15,
},
{
total: -1, free: -1, used: -1,
wantTotal: 10, wantFree: 10, wantUsed: 0,
},
} {
t.Run(fmt.Sprintf("total=%d,free=%d,used=%d", test.total, test.free, test.used), func(t *testing.T) {
gotTotal, gotUsed, gotFree := fillInMissingSizes(test.total, test.used, test.free, unknownFree)
assert.Equal(t, test.wantTotal, gotTotal, "total")
assert.Equal(t, test.wantUsed, gotUsed, "used")
assert.Equal(t, test.wantFree, gotFree, "free")
})
}
}
func TestVFSIsMetadataFile(t *testing.T) {
_, vfs := newTestVFS(t)
rawName, found := vfs.isMetadataFile("leaf.metadata")
assert.Equal(t, "leaf.metadata", rawName)
assert.Equal(t, false, found)
vfs.Opt.MetadataExtension = ".metadata"
rawName, found = vfs.isMetadataFile("leaf.metadata")
assert.Equal(t, "leaf", rawName)
assert.Equal(t, true, found)
}