Skip to content

Commit a23ffa3

Browse files
committed
gh-142155: Fix infinite recursion in shutil.copytree on Windows junctions
1 parent 52f9b5f commit a23ffa3

File tree

2 files changed

+51
-5
lines changed

2 files changed

+51
-5
lines changed

Lib/shutil.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,12 +543,22 @@ def _ignore_patterns(path, names):
543543
return _ignore_patterns
544544

545545
def _copytree(entries, src, dst, symlinks, ignore, copy_function,
546-
ignore_dangling_symlinks, dirs_exist_ok=False):
546+
ignore_dangling_symlinks, dirs_exist_ok=False, _seen=None):
547547
if ignore is not None:
548548
ignored_names = ignore(os.fspath(src), [x.name for x in entries])
549549
else:
550550
ignored_names = ()
551551

552+
# Track visited directories to detect cycles (e.g., Windows junctions)
553+
if _seen is None:
554+
_seen = set()
555+
src_st = os.stat(src)
556+
src_id = (src_st.st_dev, src_st.st_ino)
557+
if src_id in _seen:
558+
raise Error([(src, dst, "Infinite recursion detected")])
559+
_seen = _seen.copy()
560+
_seen.add(src_id)
561+
552562
os.makedirs(dst, exist_ok=dirs_exist_ok)
553563
errors = []
554564
use_srcentry = copy_function is copy2 or copy_function is copy
@@ -583,12 +593,12 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
583593
if srcentry.is_dir():
584594
copytree(srcobj, dstname, symlinks, ignore,
585595
copy_function, ignore_dangling_symlinks,
586-
dirs_exist_ok)
596+
dirs_exist_ok, _seen=_seen)
587597
else:
588598
copy_function(srcobj, dstname)
589599
elif srcentry.is_dir():
590600
copytree(srcobj, dstname, symlinks, ignore, copy_function,
591-
ignore_dangling_symlinks, dirs_exist_ok)
601+
ignore_dangling_symlinks, dirs_exist_ok, _seen=_seen)
592602
else:
593603
# Will raise a SpecialFileError for unsupported file types
594604
copy_function(srcobj, dstname)
@@ -609,7 +619,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
609619
return dst
610620

611621
def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
612-
ignore_dangling_symlinks=False, dirs_exist_ok=False):
622+
ignore_dangling_symlinks=False, dirs_exist_ok=False, _seen=None):
613623
"""Recursively copy a directory tree and return the destination directory.
614624
615625
If exception(s) occur, an Error is raised with a list of reasons.
@@ -654,7 +664,7 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
654664
return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks,
655665
ignore=ignore, copy_function=copy_function,
656666
ignore_dangling_symlinks=ignore_dangling_symlinks,
657-
dirs_exist_ok=dirs_exist_ok)
667+
dirs_exist_ok=dirs_exist_ok, _seen=_seen)
658668

659669
if hasattr(os.stat_result, 'st_file_attributes'):
660670
def _rmtree_islink(st):

Lib/test/test_shutil.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,42 @@ def test_copytree_subdirectory(self):
10991099
rv = shutil.copytree(src_dir, dst_dir)
11001100
self.assertEqual(['pol'], os.listdir(rv))
11011101

1102+
@unittest.skipUnless(sys.platform == "win32", "Windows-specific test")
1103+
def test_copytree_recursive_junction(self):
1104+
# Test that copytree raises Error for recursive junctions (Windows)
1105+
import subprocess
1106+
base_dir = self.mkdtemp()
1107+
self.addCleanup(shutil.rmtree, base_dir, ignore_errors=True)
1108+
1109+
# Create source directory structure
1110+
src_dir = os.path.join(base_dir, "Source")
1111+
junction_dir = os.path.join(src_dir, "Junction")
1112+
os.makedirs(junction_dir)
1113+
1114+
# Create a junction pointing to its parent, creating a cycle
1115+
junction_target = os.path.dirname(junction_dir) # Points to Source
1116+
try:
1117+
result = subprocess.run(
1118+
["mklink", "/J", junction_dir, junction_target],
1119+
shell=True, check=False, capture_output=True, text=True
1120+
)
1121+
if result.returncode != 0:
1122+
# Skip if we don't have permission to create junctions
1123+
self.skipTest(f"Failed to create junction: {result.stderr.strip()}")
1124+
except Exception as e:
1125+
# Skip if mklink is not available or fails for any reason
1126+
self.skipTest(f"Failed to create junction: {e}")
1127+
1128+
# Create destination directory
1129+
dst_dir = os.path.join(base_dir, "Dest")
1130+
1131+
# Test that copytree raises Error with infinite recursion message
1132+
with self.assertRaises(shutil.Error) as cm:
1133+
shutil.copytree(src_dir, dst_dir)
1134+
1135+
# Verify the error message contains "Infinite recursion detected"
1136+
self.assertIn("Infinite recursion detected", str(cm.exception))
1137+
11021138
class TestCopy(BaseTest, unittest.TestCase):
11031139

11041140
### shutil.copymode

0 commit comments

Comments
 (0)