diff --git a/Lib/posixpath.py b/Lib/posixpath.py index fccca4e066b76f..6175ed81948abc 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -388,6 +388,8 @@ def abspath(path): # Return a canonical path (i.e. the absolute location of a file on the # filesystem). +_MAXLINKS = 40 # TODO: use limit set by OS + def realpath(filename, *, strict=False): """Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.""" @@ -402,7 +404,8 @@ def realpath(filename, *, strict=False): curdir = '.' pardir = '..' getcwd = os.getcwd - return _realpath(filename, strict, sep, curdir, pardir, getcwd) + return _realpath(filename, strict, sep, curdir, pardir, getcwd, + maxlinks=_MAXLINKS if strict else None) def _realpath(filename, strict=False, sep=sep, curdir=curdir, pardir=pardir, getcwd=os.getcwd, lstat=os.lstat, readlink=os.readlink, maxlinks=None): @@ -453,7 +456,7 @@ def _realpath(filename, strict=False, sep=sep, curdir=curdir, pardir=pardir, if link_count > maxlinks: if strict: raise OSError(errno.ELOOP, os.strerror(errno.ELOOP), - newpath) + filename) path = newpath continue elif newpath in seen: @@ -465,7 +468,7 @@ def _realpath(filename, strict=False, sep=sep, curdir=curdir, pardir=pardir, # The symlink is not resolved, so we must have a symlink loop. if strict: raise OSError(errno.ELOOP, os.strerror(errno.ELOOP), - newpath) + filename) path = newpath continue target = readlink(newpath) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 57a24e9c70d5e5..919f158d7e6465 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -3,7 +3,7 @@ import posixpath import sys import unittest -from posixpath import realpath, abspath, dirname, basename +from posixpath import _MAXLINKS, realpath, abspath, dirname, basename from test import test_genericpath from test.support import import_helper from test.support import cpython_only, os_helper @@ -689,6 +689,31 @@ def test_realpath_unreadable_symlink(self): os.chmod(ABSTFN, 0o755, follow_symlinks=False) os.unlink(ABSTFN) + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash + def test_realpath_too_many_symlinks(self): + try: + os.mkdir(ABSTFN) + os.symlink('.', f'{ABSTFN}/link') + self.assertEqual(realpath(ABSTFN + '/link' * _MAXLINKS), ABSTFN) + self.assertEqual(realpath(ABSTFN + '/link' * _MAXLINKS, + strict=True), ABSTFN) + self.assertEqual(realpath(ABSTFN + '/link' * (_MAXLINKS+1)), ABSTFN) + with self.assertRaises(OSError): + realpath(ABSTFN + '/link' * (_MAXLINKS+1), strict=True) + + # Test using relative path as well. + with os_helper.change_cwd(ABSTFN): + self.assertEqual(realpath('link/' * _MAXLINKS), ABSTFN) + self.assertEqual(realpath('link/' * _MAXLINKS, strict=True), + ABSTFN) + self.assertEqual(realpath('link/' * (_MAXLINKS+1)), ABSTFN) + with self.assertRaises(OSError): + realpath('link/' * (_MAXLINKS+1), strict=True) + finally: + os_helper.unlink(f'{ABSTFN}/link') + safe_rmdir(ABSTFN) + def test_relpath(self): (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") try: diff --git a/Misc/NEWS.d/next/Library/2024-05-19-10-12-58.gh-issue-118441.QpzMKV.rst b/Misc/NEWS.d/next/Library/2024-05-19-10-12-58.gh-issue-118441.QpzMKV.rst new file mode 100644 index 00000000000000..00025722d8c03d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-05-19-10-12-58.gh-issue-118441.QpzMKV.rst @@ -0,0 +1,2 @@ +Fix error message for :func:`os.path.realpath` on Unix. +:func:`os.path.realpath` now raises :exc:`OSError` when *strict* mode is enabled and a path with too many symlinks is supplied.