Skip to content

Commit 8592297

Browse files
committed
gh-157917: Do not leak the \\?\ extended-length prefix into sys.prefix on Windows venvs
When a venv interpreter is invoked through a "\\?\" extended-length path, the venv launcher reports its own path via GetModuleFileNameW, which preserves that prefix. getpath.py consumed the value verbatim, so sys.prefix, sys.exec_prefix and sys.executable all kept the prefix. Windows does not resolve ".." or any other relative component in an extended-length path, so tools that join them onto a sysconfig path break. That is what makes "pip install" of a wheel with a "scripts=" entry fail with "OSError: [Errno 22] Invalid argument". Strip the prefix back off the launcher-provided path on Windows. A path that would still exceed MAX_PATH once unprefixed keeps the prefix, since the extended-length form is the only usable one for it.
1 parent 00307b0 commit 8592297

3 files changed

Lines changed: 59 additions & 0 deletions

File tree

‎Lib/test/test_venv.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,28 @@ def test_prefixes(self):
302302
self.assertEqual(pathlib.Path(out.strip().decode()),
303303
pathlib.Path(expected), prefix)
304304

305+
# gh-157917: the "\\?\" extended-length prefix must not leak into the prefix
306+
@requireVenvCreate
307+
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
308+
def test_prefixes_extended_length_path(self):
309+
"""
310+
Test that invoking a venv through a "\\?\" extended-length path does
311+
not leave that prefix behind in the prefixes. Windows does not
312+
resolve relative components (such as ".." or "/") in such paths, so
313+
they break tools that join them onto sysconfig-derived paths.
314+
"""
315+
rmtree(self.env_dir)
316+
self.run_with_capture(venv.create, self.env_dir)
317+
envpy = os.path.join('\\\\?\\' + self.env_dir, self.bindir, self.exe)
318+
for prefix in ('prefix', 'exec_prefix'):
319+
with self.subTest(prefix):
320+
out, err = check_output(
321+
[envpy, '-c', 'import sys; print(sys.%s)' % prefix],
322+
encoding='utf-8')
323+
self.assertFalse(out.strip().startswith('\\\\?\\'), out)
324+
self.assertEqual(pathlib.Path(out.strip()),
325+
pathlib.Path(self.env_dir), prefix)
326+
305327
@requireVenvCreate
306328
def test_sysconfig(self):
307329
"""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Fix :attr:`sys.prefix` and :attr:`sys.exec_prefix` in a virtual environment
2+
retaining the ``\\?\`` extended-length path prefix when the interpreter is
3+
invoked through such a path on Windows. Windows does not resolve relative
4+
path components in extended-length paths, so the leaked prefix caused tools
5+
that join ``..`` components onto :mod:`sysconfig` paths to fail with
6+
``OSError: [Errno 22]``. The virtual environment is now located via the
7+
canonical executable path reported by the OS rather than the literal
8+
``argv[0]``, matching the behaviour of 3.10 and earlier.

‎Modules/getpath.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,28 @@ def search_up(prefix, *landmarks, test=isfile):
211211
prefix = dirname(prefix)
212212

213213

214+
# The traditional Windows path length limit, not counting the terminating NUL.
215+
MAX_PATH = 260
216+
217+
218+
def strip_extended_length_prefix(path):
219+
"""
220+
Remove the leading "\\\\?\\" of a Windows extended-length path, and
221+
rewrite the "\\\\?\\UNC\\" form of a UNC path back to a plain "\\\\" one.
222+
Windows does not resolve relative components (such as ".." or "/") in an
223+
extended-length path, so keeping the prefix in a path that we later join
224+
onto is liable to break consumers. A path that is still too long to be
225+
used unprefixed keeps its prefix, as it has no other usable form.
226+
"""
227+
if path.startswith('\\\\?\\UNC\\'):
228+
stripped = '\\\\' + path[8:]
229+
elif path.startswith('\\\\?\\'):
230+
stripped = path[4:]
231+
else:
232+
return path
233+
return stripped if len(stripped) < MAX_PATH else path
234+
235+
214236
# ******************************************************************************
215237
# READ VARIABLES FROM config
216238
# ******************************************************************************
@@ -319,6 +341,13 @@ def search_up(prefix, *landmarks, test=isfile):
319341
real_executable = base_executable
320342
#real_executable_dir = dirname(real_executable)
321343
executable = ENV_PYTHONEXECUTABLE or ENV___PYVENV_LAUNCHER__
344+
if os_name == 'nt':
345+
# QUIRK: On Windows the venv launcher reports its own path through
346+
# GetModuleFileNameW, which hands back a "\\?\" extended-length path
347+
# whenever that is how the interpreter was invoked. Drop that prefix
348+
# again, so the venv prefix and sys.executable stay usable by tools
349+
# that join relative components onto them (gh-157917).
350+
executable = strip_extended_length_prefix(executable)
322351
executable_dir = dirname(executable)
323352

324353

0 commit comments

Comments
 (0)