Skip to content

Commit d7ae4dc

Browse files
authored
GH-73991: Disallow copying directory into itself via pathlib.Path.copy() (#122924)
1 parent bf1b5d3 commit d7ae4dc

File tree

2 files changed

+140
-14
lines changed

2 files changed

+140
-14
lines changed

Lib/pathlib/_abc.py

+37-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import functools
1515
import operator
1616
import posixpath
17+
from errno import EINVAL
1718
from glob import _GlobberBase, _no_recurse_symlinks
1819
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
1920
from pathlib._os import copyfileobj
@@ -564,14 +565,38 @@ def samefile(self, other_path):
564565
return (st.st_ino == other_st.st_ino and
565566
st.st_dev == other_st.st_dev)
566567

567-
def _samefile_safe(self, other_path):
568+
def _ensure_different_file(self, other_path):
568569
"""
569-
Like samefile(), but returns False rather than raising OSError.
570+
Raise OSError(EINVAL) if both paths refer to the same file.
570571
"""
571572
try:
572-
return self.samefile(other_path)
573+
if not self.samefile(other_path):
574+
return
573575
except (OSError, ValueError):
574-
return False
576+
return
577+
err = OSError(EINVAL, "Source and target are the same file")
578+
err.filename = str(self)
579+
err.filename2 = str(other_path)
580+
raise err
581+
582+
def _ensure_distinct_path(self, other_path):
583+
"""
584+
Raise OSError(EINVAL) if the other path is within this path.
585+
"""
586+
# Note: there is no straightforward, foolproof algorithm to determine
587+
# if one directory is within another (a particularly perverse example
588+
# would be a single network share mounted in one location via NFS, and
589+
# in another location via CIFS), so we simply checks whether the
590+
# other path is lexically equal to, or within, this path.
591+
if self == other_path:
592+
err = OSError(EINVAL, "Source and target are the same path")
593+
elif self in other_path.parents:
594+
err = OSError(EINVAL, "Source path is a parent of target path")
595+
else:
596+
return
597+
err.filename = str(self)
598+
err.filename2 = str(other_path)
599+
raise err
575600

576601
def open(self, mode='r', buffering=-1, encoding=None,
577602
errors=None, newline=None):
@@ -826,8 +851,7 @@ def _copy_file(self, target):
826851
"""
827852
Copy the contents of this file to the given target.
828853
"""
829-
if self._samefile_safe(target):
830-
raise OSError(f"{self!r} and {target!r} are the same file")
854+
self._ensure_different_file(target)
831855
with self.open('rb') as source_f:
832856
try:
833857
with target.open('wb') as target_f:
@@ -847,6 +871,13 @@ def copy(self, target, *, follow_symlinks=True, dirs_exist_ok=False,
847871
"""
848872
if not isinstance(target, PathBase):
849873
target = self.with_segments(target)
874+
try:
875+
self._ensure_distinct_path(target)
876+
except OSError as err:
877+
if on_error is None:
878+
raise
879+
on_error(err)
880+
return
850881
stack = [(self, target)]
851882
while stack:
852883
src, dst = stack.pop()

Lib/test/test_pathlib/test_pathlib_abc.py

+103-8
Original file line numberDiff line numberDiff line change
@@ -1501,19 +1501,20 @@ def iterdir(self):
15011501
raise FileNotFoundError(errno.ENOENT, "File not found", path)
15021502

15031503
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
1504-
path = str(self.resolve())
1505-
if path in self._directories:
1504+
path = str(self.parent.resolve() / self.name)
1505+
parent = str(self.parent.resolve())
1506+
if path in self._directories or path in self._symlinks:
15061507
if exist_ok:
15071508
return
15081509
else:
15091510
raise FileExistsError(errno.EEXIST, "File exists", path)
15101511
try:
15111512
if self.name:
1512-
self._directories[str(self.parent)].add(self.name)
1513+
self._directories[parent].add(self.name)
15131514
self._directories[path] = set()
15141515
except KeyError:
15151516
if not parents:
1516-
raise FileNotFoundError(errno.ENOENT, "File not found", str(self.parent)) from None
1517+
raise FileNotFoundError(errno.ENOENT, "File not found", parent) from None
15171518
self.parent.mkdir(parents=True, exist_ok=True)
15181519
self.mkdir(mode, parents=False, exist_ok=exist_ok)
15191520

@@ -1758,6 +1759,32 @@ def test_copy_symlink_follow_symlinks_false(self):
17581759
self.assertTrue(target.is_symlink())
17591760
self.assertEqual(source.readlink(), target.readlink())
17601761

1762+
@needs_symlinks
1763+
def test_copy_symlink_to_itself(self):
1764+
base = self.cls(self.base)
1765+
source = base / 'linkA'
1766+
self.assertRaises(OSError, source.copy, source)
1767+
1768+
@needs_symlinks
1769+
def test_copy_symlink_to_existing_symlink(self):
1770+
base = self.cls(self.base)
1771+
source = base / 'copySource'
1772+
target = base / 'copyTarget'
1773+
source.symlink_to(base / 'fileA')
1774+
target.symlink_to(base / 'dirC')
1775+
self.assertRaises(OSError, source.copy, target)
1776+
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
1777+
1778+
@needs_symlinks
1779+
def test_copy_symlink_to_existing_directory_symlink(self):
1780+
base = self.cls(self.base)
1781+
source = base / 'copySource'
1782+
target = base / 'copyTarget'
1783+
source.symlink_to(base / 'fileA')
1784+
target.symlink_to(base / 'dirC')
1785+
self.assertRaises(OSError, source.copy, target)
1786+
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
1787+
17611788
@needs_symlinks
17621789
def test_copy_directory_symlink_follow_symlinks_false(self):
17631790
base = self.cls(self.base)
@@ -1769,6 +1796,42 @@ def test_copy_directory_symlink_follow_symlinks_false(self):
17691796
self.assertTrue(target.is_symlink())
17701797
self.assertEqual(source.readlink(), target.readlink())
17711798

1799+
@needs_symlinks
1800+
def test_copy_directory_symlink_to_itself(self):
1801+
base = self.cls(self.base)
1802+
source = base / 'linkB'
1803+
self.assertRaises(OSError, source.copy, source)
1804+
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
1805+
1806+
@needs_symlinks
1807+
def test_copy_directory_symlink_into_itself(self):
1808+
base = self.cls(self.base)
1809+
source = base / 'linkB'
1810+
target = base / 'linkB' / 'copyB'
1811+
self.assertRaises(OSError, source.copy, target)
1812+
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
1813+
self.assertFalse(target.exists())
1814+
1815+
@needs_symlinks
1816+
def test_copy_directory_symlink_to_existing_symlink(self):
1817+
base = self.cls(self.base)
1818+
source = base / 'copySource'
1819+
target = base / 'copyTarget'
1820+
source.symlink_to(base / 'dirC')
1821+
target.symlink_to(base / 'fileA')
1822+
self.assertRaises(FileExistsError, source.copy, target)
1823+
self.assertRaises(FileExistsError, source.copy, target, follow_symlinks=False)
1824+
1825+
@needs_symlinks
1826+
def test_copy_directory_symlink_to_existing_directory_symlink(self):
1827+
base = self.cls(self.base)
1828+
source = base / 'copySource'
1829+
target = base / 'copyTarget'
1830+
source.symlink_to(base / 'dirC' / 'dirD')
1831+
target.symlink_to(base / 'dirC')
1832+
self.assertRaises(FileExistsError, source.copy, target)
1833+
self.assertRaises(FileExistsError, source.copy, target, follow_symlinks=False)
1834+
17721835
def test_copy_file_to_existing_file(self):
17731836
base = self.cls(self.base)
17741837
source = base / 'fileA'
@@ -1782,8 +1845,7 @@ def test_copy_file_to_existing_directory(self):
17821845
base = self.cls(self.base)
17831846
source = base / 'fileA'
17841847
target = base / 'dirA'
1785-
with self.assertRaises(OSError):
1786-
source.copy(target)
1848+
self.assertRaises(OSError, source.copy, target)
17871849

17881850
@needs_symlinks
17891851
def test_copy_file_to_existing_symlink(self):
@@ -1823,6 +1885,13 @@ def test_copy_file_empty(self):
18231885
self.assertTrue(target.exists())
18241886
self.assertEqual(target.read_bytes(), b'')
18251887

1888+
def test_copy_file_to_itself(self):
1889+
base = self.cls(self.base)
1890+
source = base / 'empty'
1891+
source.write_bytes(b'')
1892+
self.assertRaises(OSError, source.copy, source)
1893+
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
1894+
18261895
def test_copy_dir_simple(self):
18271896
base = self.cls(self.base)
18281897
source = base / 'dirC'
@@ -1909,6 +1978,28 @@ def test_copy_dir_to_existing_directory_dirs_exist_ok(self):
19091978
self.assertTrue(target.joinpath('fileC').read_text(),
19101979
"this is file C\n")
19111980

1981+
def test_copy_dir_to_itself(self):
1982+
base = self.cls(self.base)
1983+
source = base / 'dirC'
1984+
self.assertRaises(OSError, source.copy, source)
1985+
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
1986+
1987+
def test_copy_dir_to_itself_on_error(self):
1988+
base = self.cls(self.base)
1989+
source = base / 'dirC'
1990+
errors = []
1991+
source.copy(source, on_error=errors.append)
1992+
self.assertEqual(len(errors), 1)
1993+
self.assertIsInstance(errors[0], OSError)
1994+
1995+
def test_copy_dir_into_itself(self):
1996+
base = self.cls(self.base)
1997+
source = base / 'dirC'
1998+
target = base / 'dirC' / 'dirD' / 'copyC'
1999+
self.assertRaises(OSError, source.copy, target)
2000+
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
2001+
self.assertFalse(target.exists())
2002+
19122003
def test_copy_missing_on_error(self):
19132004
base = self.cls(self.base)
19142005
source = base / 'foo'
@@ -2876,8 +2967,12 @@ def readlink(self):
28762967
raise FileNotFoundError(errno.ENOENT, "File not found", path)
28772968

28782969
def symlink_to(self, target, target_is_directory=False):
2879-
self._directories[str(self.parent)].add(self.name)
2880-
self._symlinks[str(self)] = str(target)
2970+
path = str(self.parent.resolve() / self.name)
2971+
parent = str(self.parent.resolve())
2972+
if path in self._symlinks:
2973+
raise FileExistsError(errno.EEXIST, "File exists", path)
2974+
self._directories[parent].add(self.name)
2975+
self._symlinks[path] = str(target)
28812976

28822977

28832978
class DummyPathWithSymlinksTest(DummyPathTest):

0 commit comments

Comments
 (0)