drive: detect shortcut loops to avoid infinite recursion

A folder shortcut pointing at one of its own ancestor folders (for
example a shortcut to FolderX placed inside FolderX) made rclone recurse
forever when dereferencing shortcuts, duplicating the folder contents
until the disk was full.

Rclone now detects when a folder shortcut targets an ancestor directory
using the directory cache, leaves that shortcut out of the listing and
logs an ERROR, so the rest of the drive can still be copied.

Fixes #7118
Fixes #9565
Closes #9051
This commit is contained in:
Nick Craig-Wood
2026-07-10 18:45:41 +01:00
parent 887c2b6b58
commit 0baac15c49
3 changed files with 61 additions and 3 deletions
+28
View File
@@ -2439,12 +2439,40 @@ func (f *Fs) resolveShortcut(ctx context.Context, item *drive.File) (newItem *dr
return newItem, nil return newItem, nil
} }
// shortcutLoop reports whether a folder shortcut at remote pointing to
// the directory targetID would create a recursive loop.
//
// This is the case when targetID is the ID of remote's parent or any
// other ancestor directory, as descending into it would list an
// ancestor again forever. The ancestor IDs are looked up in the
// directory cache which is always populated for the parents of remote
// as they are listed before their children.
func (f *Fs) shortcutLoop(remote, targetID string) bool {
for dir := path.Dir(remote); ; dir = path.Dir(dir) {
if dir == "." {
dir = ""
}
if id, ok := f.dirCache.Get(dir); ok && actualID(id) == targetID {
return true
}
if dir == "" {
return false
}
}
}
// itemToDirEntry converts a drive.File to an fs.DirEntry. // itemToDirEntry converts a drive.File to an fs.DirEntry.
// When the drive.File cannot be represented as an fs.DirEntry // When the drive.File cannot be represented as an fs.DirEntry
// (nil, nil) is returned. // (nil, nil) is returned.
func (f *Fs) itemToDirEntry(ctx context.Context, remote string, item *drive.File) (entry fs.DirEntry, err error) { func (f *Fs) itemToDirEntry(ctx context.Context, remote string, item *drive.File) (entry fs.DirEntry, err error) {
switch { switch {
case item.MimeType == driveFolderType: case item.MimeType == driveFolderType:
// A folder shortcut pointing at one of its own ancestors
// would make rclone recurse forever, so drop it.
if isShortcutID(item.Id) && f.shortcutLoop(remote, actualID(item.Id)) {
fs.Errorf(remote, "Ignoring folder shortcut as it points to an ancestor directory creating a loop")
return nil, nil
}
// cache the directory ID for later lookups // cache the directory ID for later lookups
f.dirCache.Put(remote, item.Id) f.dirCache.Put(remote, item.Id)
// cache the resource key for later lookups // cache the resource key for later lookups
+29
View File
@@ -24,6 +24,7 @@ import (
"github.com/rclone/rclone/fs/sync" "github.com/rclone/rclone/fs/sync"
"github.com/rclone/rclone/fstest" "github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/fstest/fstests" "github.com/rclone/rclone/fstest/fstests"
"github.com/rclone/rclone/lib/dircache"
"github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/random"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -145,6 +146,34 @@ func TestInternalFindExportFormat(t *testing.T) {
} }
} }
func TestShortcutLoop(t *testing.T) {
f := &Fs{}
f.dirCache = dircache.New("", "root", f)
// root is FolderX with ID "X"
f.dirCache.Put("", "X")
// a real subfolder
f.dirCache.Put("sub", "sub-id")
// a nested subfolder reached via a shortcut, composite ID
f.dirCache.Put("sub/nested", joinID("nested-id", "shortcut-id"))
for _, test := range []struct {
name string
remote string
targetID string
want bool
}{
{"shortcut to the root itself", "loop", "X", true},
{"shortcut to the parent", "sub/loop", "sub-id", true},
{"shortcut to a grandparent", "sub/nested/loop", "X", true},
{"shortcut to a composite ancestor", "sub/nested/loop", "nested-id", true},
{"shortcut to an unrelated folder", "sub/loop", "other-id", false},
{"shortcut to a sibling", "sub/loop", "sub-id2", false},
} {
got := f.shortcutLoop(test.remote, test.targetID)
assert.Equal(t, test.want, got, test.name)
}
}
func TestMimeTypesToExtension(t *testing.T) { func TestMimeTypesToExtension(t *testing.T) {
for mimeType, extension := range _mimeTypeToExtension { for mimeType, extension := range _mimeTypeToExtension {
extensions, err := mime.ExtensionsByType(mimeType) extensions, err := mime.ExtensionsByType(mimeType)
+4 -3
View File
@@ -499,9 +499,10 @@ used to create shortcuts.
Shortcuts can be completely ignored with the `--drive-skip-shortcuts` flag Shortcuts can be completely ignored with the `--drive-skip-shortcuts` flag
or the corresponding `skip_shortcuts` configuration setting. or the corresponding `skip_shortcuts` configuration setting.
If you have shortcuts that lead to an infinite recursion in your drive (e.g. a If you have a folder shortcut that points at one of its own parent folders it
shortcut pointing to a parent folder), `skip_shortcuts` might be mandatory to would lead to an infinite recursion. Rclone detects this, leaves the offending
be able to copy the drive. shortcut out of the listing and logs an ERROR, so the rest of the drive can
still be copied.
### Emptying trash ### Emptying trash