diff --git a/Lib/ntpath.py b/Lib/ntpath.py index b3c23f0abc2d889..6649298f9f67850 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 \\?\:\. + 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 936332bf94ffe77..df7bb011ede208e 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -1535,6 +1535,33 @@ 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') + + 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 000000000000000..9218ecfe9ce62e3 --- /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.