Skip to content

Commit 8909bfd

Browse files
committed
gh-145311: fix ensurepip and venv hang when stdin is a pipe
Pass `stdin=subprocess.DEVNULL` to the subprocess calls in `ensurepip._run_pip()` and `venv.EnvBuilder._call_new_python()` so they do not inherit an open pipe from the parent process and hang waiting for input that never arrives.
1 parent 40095d5 commit 8909bfd

File tree

5 files changed

+100
-1
lines changed

5 files changed

+100
-1
lines changed

Lib/ensurepip/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def _run_pip(args, additional_paths=None):
8484
if sys.flags.isolated:
8585
# run code in isolated mode if currently running isolated
8686
cmd.insert(1, '-I')
87-
return subprocess.run(cmd, check=True).returncode
87+
return subprocess.run(cmd, stdin=subprocess.DEVNULL, check=True).returncode
8888

8989

9090
def version():

Lib/test/test_ensurepip.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import contextlib
22
import os
33
import os.path
4+
import subprocess
45
import sys
56
import tempfile
7+
import textwrap
68
import test.support
79
import unittest
810
import unittest.mock
@@ -362,5 +364,50 @@ def test_uninstall_error_code(self):
362364
self.assertEqual(exit_code, 2)
363365

364366

367+
class TestRunPip(unittest.TestCase):
368+
def test_stdin_is_devnull(self):
369+
mock_result = unittest.mock.MagicMock()
370+
mock_result.returncode = 0
371+
with unittest.mock.patch('ensurepip.subprocess.run',
372+
return_value=mock_result) as mock_run:
373+
ensurepip._run_pip(['install', 'pip'])
374+
self.assertIs(mock_run.call_args.kwargs.get('stdin'), subprocess.DEVNULL)
375+
376+
377+
class TestRunPipStdinHang(unittest.TestCase):
378+
"""gh-145311: _run_pip must not hang when stdin is an open pipe."""
379+
380+
@test.support.requires_subprocess()
381+
def test_run_pip_does_not_hang_on_piped_stdin(self):
382+
# Spawn _run_pip in a child process whose stdin is an open pipe
383+
# whose write end we never close — the condition that caused the hang.
384+
script = textwrap.dedent("""\
385+
import ensurepip, os
386+
with ensurepip._get_pip_whl_path_ctx() as whl:
387+
ensurepip._run_pip(["--version"], [os.fspath(whl)])
388+
print("ok")
389+
""")
390+
r_fd, w_fd = os.pipe()
391+
try:
392+
with open(r_fd, "rb") as pipe_r:
393+
result = subprocess.run(
394+
[sys.executable, "-c", script],
395+
stdin=pipe_r,
396+
capture_output=True,
397+
timeout=10,
398+
text=True,
399+
)
400+
except subprocess.TimeoutExpired:
401+
self.fail(
402+
"ensurepip._run_pip hung with an open pipe on stdin; "
403+
"ensure subprocess.run(..., stdin=subprocess.DEVNULL, ...) "
404+
"is used (gh-145311)"
405+
)
406+
finally:
407+
os.close(w_fd)
408+
self.assertEqual(result.returncode, 0)
409+
self.assertIn("ok", result.stdout)
410+
411+
365412
if __name__ == "__main__":
366413
unittest.main()

Lib/test/test_venv.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import sys
1818
import sysconfig
1919
import tempfile
20+
import textwrap
2021
from test.support import (captured_stdout, captured_stderr,
2122
skip_if_broken_multiprocessing_synchronize, verbose,
2223
requires_subprocess, is_android, is_apple_mobile,
@@ -253,6 +254,7 @@ def pip_cmd_checker(cmd, **kwargs):
253254
exe_dir = os.path.normcase(os.path.dirname(cmd[0]))
254255
expected_dir = os.path.normcase(os.path.dirname(expect_exe))
255256
self.assertEqual(exe_dir, expected_dir)
257+
self.assertIs(kwargs.get('stdin'), subprocess.DEVNULL) # gh-145311
256258

257259
fake_context = builder.ensure_directories(fake_env_dir)
258260
with patch('venv.subprocess.check_output', pip_cmd_checker):
@@ -1058,5 +1060,49 @@ def test_with_pip(self):
10581060
self.do_test_with_pip(True)
10591061

10601062

1063+
class TestCallNewPythonStdinHang(unittest.TestCase):
1064+
"""gh-145311: _call_new_python must not hang when stdin is an open pipe."""
1065+
1066+
@requires_subprocess()
1067+
def test_call_new_python_does_not_hang_on_piped_stdin(self):
1068+
# Spawn _call_new_python in a child process whose stdin is an open
1069+
# pipe whose write end we never close — the condition that caused the
1070+
# hang before kwargs['stdin'] = subprocess.DEVNULL was added.
1071+
#
1072+
# A minimal SimpleNamespace context is sufficient: _call_new_python
1073+
# only reads context.env_exec_cmd and context.env_dir.
1074+
script = textwrap.dedent("""\
1075+
import sys, tempfile, types, venv
1076+
with tempfile.TemporaryDirectory() as env_dir:
1077+
ctx = types.SimpleNamespace(
1078+
env_exec_cmd=sys.executable,
1079+
env_dir=env_dir,
1080+
)
1081+
builder = venv.EnvBuilder()
1082+
builder._call_new_python(ctx, "-c", "pass")
1083+
print("ok")
1084+
""")
1085+
r_fd, w_fd = os.pipe()
1086+
try:
1087+
with open(r_fd, "rb") as pipe_r:
1088+
result = subprocess.run(
1089+
[sys.executable, "-c", script],
1090+
stdin=pipe_r,
1091+
capture_output=True,
1092+
timeout=10,
1093+
text=True,
1094+
)
1095+
except subprocess.TimeoutExpired:
1096+
self.fail(
1097+
"venv._call_new_python hung with an open pipe on stdin; "
1098+
"ensure kwargs['stdin'] = subprocess.DEVNULL is set "
1099+
"(gh-145311)"
1100+
)
1101+
finally:
1102+
os.close(w_fd)
1103+
self.assertEqual(result.returncode, 0)
1104+
self.assertIn("ok", result.stdout)
1105+
1106+
10611107
if __name__ == "__main__":
10621108
unittest.main()

Lib/venv/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ def _call_new_python(self, context, *py_args, **kwargs):
444444
env.pop('PYTHONPATH', None)
445445
kwargs['cwd'] = context.env_dir
446446
kwargs['executable'] = context.env_exec_cmd
447+
kwargs['stdin'] = subprocess.DEVNULL # gh-145311
447448
subprocess.check_output(args, **kwargs)
448449

449450
def _setup_pip(self, context):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :mod:`venv` and :mod:`ensurepip` hanging when stdin is connected to
2+
a pipe. :func:`subprocess.check_output` in
3+
:meth:`~venv.EnvBuilder._call_new_python` and :func:`subprocess.run` in
4+
:func:`ensurepip._run_pip` now pass ``stdin=subprocess.DEVNULL`` to
5+
prevent child processes from blocking on an inherited pipe descriptor.

0 commit comments

Comments
 (0)