diff --git a/Lib/shutil.py b/Lib/shutil.py index 8d8fe145567822..db7a5f82b87477 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -543,12 +543,22 @@ def _ignore_patterns(path, names): return _ignore_patterns def _copytree(entries, src, dst, symlinks, ignore, copy_function, - ignore_dangling_symlinks, dirs_exist_ok=False): + ignore_dangling_symlinks, dirs_exist_ok=False, _seen=None): if ignore is not None: ignored_names = ignore(os.fspath(src), [x.name for x in entries]) else: ignored_names = () + # Track visited directories to detect cycles (e.g., Windows junctions) + if _seen is None: + _seen = frozenset() + src_st = os.stat(src) + src_id = (src_st.st_dev, src_st.st_ino) + if src_id in _seen: + raise Error([(src, dst, "Infinite recursion detected")]) + + _seen = _seen | {src_id} + os.makedirs(dst, exist_ok=dirs_exist_ok) errors = [] use_srcentry = copy_function is copy2 or copy_function is copy @@ -583,12 +593,12 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function, if srcentry.is_dir(): copytree(srcobj, dstname, symlinks, ignore, copy_function, ignore_dangling_symlinks, - dirs_exist_ok) + dirs_exist_ok, _seen=_seen) else: copy_function(srcobj, dstname) elif srcentry.is_dir(): copytree(srcobj, dstname, symlinks, ignore, copy_function, - ignore_dangling_symlinks, dirs_exist_ok) + ignore_dangling_symlinks, dirs_exist_ok, _seen=_seen) else: # Will raise a SpecialFileError for unsupported file types copy_function(srcobj, dstname) @@ -609,7 +619,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function, return dst def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, - ignore_dangling_symlinks=False, dirs_exist_ok=False): + ignore_dangling_symlinks=False, dirs_exist_ok=False, _seen=None): """Recursively copy a directory tree and return the destination directory. 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, return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks, ignore=ignore, copy_function=copy_function, ignore_dangling_symlinks=ignore_dangling_symlinks, - dirs_exist_ok=dirs_exist_ok) + dirs_exist_ok=dirs_exist_ok, _seen=_seen) if hasattr(os.stat_result, 'st_file_attributes'): def _rmtree_islink(st): diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index ebb6cf88336249..2af394b79769f2 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1099,6 +1099,42 @@ def test_copytree_subdirectory(self): rv = shutil.copytree(src_dir, dst_dir) self.assertEqual(['pol'], os.listdir(rv)) + @unittest.skipUnless(sys.platform == "win32", "Windows-specific test") + def test_copytree_recursive_junction(self): + # Test that copytree raises Error for recursive junctions (Windows) + base_dir = self.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir, ignore_errors=True) + + # Create source directory structure + src_dir = os.path.join(base_dir, "source") + junction_dir = os.path.join(src_dir, "junction") + os.makedirs(junction_dir) + + # Create a junction pointing to its parent, creating a cycle + junction_target = os.path.dirname(junction_dir) # Points to Source + try: + result = subprocess.run( + ["mklink", "/J", junction_dir, junction_target], + shell=True, check=False, capture_output=True, text=True + ) + if result.returncode != 0: + # Skip if we don't have permission to create junctions + self.skipTest(f"Failed to create junction: {result.stderr.strip()}") + except Exception as e: + # Skip if mklink is not available or fails for any reason + self.skipTest(f"Failed to create junction: {e}") + + # Create destination directory + dst_dir = os.path.join(base_dir, "Dest") + + # Test that copytree raises Error with infinite recursion message + with self.assertRaises(shutil.Error) as cm: + shutil.copytree(src_dir, dst_dir) + + # Verify the error message contains "Infinite recursion detected" + self.assertIn("Infinite recursion detected", str(cm.exception)) + + class TestCopy(BaseTest, unittest.TestCase): ### shutil.copymode diff --git a/Misc/NEWS.d/next/Library/2025-12-01-17-17-12.gh-issue-142155.wiUAfL.rst b/Misc/NEWS.d/next/Library/2025-12-01-17-17-12.gh-issue-142155.wiUAfL.rst new file mode 100644 index 00000000000000..ae9abf241ee7d5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-01-17-17-12.gh-issue-142155.wiUAfL.rst @@ -0,0 +1 @@ +Fix infinite recursion crash in :func:`shutil.copytree` when a cycle involving a directory junction is present on Windows.