Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Lib/ensurepip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _run_pip(args, additional_paths=None):
if sys.flags.isolated:
# run code in isolated mode if currently running isolated
cmd.insert(1, '-I')
return subprocess.run(cmd, check=True).returncode
return subprocess.run(cmd, stdin=subprocess.DEVNULL, check=True).returncode


def version():
Expand Down
47 changes: 47 additions & 0 deletions Lib/test/test_ensurepip.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import contextlib
import os
import os.path
import subprocess
import sys
import tempfile
import textwrap
import test.support
import unittest
import unittest.mock
Expand Down Expand Up @@ -362,5 +364,50 @@ def test_uninstall_error_code(self):
self.assertEqual(exit_code, 2)


class TestRunPip(unittest.TestCase):
def test_stdin_is_devnull(self):
mock_result = unittest.mock.MagicMock()
mock_result.returncode = 0
with unittest.mock.patch('ensurepip.subprocess.run',
return_value=mock_result) as mock_run:
ensurepip._run_pip(['install', 'pip'])
self.assertIs(mock_run.call_args.kwargs.get('stdin'), subprocess.DEVNULL)


class TestRunPipStdinHang(unittest.TestCase):
"""gh-145311: _run_pip must not hang when stdin is an open pipe."""

@test.support.requires_subprocess()
def test_run_pip_does_not_hang_on_piped_stdin(self):
# Spawn _run_pip in a child process whose stdin is an open pipe
# whose write end we never close — the condition that caused the hang.
script = textwrap.dedent("""\
import ensurepip, os
with ensurepip._get_pip_whl_path_ctx() as whl:
ensurepip._run_pip(["--version"], [os.fspath(whl)])
print("ok")
""")
r_fd, w_fd = os.pipe()
try:
with open(r_fd, "rb") as pipe_r:
result = subprocess.run(
[sys.executable, "-c", script],
stdin=pipe_r,
capture_output=True,
timeout=10,
text=True,
)
except subprocess.TimeoutExpired:
self.fail(
"ensurepip._run_pip hung with an open pipe on stdin; "
"ensure subprocess.run(..., stdin=subprocess.DEVNULL, ...) "
"is used (gh-145311)"
)
finally:
os.close(w_fd)
self.assertEqual(result.returncode, 0)
self.assertIn("ok", result.stdout)


if __name__ == "__main__":
unittest.main()
46 changes: 46 additions & 0 deletions Lib/test/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import sys
import sysconfig
import tempfile
import textwrap
from test.support import (captured_stdout, captured_stderr,
skip_if_broken_multiprocessing_synchronize, verbose,
requires_subprocess, is_android, is_apple_mobile,
Expand Down Expand Up @@ -253,6 +254,7 @@ def pip_cmd_checker(cmd, **kwargs):
exe_dir = os.path.normcase(os.path.dirname(cmd[0]))
expected_dir = os.path.normcase(os.path.dirname(expect_exe))
self.assertEqual(exe_dir, expected_dir)
self.assertIs(kwargs.get('stdin'), subprocess.DEVNULL) # gh-145311

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


class TestCallNewPythonStdinHang(unittest.TestCase):
"""gh-145311: _call_new_python must not hang when stdin is an open pipe."""

@requires_subprocess()
def test_call_new_python_does_not_hang_on_piped_stdin(self):
# Spawn _call_new_python in a child process whose stdin is an open
# pipe whose write end we never close — the condition that caused the
# hang before kwargs['stdin'] = subprocess.DEVNULL was added.
#
# A minimal SimpleNamespace context is sufficient: _call_new_python
# only reads context.env_exec_cmd and context.env_dir.
script = textwrap.dedent("""\
import sys, tempfile, types, venv
with tempfile.TemporaryDirectory() as env_dir:
ctx = types.SimpleNamespace(
env_exec_cmd=sys.executable,
env_dir=env_dir,
)
builder = venv.EnvBuilder()
builder._call_new_python(ctx, "-c", "pass")
print("ok")
""")
r_fd, w_fd = os.pipe()
try:
with open(r_fd, "rb") as pipe_r:
result = subprocess.run(
[sys.executable, "-c", script],
stdin=pipe_r,
capture_output=True,
timeout=10,
text=True,
)
except subprocess.TimeoutExpired:
self.fail(
"venv._call_new_python hung with an open pipe on stdin; "
"ensure kwargs['stdin'] = subprocess.DEVNULL is set "
"(gh-145311)"
)
finally:
os.close(w_fd)
self.assertEqual(result.returncode, 0)
self.assertIn("ok", result.stdout)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions Lib/venv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ def _call_new_python(self, context, *py_args, **kwargs):
env.pop('PYTHONPATH', None)
kwargs['cwd'] = context.env_dir
kwargs['executable'] = context.env_exec_cmd
kwargs['stdin'] = subprocess.DEVNULL # gh-145311
subprocess.check_output(args, **kwargs)

def _setup_pip(self, context):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :mod:`venv` and :mod:`ensurepip` hanging when stdin is connected to
a pipe. Subprocess calls in both modules now pass ``stdin=subprocess.DEVNULL``
to prevent child processes from blocking on an inherited pipe descriptor.
Loading