diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 2f30d3108021dc..42b2b2fc9415e5 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -302,6 +302,28 @@ def test_prefixes(self): self.assertEqual(pathlib.Path(out.strip().decode()), pathlib.Path(expected), prefix) + # gh-157917: the "\\?\" extended-length prefix must not leak into the prefix + @requireVenvCreate + @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') + def test_prefixes_extended_length_path(self): + """ + Test that invoking a venv through a "\\?\" extended-length path does + not leave that prefix behind in the prefixes. Windows does not + resolve relative components (such as ".." or "/") in such paths, so + they break tools that join them onto sysconfig-derived paths. + """ + rmtree(self.env_dir) + self.run_with_capture(venv.create, self.env_dir) + envpy = os.path.join('\\\\?\\' + self.env_dir, self.bindir, self.exe) + for prefix in ('prefix', 'exec_prefix'): + with self.subTest(prefix): + out, err = check_output( + [envpy, '-c', 'import sys; print(sys.%s)' % prefix], + encoding='utf-8') + self.assertFalse(out.strip().startswith('\\\\?\\'), out) + self.assertEqual(pathlib.Path(out.strip()), + pathlib.Path(self.env_dir), prefix) + @requireVenvCreate def test_sysconfig(self): """ diff --git a/Misc/NEWS.d/next/Windows/2026-09-30-00-00-00.gh-issue-157917.48oa0L.rst b/Misc/NEWS.d/next/Windows/2026-09-30-00-00-00.gh-issue-157917.48oa0L.rst new file mode 100644 index 00000000000000..2d5e6a14a31d53 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-09-30-00-00-00.gh-issue-157917.48oa0L.rst @@ -0,0 +1,9 @@ +Fix :attr:`sys.prefix`, :attr:`sys.exec_prefix` and :attr:`sys.executable` +retaining the ``\\?\`` extended-length path prefix in a virtual environment +when the interpreter is invoked through such a path on Windows. The venv +launcher reports its own path through ``GetModuleFileNameW``, which preserves +the prefix, and that value was used verbatim. Windows does not resolve +relative path components in extended-length paths, so the leaked prefix made +tools that join ``..`` components onto :mod:`sysconfig` paths fail with +``OSError: [Errno 22]``. The prefix is now stripped again, except for paths +that are still too long to be used without it. diff --git a/Modules/getpath.py b/Modules/getpath.py index c322d5c4b4c251..26c255e7107ca2 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -211,6 +211,28 @@ def search_up(prefix, *landmarks, test=isfile): prefix = dirname(prefix) +# The traditional Windows path length limit, not counting the terminating NUL. +MAX_PATH = 260 + + +def strip_extended_length_prefix(path): + """ + Remove the leading "\\\\?\\" of a Windows extended-length path, and + rewrite the "\\\\?\\UNC\\" form of a UNC path back to a plain "\\\\" one. + Windows does not resolve relative components (such as ".." or "/") in an + extended-length path, so keeping the prefix in a path that we later join + onto is liable to break consumers. A path that is still too long to be + used unprefixed keeps its prefix, as it has no other usable form. + """ + if path.startswith('\\\\?\\UNC\\'): + stripped = '\\\\' + path[8:] + elif path.startswith('\\\\?\\'): + stripped = path[4:] + else: + return path + return stripped if len(stripped) < MAX_PATH else path + + # ****************************************************************************** # READ VARIABLES FROM config # ****************************************************************************** @@ -319,6 +341,13 @@ def search_up(prefix, *landmarks, test=isfile): real_executable = base_executable #real_executable_dir = dirname(real_executable) executable = ENV_PYTHONEXECUTABLE or ENV___PYVENV_LAUNCHER__ + if os_name == 'nt': + # QUIRK: On Windows the venv launcher reports its own path through + # GetModuleFileNameW, which hands back a "\\?\" extended-length path + # whenever that is how the interpreter was invoked. Drop that prefix + # again, so the venv prefix and sys.executable stay usable by tools + # that join relative components onto them (gh-157917). + executable = strip_extended_length_prefix(executable) executable_dir = dirname(executable)