Skip to content

local: stop --copy-links following symlink loops - fixes #4402 - #9947

Merged
ncw merged 1 commit into
rclone:masterfrom
hsdfat:fix-local-symlink-loop-4402
Sep 21, 2026
Merged

ncw merged 1 commit into
rclone:masterfrom
hsdfat:fix-local-symlink-loop-4402

Conversation

@hsdfat

@hsdfat hsdfat commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

What does this change do?

With -L/--copy-links, a symlink pointing to one of its parent directories was followed until the OS gave up with "too many levels of symbolic links" about 40 levels deep. Every looping symlink multiplies the listing, so the tree from the issue (two directories, two files, three symlinks) listed about 6 million entries in a minute and was still going.

This implements the algorithm suggested in the issue: when a followed symlink points to a directory, compare it with the directory being listed and each of its parents using os.SameFile (device and inode). A match is a loop, and it is reported and skipped through the existing circular-symlink path (a no-retry error, counted, entry skipped). A symlink to a sibling directory is still followed as before.

  • The parents are only read when a followed symlink points to a directory, at most once per List call, so ordinary listings do no extra work.
  • Parents above the root of the remote count too - a link to a directory above the root always leads back into it.
  • Loops which the directory filters exclude are skipped quietly, so people who already --exclude a looping link see no new error.

One behaviour change to be aware of: a non-recursive listing (rclone lsf -L dir, --max-depth 1) of a directory containing a symlink to its parent used to show that entry and exit 0; it now reports the loop and exits with an error, as find -L does. Recursive commands already ended with ELOOP errors on these trees.

Testing

  • TestCopyLinksLoop builds the tree from the issue and lists it with walk.ListR for three cases: from the root (sibling links followed once, 4 loops reported), with the remote rooted below a looping parent, and with the loop excluded by a filter (skipped without an error). It stops after 100 entries so it fails fast rather than hanging on master, where all three cases fail. A fourth case calls List on the directory directly and checks the non-recursive behaviour described above: the link to the parent is skipped with one error and the sibling link is still returned.
  • End to end with the tree from the issue: master listed 5,962,454 entries (1.84 GB) and was still running after 60s; with this change rclone lsf -R --copy-links finishes in 0.06s with 8 entries and 4 loop errors, the same loops find -L reports.
  • go build ./..., make quicktest, go test -race ./backend/local/, golangci-lint run ./backend/local/ (also with GOOS=windows), and go vet for windows/darwin pass.
  • go run ./fstest/test_all -backends local: PASS: All tests finished OK.

The check uses os.SameFile, which works on Windows too; the test skips where symlinks can't be created, and I have not run it on Windows.

Linked issue

Fixes #4402

Checklist

  • This change is trivial OR it has been discussed and agreed in the linked issue.
  • I have read the contribution guidelines.
  • (If I used AI tools to help write this code) I have read and understood the AI-assisted contributions guidance, and I have tested and take ownership of this change myself.
  • I have added tests for all changes in this PR if appropriate.
  • I have added documentation for the changes if appropriate. (A sentence in the symlinks section of local.md.)
  • All commit messages are in house style.
  • (Backend changes only) test_all passes for this backend and if submitting a new backend can provide a test account for the integration tester - see CONTRIBUTING.md.
  • This Pull Request is ready for review.

With -L/--copy-links a symlink pointing to one of its parent
directories was followed until the OS gave up with "too many levels
of symbolic links" 40 levels deep. As every looping symlink multiplies
the listing, a small tree with three such symlinks listed millions of
entries.

When a followed symlink points to a directory, compare it with the
directory being listed and each of its parents using os.SameFile and
if it matches report an error and skip it, the same way as a circular
symlink. A symlink to a sibling directory is still followed. Loops
excluded by the directory filters are skipped quietly as before.

@ncw ncw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for fixing this @hsdfat

Thank you also for flagging the behaviour change for non-recursive listings. It also means rclone mount -L will no longer show a link -> .. entry that could previously be browsed lazily. I think that is the right trade-off, since find -L behaves the same way.

I'll merge now,

@ncw
ncw merged commit b6beea4 into rclone:master Sep 21, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rclone sync --copy-links: infinite symlink loop

2 participants