GH-109187: Improve symlink loop handling in pathlib.Path.resolve() (GH-109192)

Treat symlink loops like other errors: in strict mode, raise `OSError`, and
in non-strict mode, do not raise any exception.
This commit is contained in:
Barney Gale 2023-09-26 17:57:17 +01:00 committed by GitHub
parent 859618c8cd
commit ecd813f054
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 21 additions and 30 deletions

View file

@ -1381,15 +1381,19 @@ call fails (for example because the path doesn't exist).
>>> p.resolve() >>> p.resolve()
PosixPath('/home/antoine/pathlib/setup.py') PosixPath('/home/antoine/pathlib/setup.py')
If the path doesn't exist and *strict* is ``True``, :exc:`FileNotFoundError` If a path doesn't exist or a symlink loop is encountered, and *strict* is
is raised. If *strict* is ``False``, the path is resolved as far as possible ``True``, :exc:`OSError` is raised. If *strict* is ``False``, the path is
and any remainder is appended without checking whether it exists. If an resolved as far as possible and any remainder is appended without checking
infinite loop is encountered along the resolution path, :exc:`RuntimeError` whether it exists.
is raised.
.. versionchanged:: 3.6 .. versionchanged:: 3.6
The *strict* parameter was added (pre-3.6 behavior is strict). The *strict* parameter was added (pre-3.6 behavior is strict).
.. versionchanged:: 3.13
Symlink loops are treated like other errors: :exc:`OSError` is raised in
strict mode, and no exception is raised in non-strict mode. In previous
versions, :exc:`RuntimeError` is raised no matter the value of *strict*.
.. method:: Path.rglob(pattern, *, case_sensitive=None, follow_symlinks=None) .. method:: Path.rglob(pattern, *, case_sensitive=None, follow_symlinks=None)
Glob the given relative *pattern* recursively. This is like calling Glob the given relative *pattern* recursively. This is like calling

View file

@ -1230,26 +1230,7 @@ class Path(PurePath):
normalizing it. normalizing it.
""" """
def check_eloop(e): return self.with_segments(os.path.realpath(self, strict=strict))
winerror = getattr(e, 'winerror', 0)
if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME:
raise RuntimeError("Symlink loop from %r" % e.filename)
try:
s = os.path.realpath(self, strict=strict)
except OSError as e:
check_eloop(e)
raise
p = self.with_segments(s)
# In non-strict mode, realpath() doesn't raise on symlink loops.
# Ensure we get an exception by calling stat()
if not strict:
try:
p.stat()
except OSError as e:
check_eloop(e)
return p
def owner(self): def owner(self):
""" """

View file

@ -3178,10 +3178,11 @@ class PosixPathTest(PathTest):
self.assertEqual(str(P('//a').absolute()), '//a') self.assertEqual(str(P('//a').absolute()), '//a')
self.assertEqual(str(P('//a/b').absolute()), '//a/b') self.assertEqual(str(P('//a/b').absolute()), '//a/b')
def _check_symlink_loop(self, *args, strict=True): def _check_symlink_loop(self, *args):
path = self.cls(*args) path = self.cls(*args)
with self.assertRaises(RuntimeError): with self.assertRaises(OSError) as cm:
print(path.resolve(strict)) path.resolve(strict=True)
self.assertEqual(cm.exception.errno, errno.ELOOP)
@unittest.skipIf( @unittest.skipIf(
is_emscripten or is_wasi, is_emscripten or is_wasi,
@ -3240,7 +3241,8 @@ class PosixPathTest(PathTest):
os.symlink('linkZ/../linkZ', join('linkZ')) os.symlink('linkZ/../linkZ', join('linkZ'))
self._check_symlink_loop(BASE, 'linkZ') self._check_symlink_loop(BASE, 'linkZ')
# Non-strict # Non-strict
self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False) p = self.cls(BASE, 'linkZ', 'foo')
self.assertEqual(p.resolve(strict=False), p)
# Loops with absolute symlinks. # Loops with absolute symlinks.
os.symlink(join('linkU/inside'), join('linkU')) os.symlink(join('linkU/inside'), join('linkU'))
self._check_symlink_loop(BASE, 'linkU') self._check_symlink_loop(BASE, 'linkU')
@ -3249,7 +3251,8 @@ class PosixPathTest(PathTest):
os.symlink(join('linkW/../linkW'), join('linkW')) os.symlink(join('linkW/../linkW'), join('linkW'))
self._check_symlink_loop(BASE, 'linkW') self._check_symlink_loop(BASE, 'linkW')
# Non-strict # Non-strict
self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False) q = self.cls(BASE, 'linkW', 'foo')
self.assertEqual(q.resolve(strict=False), q)
def test_glob(self): def test_glob(self):
P = self.cls P = self.cls

View file

@ -0,0 +1,3 @@
:meth:`pathlib.Path.resolve` now treats symlink loops like other errors: in
strict mode, :exc:`OSError` is raised, and in non-strict mode, no exception
is raised.