From 8909bfd92e8b719bd27971a6a7d9aa3d63318b20 Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:04:28 -0600 Subject: [PATCH 1/2] 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. --- Lib/ensurepip/__init__.py | 2 +- Lib/test/test_ensurepip.py | 47 +++++++++++++++++++ Lib/test/test_venv.py | 46 ++++++++++++++++++ Lib/venv/__init__.py | 1 + ...-03-15-09-47-31.gh-issue-145311.tKxdj2.rst | 5 ++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 6164ea62324cce..1746b540ea5048 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -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(): diff --git a/Lib/test/test_ensurepip.py b/Lib/test/test_ensurepip.py index c62b340f6a340f..2ecf1daf006a86 100644 --- a/Lib/test/test_ensurepip.py +++ b/Lib/test/test_ensurepip.py @@ -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 @@ -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() diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 78461abcd69f33..438bd3e85b94d5 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -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, @@ -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): @@ -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() diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index 21f82125f5a7c4..058425d41be5be 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -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): diff --git a/Misc/NEWS.d/next/Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst b/Misc/NEWS.d/next/Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst new file mode 100644 index 00000000000000..c134dd5a7b8e5d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst @@ -0,0 +1,5 @@ +Fix :mod:`venv` and :mod:`ensurepip` hanging when stdin is connected to +a pipe. :func:`subprocess.check_output` in +:meth:`~venv.EnvBuilder._call_new_python` and :func:`subprocess.run` in +:func:`ensurepip._run_pip` now pass ``stdin=subprocess.DEVNULL`` to +prevent child processes from blocking on an inherited pipe descriptor. From fb81c839ff08b03b95cb173e629c9e3799d9b8ff Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:21:36 -0600 Subject: [PATCH 2/2] gh-145311: fix NEWS blurb sphinx refs to private APIs --- .../Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst b/Misc/NEWS.d/next/Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst index c134dd5a7b8e5d..0a83a24a8c805d 100644 --- a/Misc/NEWS.d/next/Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst +++ b/Misc/NEWS.d/next/Library/2026-03-15-09-47-31.gh-issue-145311.tKxdj2.rst @@ -1,5 +1,3 @@ Fix :mod:`venv` and :mod:`ensurepip` hanging when stdin is connected to -a pipe. :func:`subprocess.check_output` in -:meth:`~venv.EnvBuilder._call_new_python` and :func:`subprocess.run` in -:func:`ensurepip._run_pip` now pass ``stdin=subprocess.DEVNULL`` to -prevent child processes from blocking on an inherited pipe descriptor. +a pipe. Subprocess calls in both modules now pass ``stdin=subprocess.DEVNULL`` +to prevent child processes from blocking on an inherited pipe descriptor.