https://github.com/python/cpython/commit/c7f741100b53d064d995c13072cb439f8c34ed64
commit: c7f741100b53d064d995c13072cb439f8c34ed64
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: gpshead <[email protected]>
date: 2025-11-29T06:47:04Z
summary:

[3.14] gh-87512: Fix `subprocess` using `timeout=` on Windows blocking with a 
large `input=` (GH-142058) (#142068)

gh-87512: Fix `subprocess` using `timeout=` on Windows blocking with a large 
`input=` (GH-142058)

On Windows, Popen._communicate() previously wrote to stdin synchronously, which 
could block indefinitely if the subprocess didn't consume input= quickly and 
the pipe buffer filled up. The timeout= parameter was only checked when joining 
the reader threads, not during the stdin write.

This change moves the Windows stdin writing to a background thread (similar to 
how stdout/stderr are read in threads), allowing the timeout to be properly 
enforced. If timeout expires, TimeoutExpired is raised promptly and the writer 
thread continues in the background. Subsequent calls to communicate() will join 
the existing writer thread.

Adds test_communicate_timeout_large_input to verify that TimeoutExpired is 
raised promptly when communicate() is called with large input and a timeout, 
even when the subprocess doesn't consume stdin quickly.

This test already passed on POSIX (where select() is used) but failed on 
Windows where the stdin write blocks without checking the timeout.
(cherry picked from commit 5b1862bdd8021b5295df95d730c2d2efa827fa88)

Co-authored-by: Gregory P. Smith <[email protected]>
Co-authored-by: Claude Opus 4.5 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-11-29-03-02-45.gh-issue-87512.bn4xbm.rst
M Lib/subprocess.py
M Lib/test/test_subprocess.py

diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 301133e9e9c712..7c7ee51f514ed1 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -1614,6 +1614,10 @@ def _readerthread(self, fh, buffer):
             fh.close()
 
 
+        def _writerthread(self, input):
+            self._stdin_write(input)
+
+
         def _communicate(self, input, endtime, orig_timeout):
             # Start reader threads feeding into a list hanging off of this
             # object, unless they've already been started.
@@ -1632,8 +1636,23 @@ def _communicate(self, input, endtime, orig_timeout):
                 self.stderr_thread.daemon = True
                 self.stderr_thread.start()
 
-            if self.stdin:
-                self._stdin_write(input)
+            # Start writer thread to send input to stdin, unless already
+            # started.  The thread writes input and closes stdin when done,
+            # or continues in the background on timeout.
+            if self.stdin and not hasattr(self, "_stdin_thread"):
+                self._stdin_thread = \
+                        threading.Thread(target=self._writerthread,
+                                         args=(input,))
+                self._stdin_thread.daemon = True
+                self._stdin_thread.start()
+
+            # Wait for the writer thread, or time out.  If we time out, the
+            # thread remains writing and the fd left open in case the user
+            # calls communicate again.
+            if hasattr(self, "_stdin_thread"):
+                self._stdin_thread.join(self._remaining_time(endtime))
+                if self._stdin_thread.is_alive():
+                    raise TimeoutExpired(self.args, orig_timeout)
 
             # Wait for the reader threads, or time out.  If we time out, the
             # threads remain reading and the fds left open in case the user
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index 5e73810dcb7253..b869e30135e02b 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -1034,6 +1034,62 @@ def test_communicate_timeout_large_output(self):
         (stdout, _) = p.communicate()
         self.assertEqual(len(stdout), 4 * 64 * 1024)
 
+    def test_communicate_timeout_large_input(self):
+        # Test that timeout is enforced when writing large input to a
+        # slow-to-read subprocess, and that partial input is preserved
+        # for continuation after timeout (gh-141473).
+        #
+        # This is a regression test for Windows matching POSIX behavior.
+        # On POSIX, select() is used to multiplex I/O with timeout checking.
+        # On Windows, stdin writing must also honor the timeout rather than
+        # blocking indefinitely when the pipe buffer fills.
+
+        # Input larger than typical pipe buffer (4-64KB on Windows)
+        input_data = b"x" * (128 * 1024)
+
+        p = subprocess.Popen(
+            [sys.executable, "-c",
+             "import sys, time; "
+             "time.sleep(30); "  # Don't read stdin for a long time
+             "sys.stdout.buffer.write(sys.stdin.buffer.read())"],
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE)
+
+        try:
+            timeout = 0.2
+            start = time.monotonic()
+            try:
+                p.communicate(input_data, timeout=timeout)
+                # If we get here without TimeoutExpired, the timeout was 
ignored
+                elapsed = time.monotonic() - start
+                self.fail(
+                    f"TimeoutExpired not raised. communicate() completed in "
+                    f"{elapsed:.2f}s, but subprocess sleeps for 30s. "
+                    "Stdin writing blocked without enforcing timeout.")
+            except subprocess.TimeoutExpired:
+                elapsed = time.monotonic() - start
+
+            # Timeout should occur close to the specified timeout value,
+            # not after waiting for the subprocess to finish sleeping.
+            # Allow generous margin for slow CI, but must be well under
+            # the subprocess sleep time.
+            self.assertLess(elapsed, 5.0,
+                f"TimeoutExpired raised after {elapsed:.2f}s; expected 
~{timeout}s. "
+                "Stdin writing blocked without checking timeout.")
+
+            # After timeout, continue communication. The remaining input
+            # should be sent and we should receive all data back.
+            stdout, stderr = p.communicate()
+
+            # Verify all input was eventually received by the subprocess
+            self.assertEqual(len(stdout), len(input_data),
+                f"Expected {len(input_data)} bytes output but got 
{len(stdout)}")
+            self.assertEqual(stdout, input_data)
+        finally:
+            p.kill()
+            p.wait()
+
     # Test for the fd leak reported in http://bugs.python.org/issue2791.
     def test_communicate_pipe_fd_leak(self):
         for stdin_pipe in (False, True):
diff --git 
a/Misc/NEWS.d/next/Library/2025-11-29-03-02-45.gh-issue-87512.bn4xbm.rst 
b/Misc/NEWS.d/next/Library/2025-11-29-03-02-45.gh-issue-87512.bn4xbm.rst
new file mode 100644
index 00000000000000..091350108eea1b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-29-03-02-45.gh-issue-87512.bn4xbm.rst
@@ -0,0 +1,5 @@
+Fix :func:`subprocess.Popen.communicate` timeout handling on Windows
+when writing large input. Previously, the timeout was ignored during
+stdin writing, causing the method to block indefinitely if the child
+process did not consume input quickly. The stdin write is now performed
+in a background thread, allowing the timeout to be properly enforced.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to