From 7bb3bcaac3a80a2b36874e55c4fef4e701a03a9e Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Thu, 11 Jun 2026 11:47:02 +0100 Subject: [PATCH] local: don't resolve relative roots to absolute paths - fixes #9510 cleanRootPath used filepath.Abs which prepends the current directory, but the resulting absolute path does not always refer to the same directory as the original relative path - for example when the current directory is shadowed by a mount or has been removed. This made "rclone copy --links . ../dst" fail where "cp -ra . ../dst" succeeds. rclone now cleans the path lexically on non-Windows platforms instead, leaving relative roots relative so the OS resolves them against the live working directory. Windows still makes the path absolute as required for UNC long-path conversion. --- backend/local/local.go | 15 ++++++++++++--- backend/local/tests_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/backend/local/local.go b/backend/local/local.go index 2133279d0..c4f526f8e 100644 --- a/backend/local/local.go +++ b/backend/local/local.go @@ -1726,9 +1726,18 @@ func cleanRootPath(s string, noUNC bool, enc encoder.MultiEncoder) string { if runtime.GOOS == "windows" { s = vol + s } - s2, err := filepath.Abs(s) - if err == nil { - s = s2 + // UNC paths on Windows must be absolute, so make the path absolute + // there. On other platforms filepath.Abs would prepend the current + // directory, but the resulting absolute string is not guaranteed to + // refer to the same directory as the original relative path - for + // example when the current directory is shadowed by a mount or has been + // removed - so just clean it lexically instead. + if runtime.GOOS == "windows" { + if s2, err := filepath.Abs(s); err == nil { + s = s2 + } + } else { + s = filepath.Clean(s) } if !noUNC { // Convert to UNC. It does nothing on non windows platforms. diff --git a/backend/local/tests_test.go b/backend/local/tests_test.go index f85ea0f15..ba66676fd 100644 --- a/backend/local/tests_test.go +++ b/backend/local/tests_test.go @@ -30,3 +30,29 @@ func TestCleanWindows(t *testing.T) { } } } + +// Relative roots must stay relative so the OS resolves them against the +// live working directory rather than a canonicalised string that may no +// longer refer to the same directory (#9510). +var testsRelative = [][2]string{ + {".", "."}, + {"./", "."}, + {"sub/dir", "sub/dir"}, + {"sub/dir/", "sub/dir"}, + {"./sub/dir", "sub/dir"}, + {"sub/../dir", "dir"}, + {"..", ".."}, +} + +func TestCleanRootPathRelative(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("non-windows only") + } + for _, test := range testsRelative { + got := cleanRootPath(test[0], true, encoder.OS) + expect := test[1] + if got != expect { + t.Fatalf("got %q, expected %q", got, expect) + } + } +}