https://github.com/python/cpython/commit/b5d4b8544d5706fd80f7a8b278f45d25adfc2c69
commit: b5d4b8544d5706fd80f7a8b278f45d25adfc2c69
branch: 3.14
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-29T20:27:08Z
summary:

[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 e48b1eb949daacaa84c87da57732866ab1894a86)

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 8e5eb95474e488..ba19b193580364 100644
--- a/Lib/ntpath.py
+++ b/Lib/ntpath.py
@@ -677,6 +677,7 @@ def realpath(path, *, strict=False):
             prefix = b'\\\\?\\'
             unc_prefix = b'\\\\?\\UNC\\'
             new_unc_prefix = b'\\\\'
+            colon_sep = b':\\'
             # bpo-38081: Special case for realpath(b'nul')
             devnull = b'nul'
             if normcase(path) == devnull:
@@ -685,6 +686,7 @@ def realpath(path, *, strict=False):
             prefix = '\\\\?\\'
             unc_prefix = '\\\\?\\UNC\\'
             new_unc_prefix = '\\\\'
+            colon_sep = ':\\'
             # bpo-38081: Special case for realpath('nul')
             devnull = 'nul'
             if normcase(path) == devnull:
@@ -722,25 +724,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 as ex:
-                # 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 e1902f90db87ee..e10e062639ff1d 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -1437,6 +1437,33 @@ def test_realpath_drive_relative(self):
                          drive + ':\\spam')
         self.assertEqual(ntpath.realpath(drive + ':'), drive + ':\\')
 
+    @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')
+
+                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)
+
     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]

Reply via email to