Skip to content

Commit 630df37

Browse files
authored
GH-117546: Fix symlink resolution in os.path.realpath('loop/../link') (#117568)
Continue resolving symlink targets after encountering a symlink loop, which matches coreutils `realpath` behaviour.
1 parent 6bc0b33 commit 630df37

File tree

4 files changed

+7
-17
lines changed

4 files changed

+7
-17
lines changed

Doc/library/os.path.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,8 @@ the :mod:`glob` module.)
409409
style names such as ``C:\\PROGRA~1`` to ``C:\\Program Files``.
410410

411411
If a path doesn't exist or a symlink loop is encountered, and *strict* is
412-
``True``, :exc:`OSError` is raised. If *strict* is ``False``, the path is
413-
resolved as far as possible and any remainder is appended without checking
414-
whether it exists.
412+
``True``, :exc:`OSError` is raised. If *strict* is ``False`` these errors
413+
are ignored, and so the result might be missing or otherwise inaccessible.
415414

416415
.. note::
417416
This function emulates the operating system's procedure for making a path

Lib/posixpath.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,6 @@ def realpath(filename, *, strict=False):
431431
# the same links.
432432
seen = {}
433433

434-
# Whether we're calling lstat() and readlink() to resolve symlinks. If we
435-
# encounter an OSError for a symlink loop in non-strict mode, this is
436-
# switched off.
437-
querying = True
438-
439434
while rest:
440435
name = rest.pop()
441436
if name is None:
@@ -453,9 +448,6 @@ def realpath(filename, *, strict=False):
453448
newpath = path + name
454449
else:
455450
newpath = path + sep + name
456-
if not querying:
457-
path = newpath
458-
continue
459451
try:
460452
st = os.lstat(newpath)
461453
if not stat.S_ISLNK(st.st_mode):
@@ -477,11 +469,8 @@ def realpath(filename, *, strict=False):
477469
if strict:
478470
# Raise OSError(errno.ELOOP)
479471
os.stat(newpath)
480-
else:
481-
# Return already resolved part + rest of the path unchanged.
482-
path = newpath
483-
querying = False
484-
continue
472+
path = newpath
473+
continue
485474
seen[newpath] = None # not resolved symlink
486475
target = os.readlink(newpath)
487476
if target.startswith(sep):

Lib/test/test_posixpath.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def test_realpath_symlink_loops(self):
484484
self.assertEqual(realpath(ABSTFN+"1/../x"), dirname(ABSTFN) + "/x")
485485
os.symlink(ABSTFN+"x", ABSTFN+"y")
486486
self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "y"),
487-
ABSTFN + "y")
487+
ABSTFN + "x")
488488
self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "1"),
489489
ABSTFN + "1")
490490

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix issue where :func:`os.path.realpath` stopped resolving symlinks after
2+
encountering a symlink loop on POSIX.

0 commit comments

Comments
 (0)