https://github.com/python/cpython/commit/68b741e10d822b2c96dc20e01a993af64401d0b9
commit: 68b741e10d822b2c96dc20e01a993af64401d0b9
branch: 3.15
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-10-09T20:58:12+03:00
summary:
[3.15] gh-89760: Fix os.path.realpath() for volume GUID paths on Windows
(GH-155396) (GH-156611)
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 e48b1eb949daacaa84c87da57732866ab1894a86)
* gh-156707: Remove the junction created in test_realpath_volume_guid_path()
The junction pointed to a volume without a mount point. It was left for the
temporary directory cleanup, which descended into it and tried to remove
files on that volume.
files:
A Misc/NEWS.d/next/Library/2026-08-09-00-30-00.gh-issue-89760.rpvol.rst
M Lib/ntpath.py
M Lib/test/test_ntpath.py
diff --git a/Lib/ntpath.py b/Lib/ntpath.py
index b3c23f0abc2d88..6649298f9f6785 100644
--- a/Lib/ntpath.py
+++ b/Lib/ntpath.py
@@ -666,6 +666,7 @@ def realpath(path, /, *, strict=False):
prefix = b'\\\\?\\'
unc_prefix = b'\\\\?\\UNC\\'
new_unc_prefix = b'\\\\'
+ colon_sep = b':\\'
cwd = os.getcwdb()
# bpo-38081: Special case for realpath(b'nul')
devnull = b'nul'
@@ -675,6 +676,7 @@ def realpath(path, /, *, strict=False):
prefix = '\\\\?\\'
unc_prefix = '\\\\?\\UNC\\'
new_unc_prefix = '\\\\'
+ colon_sep = ':\\'
cwd = os.getcwd()
# bpo-38081: Special case for realpath('nul')
devnull = 'nul'
@@ -718,25 +720,29 @@ def realpath(path, /, *, strict=False):
# strip off that prefix unless it was already provided on the original
# path.
if not had_prefix and path.startswith(prefix):
- # For UNC paths, the prefix will actually be \\?\UNC\
- # Handle that case as well.
+ # For UNC drives, the path starts with \\?\UNC\.
if path.startswith(unc_prefix):
spath = new_unc_prefix + path[len(unc_prefix):]
- else:
+ # For drive-letter drives, the path starts with \\?\<letter>:\.
+ elif path.startswith(colon_sep, len(prefix) + 1):
spath = path[len(prefix):]
- # Ensure that the non-prefixed path resolves to the same path
- try:
- if _getfinalpathname(spath) == path:
- path = spath
- except ValueError:
- # Unexpected, as an invalid path should not have gained a
prefix
- # at any point, but we ignore this error just in case.
- pass
- except OSError as ex:
- # If the path does not exist and originally did not exist, then
- # strip the prefix anyway.
- if ex.winerror == initial_winerror:
- path = spath
+ # For all others, e.g. volume GUID paths, it cannot be stripped.
+ else:
+ spath = None
+ if spath is not None:
+ # Ensure that the non-prefixed path resolves to the same path
+ try:
+ if _getfinalpathname(spath) == path:
+ path = spath
+ except ValueError:
+ # Unexpected, as an invalid path should not have gained a
+ # prefix at any point, but we ignore this error just in
case.
+ pass
+ except OSError as ex:
+ # If the path does not exist and originally did not exist,
+ # then strip the prefix anyway.
+ if ex.winerror == initial_winerror:
+ path = spath
return path
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
index a3728b58335e63..aa47c3f7070896 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -1525,6 +1525,37 @@ def test_isjunction(self):
self.assertFalse(ntpath.isjunction('tmpdir'))
self.assertPathEqual(ntpath.realpath('testjunc'),
ntpath.realpath('tmpdir'))
+ @unittest.skipIf(sys.platform != 'win32', "Can only test junctions with
creation on win32.")
+ def test_realpath_volume_guid_path(self):
+ # gh-89760: the \\?\ prefix cannot be stripped from a volume GUID path.
+ # Find a volume which is not mounted as a drive.
+ for volume in os.listvolumes():
+ if not os.listmounts(volume):
+ break
+ else:
+ raise unittest.SkipTest('no volume without a mount point')
+
+ with os_helper.temp_dir() as d:
+ with os_helper.change_cwd(d):
+ # _winapi.CreateJunction() adds the \\??\\ prefix to a path
+ # which already has a prefix.
+ try:
+ subprocess.run(['cmd', '/c', 'mklink', '/j',
+ 'testjunc', volume],
+ check=True, capture_output=True)
+ except (OSError, subprocess.CalledProcessError):
+ raise unittest.SkipTest('creating the test junction
failed')
+
+ try:
+ for path in 'testjunc', 'testjunc/spam',
'testjunc/spam/eggs':
+ with self.subTest(path=path):
+ realpath = ntpath.realpath(path)
+ self.assertStartsWith(realpath, '\\\\?\\Volume{')
+ self.assertTrue(ntpath.isabs(realpath), realpath)
+ finally:
+ # Remove the junction, not the volume it points to.
+ os_helper.rmdir('testjunc')
+
def test_isfile_invalid_paths(self):
isfile = ntpath.isfile
self.assertIs(isfile('/tmp\udfffabcds'), False)
diff --git
a/Misc/NEWS.d/next/Library/2026-08-09-00-30-00.gh-issue-89760.rpvol.rst
b/Misc/NEWS.d/next/Library/2026-08-09-00-30-00.gh-issue-89760.rpvol.rst
new file mode 100644
index 00000000000000..9218ecfe9ce62e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-09-00-30-00.gh-issue-89760.rpvol.rst
@@ -0,0 +1,2 @@
+Fix :func:`os.path.realpath` on Windows: the ``\\?\`` prefix is no longer
+stripped from a volume GUID path, which made the result invalid.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]