Skip to content

Commit b5d4b85

Browse files
[3.14] gh-89760: Fix os.path.realpath() for volume GUID paths on Windows (GH-155396) (GH-156612)
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. (cherry picked from commit e48b1eb)
1 parent d4ae98b commit b5d4b85

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
@@ -677,6 +677,7 @@ def realpath(path, *, strict=False):
677677
prefix = b'\\\\?\\'
678678
unc_prefix = b'\\\\?\\UNC\\'
679679
new_unc_prefix = b'\\\\'
680+
colon_sep = b':\\'
680681
# bpo-38081: Special case for realpath(b'nul')
681682
devnull = b'nul'
682683
if normcase(path) == devnull:
@@ -685,6 +686,7 @@ def realpath(path, *, strict=False):
685686
prefix = '\\\\?\\'
686687
unc_prefix = '\\\\?\\UNC\\'
687688
new_unc_prefix = '\\\\'
689+
colon_sep = ':\\'
688690
# bpo-38081: Special case for realpath('nul')
689691
devnull = 'nul'
690692
if normcase(path) == devnull:
@@ -722,25 +724,29 @@ def realpath(path, *, strict=False):
722724
# strip off that prefix unless it was already provided on the original
723725
# path.
724726
if not had_prefix and path.startswith(prefix):
725-
# For UNC paths, the prefix will actually be \\?\UNC\
726-
# Handle that case as well.
727+
# For UNC drives, the path starts with \\?\UNC\.
727728
if path.startswith(unc_prefix):
728729
spath = new_unc_prefix + path[len(unc_prefix):]
729-
else:
730+
# For drive-letter drives, the path starts with \\?\<letter>:\.
731+
elif path.startswith(colon_sep, len(prefix) + 1):
730732
spath = path[len(prefix):]
731-
# Ensure that the non-prefixed path resolves to the same path
732-
try:
733-
if _getfinalpathname(spath) == path:
734-
path = spath
735-
except ValueError as ex:
736-
# Unexpected, as an invalid path should not have gained a prefix
737-
# at any point, but we ignore this error just in case.
738-
pass
739-
except OSError as ex:
740-
# If the path does not exist and originally did not exist, then
741-
# strip the prefix anyway.
742-
if ex.winerror == initial_winerror:
743-
path = spath
733+
# For all others, e.g. volume GUID paths, it cannot be stripped.
734+
else:
735+
spath = None
736+
if spath is not None:
737+
# Ensure that the non-prefixed path resolves to the same path
738+
try:
739+
if _getfinalpathname(spath) == path:
740+
path = spath
741+
except ValueError:
742+
# Unexpected, as an invalid path should not have gained a
743+
# prefix at any point, but we ignore this error just in case.
744+
pass
745+
except OSError as ex:
746+
# If the path does not exist and originally did not exist,
747+
# then strip the prefix anyway.
748+
if ex.winerror == initial_winerror:
749+
path = spath
744750
return path
745751

746752

Lib/test/test_ntpath.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,33 @@ def test_realpath_drive_relative(self):
14371437
drive + ':\\spam')
14381438
self.assertEqual(ntpath.realpath(drive + ':'), drive + ':\\')
14391439

1440+
@unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
1441+
def test_realpath_volume_guid_path(self):
1442+
# gh-89760: the \\?\ prefix cannot be stripped from a volume GUID path.
1443+
# Find a volume which is not mounted as a drive.
1444+
for volume in os.listvolumes():
1445+
if not os.listmounts(volume):
1446+
break
1447+
else:
1448+
raise unittest.SkipTest('no volume without a mount point')
1449+
1450+
with os_helper.temp_dir() as d:
1451+
with os_helper.change_cwd(d):
1452+
# _winapi.CreateJunction() adds the \\??\\ prefix to a path
1453+
# which already has a prefix.
1454+
try:
1455+
subprocess.run(['cmd', '/c', 'mklink', '/j',
1456+
'testjunc', volume],
1457+
check=True, capture_output=True)
1458+
except (OSError, subprocess.CalledProcessError):
1459+
raise unittest.SkipTest('creating the test junction failed')
1460+
1461+
for path in 'testjunc', 'testjunc/spam', 'testjunc/spam/eggs':
1462+
with self.subTest(path=path):
1463+
realpath = ntpath.realpath(path)
1464+
self.assertStartsWith(realpath, '\\\\?\\Volume{')
1465+
self.assertTrue(ntpath.isabs(realpath), realpath)
1466+
14401467
def test_isfile_invalid_paths(self):
14411468
isfile = ntpath.isfile
14421469
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)