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-02 11:06:24 +01:00
parent b59a17e5e8
commit 00bf6c19d8
3 changed files with 61 additions and 3 deletions

View File

@@ -2416,12 +2416,40 @@ func (f *Fs) resolveShortcut(ctx context.Context, item *drive.File) (newItem *dr
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.
// When the drive.File cannot be represented as an fs.DirEntry
// (nil, nil) is returned.
func (f *Fs) itemToDirEntry(ctx context.Context, remote string, item *drive.File) (entry fs.DirEntry, err error) {
switch {
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
f.dirCache.Put(remote, item.Id)
// cache the resource key for later lookups

View File

@@ -24,6 +24,7 @@ import (
"github.com/rclone/rclone/fs/sync"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/fstest/fstests"
"github.com/rclone/rclone/lib/dircache"
"github.com/rclone/rclone/lib/random"
"github.com/stretchr/testify/assert"
"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) {
for mimeType, extension := range _mimeTypeToExtension {
extensions, err := mime.ExtensionsByType(mimeType)

View File

@@ -483,9 +483,10 @@ used to create shortcuts.
Shortcuts can be completely ignored with the `--drive-skip-shortcuts` flag
or the corresponding `skip_shortcuts` configuration setting.
If you have shortcuts that lead to an infinite recursion in your drive (e.g. a
shortcut pointing to a parent folder), `skip_shortcuts` might be mandatory to
be able to copy the drive.
If you have a folder shortcut that points at one of its own parent folders it
would lead to an infinite recursion. Rclone detects this, leaves the offending
shortcut out of the listing and logs an ERROR, so the rest of the drive can
still be copied.
### Emptying trash