Skip to content

Commit c77f055

Browse files
gh-89760: Fix os.path.realpath() for volume GUID paths on Windows
The \\?\ prefix was stripped from the resolved path if it could not be resolved without the prefix and failed with the same error as the original path. This produced an invalid, seemingly relative path for a junction which points to a volume without a drive letter. The prefix is now only stripped for drive-letter and UNC paths.
1 parent 998b890 commit c77f055

3 files changed

Lines changed: 51 additions & 16 deletions

File tree

Lib/ntpath.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,7 @@ def realpath(path, /, *, strict=False):
666666
prefix = b'\\\\?\\'
667667
unc_prefix = b'\\\\?\\UNC\\'
668668
new_unc_prefix = b'\\\\'
669+
colon_sep = b':\\'
669670
cwd = os.getcwdb()
670671
# bpo-38081: Special case for realpath(b'nul')
671672
devnull = b'nul'
@@ -675,6 +676,7 @@ def realpath(path, /, *, strict=False):
675676
prefix = '\\\\?\\'
676677
unc_prefix = '\\\\?\\UNC\\'
677678
new_unc_prefix = '\\\\'
679+
colon_sep = ':\\'
678680
cwd = os.getcwd()
679681
# bpo-38081: Special case for realpath('nul')
680682
devnull = 'nul'
@@ -718,25 +720,29 @@ def realpath(path, /, *, strict=False):
718720
# strip off that prefix unless it was already provided on the original
719721
# path.
720722
if not had_prefix and path.startswith(prefix):
721-
# For UNC paths, the prefix will actually be \\?\UNC\
722-
# Handle that case as well.
723+
# For UNC drives, the path starts with \\?\UNC\.
723724
if path.startswith(unc_prefix):
724725
spath = new_unc_prefix + path[len(unc_prefix):]
725-
else:
726+
# For drive-letter drives, the path starts with \\?\<letter>:\.
727+
elif path.startswith(colon_sep, len(prefix) + 1):
726728
spath = path[len(prefix):]
727-
# Ensure that the non-prefixed path resolves to the same path
728-
try:
729-
if _getfinalpathname(spath) == path:
730-
path = spath
731-
except ValueError:
732-
# Unexpected, as an invalid path should not have gained a prefix
733-
# at any point, but we ignore this error just in case.
734-
pass
735-
except OSError as ex:
736-
# If the path does not exist and originally did not exist, then
737-
# strip the prefix anyway.
738-
if ex.winerror == initial_winerror:
739-
path = spath
729+
# For all others, e.g. volume GUID paths, it cannot be stripped.
730+
else:
731+
spath = None
732+
if spath is not None:
733+
# Ensure that the non-prefixed path resolves to the same path
734+
try:
735+
if _getfinalpathname(spath) == path:
736+
path = spath
737+
except ValueError:
738+
# Unexpected, as an invalid path should not have gained a
739+
# prefix at any point, but we ignore this error just in case.
740+
pass
741+
except OSError as ex:
742+
# If the path does not exist and originally did not exist,
743+
# then strip the prefix anyway.
744+
if ex.winerror == initial_winerror:
745+
path = spath
740746
return path
741747

742748

Lib/test/test_ntpath.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,6 +1535,33 @@ def test_isjunction(self):
15351535
self.assertFalse(ntpath.isjunction('tmpdir'))
15361536
self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))
15371537

1538+
@unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
1539+
def test_realpath_volume_guid_path(self):
1540+
# gh-89760: the \\?\ prefix cannot be stripped from a volume GUID path.
1541+
# Find a volume which is not mounted as a drive.
1542+
for volume in os.listvolumes():
1543+
if not os.listmounts(volume):
1544+
break
1545+
else:
1546+
raise unittest.SkipTest('no volume without a mount point')
1547+
1548+
with os_helper.temp_dir() as d:
1549+
with os_helper.change_cwd(d):
1550+
# _winapi.CreateJunction() adds the \\??\\ prefix to a path
1551+
# which already has a prefix.
1552+
try:
1553+
subprocess.run(['cmd', '/c', 'mklink', '/j',
1554+
'testjunc', volume],
1555+
check=True, capture_output=True)
1556+
except (OSError, subprocess.CalledProcessError):
1557+
raise unittest.SkipTest('creating the test junction failed')
1558+
1559+
for path in 'testjunc', 'testjunc/spam', 'testjunc/spam/eggs':
1560+
with self.subTest(path=path):
1561+
realpath = ntpath.realpath(path)
1562+
self.assertStartsWith(realpath, '\\\\?\\Volume{')
1563+
self.assertTrue(ntpath.isabs(realpath), realpath)
1564+
15381565
def test_isfile_invalid_paths(self):
15391566
isfile = ntpath.isfile
15401567
self.assertIs(isfile('/tmp\udfffabcds'), False)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`os.path.realpath` on Windows: the ``\\?\`` prefix is no longer
2+
stripped from a volume GUID path, which made the result invalid.

0 commit comments

Comments
 (0)