GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [subprocess] run() sometimes ignores timeout in Windows · Issue #87512 · python/cpython

[subprocess] run() sometimes ignores timeout in Windows #87512

New issue
Copy link
New issue
Copy link
Open
Open
[subprocess] run() sometimes ignores timeout in Windows#87512
Copy link
Labels
OS-windowsstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-subprocessSubprocess issues.Subprocess issues.type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@eryksun
eryksun
opened on Feb 28, 2021
Issue body actions
BPO 43346
Nosy @pfmoore, @ericvsmith, @tjguk, @zware, @eryksun, @zooba

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2021-02-28.22:54:01.059>
labels = ['type-bug', '3.8', '3.9', '3.10', '3.11', 'library', 'OS-windows']
title = 'subprocess.run() sometimes ignores timeout in Windows'
updated_at = <Date 2021-06-28.18:54:15.735>
user = 'https://github.com/eryksun'

bugs.python.org fields:

activity = <Date 2021-06-28.18:54:15.735>
actor = 'zach.ware'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Library (Lib)', 'Windows']
creation = <Date 2021-02-28.22:54:01.059>
creator = 'eryksun'
dependencies = []
files = []
hgrepos = []
issue_num = 43346
keywords = []
message_count = 2.0
messages = ['387822', '387823']
nosy_count = 7.0
nosy_names = ['paul.moore', 'eric.smith', 'tim.golden', 'zach.ware', 'eryksun', 'steve.dower', 'bugale bugale']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue43346'
versions = ['Python 3.8', 'Python 3.9', 'Python 3.10', 'Python 3.11']

Linked PRs

  • gh-87512: Fix subprocess using timeout= on Windows blocking with a large input= #142058
  • [3.14] gh-87512: Fix subprocess using timeout= on Windows blocking with a large input= (GH-142058) #142068
  • [3.13] gh-87512: Fix subprocess using timeout= on Windows blocking with a large input= (GH-142058) #142069
  • gh-87512: Fix ignored timeout in subprocess.run() on Windows #155409

Activity

Next
  1. eryksun commented on Feb 28, 2021

    @eryksun
    eryksun
    on Feb 28, 2021
    ContributorAuthor
    More actions

    subprocess.run() handles TimeoutExpired by terminating the process and waiting on it. On POSIX, the exception object contains the partially read stdout and stderr bytes. For example:

        cmd = 'echo spam; echo eggs >&2; sleep 2'
        try: p = subprocess.run(cmd, shell=True, capture_output=True,
                                text=True, timeout=1)
        except subprocess.TimeoutExpired as e: ex = e
         
        >>> ex.stdout, ex.stderr
        (b'spam\n', b'eggs\n')

    On Windows, subprocess.run() has to finish reading output with a second communicate() call, after which it manually sets the exception's stdout and stderr attributes.

    The poses the problem that the second communicate() call may block indefinitely, even though the child process has terminated.

    The primary issue is that the pipe handles may be inherited by one or more descendant processes (e.g. via shell=True), which are all regarded as potential writers that keep the pipe from closing. Reading from an open pipe that's empty will block until data becomes available. This is generally desirable for efficiency, compared to polling in a loop. But in this case, the downside is that run() in Windows will effectively ignore the given timeout.

    Another problem is that _communicate() writes the input to stdin on the calling thread with a single write() call. If the input exceeds the pipe capacity (4 KiB by default -- but a pipesize 'suggested' size could be supported), the write will block until the child process reads the excess data. This could block indefinitely, which will effectively ignore a given timeout. The POSIX implementation, in contrast, correctly handles a timeout in this case.

    Also, Popen.__exit__() closes the stdout, stderr, and stdin files without regard to the _communicate() worker threads. This may seem innocuous, but if a worker thread is blocked on synchronous I/O with one of these files, WinAPI CloseHandle() will also block if it's closing the last handle for the file in the current process. (In this case, the kernel I/O manager has a close procedure that waits to acquire the file for the current thread before performing various housekeeping operations, primarily in the filesystem, such as clearing byte-range locks set by the current process.) A blocked close() is easy to demonstrate. For example:

        args = 'python -c "import time; time.sleep(99)"'
        p = subprocess.Popen(args, shell=True, stdout=subprocess.PIPE)
        try: p.communicate(timeout=1)
        except: pass
    
        p.kill() # terminates the shell process -- not python.exe
        with p: pass # stdout.close() blocks until python.exe exits

    The Windows implementation of Popen._communicate() could be redesigned as follows:

    * read in chunks, with a size from 1 byte up to the maximum available,
      as determined by `_winapi.PeekNamedPipe()`
    * write to the child's `stdin` on a separate thread
    * after `communicate()` has started, ensure that synchronous I/O in worker
      threads has been canceled via `CancelSynchronousIo()` before closing 
      the pipes.
    

    The _winapi module would need to wrap OpenThread() and CancelSynchronousIo(), plus define the TERMINATE_THREAD (0x0001) access right.

    With the proposed changes, subprocess.run() would no longer special case TimeoutExpired on Windows.

  2. eryksun
    added
    3.10 (EOL)end of lifeend of life
    3.8 (EOL)end of lifeend of life
    3.9 (EOL)end of lifeend of life
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    OS-windows
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Feb 28, 2021
  3. eryksun commented on Feb 28, 2021

    @eryksun
    eryksun
    on Feb 28, 2021
    ContributorAuthor
    More actions

    Demo Popen() methods, for discussion:

        def _read_output(self, fileobj):
            handle = msvcrt.get_osfhandle(fileobj.fileno())
            output = self._fileobj2output[fileobj]
            
            while True:
                try:
                    size = _winapi.PeekNamedPipe(handle)[0] or 1
                    data = _winapi.ReadFile(handle, size)[0]
                except BrokenPipeError:
                    break
                except OSError as e:
                    if e.winerror == _winapi.ERROR_OPERATION_ABORTED:
                        # Should this be mapped to InterruptedError
                        # (EINTR) in PC/errmap.h?
                        break
                    raise
                output.append(data)
    
            fileobj.close()
    
    
        def _communicate(self, input, endtime, orig_timeout):
            if not self._communication_started:
                self._fileobj2thread = {}
                self._fileobj2output = {}
                if self.stdout:
                    self._fileobj2output[self.stdout] = []
                if self.stderr:
                    self._fileobj2output[self.stderr] = []
    
            stdout = self._fileobj2output.get(self.stdout)
            stderr = self._fileobj2output.get(self.stderr)
    
            thread_list = []
            for fileobj in (self.stdin, self.stdout, self.stderr):
                if fileobj is None:
                    continue
                if fileobj in self._fileobj2thread:
                    thread = self._fileobj2thread[fileobj]
                else:
                    if fileobj == self.stdin:
                        target, args = self._stdin_write, (input,)
                    else:
                        target, args = self._read_output, (fileobj,)
                    thread = threading.Thread(target=target, args=args,
                                daemon=True)
                    thread.start()
                    self._fileobj2thread[fileobj] = thread
                thread_list.append(thread)
    
            for thread in thread_list:
                thread.join(self._remaining_time(endtime))
                if thread.is_alive():
                    self._check_timeout(endtime, orig_timeout,
                        stdout, stderr, skip_check_and_raise=True)
    
            # Join partial reads.
            if stdout is not None:
                stdout = b''.join(stdout)
            if stderr is not None:
                stderr = b''.join(stderr)
    
            if self.text_mode:
                if stdout is not None:
                    stdout = self._translate_newlines(stdout,
                        self.stdout.encoding, self.stdout.errors)
                if stderr is not None:
                    stderr = self._translate_newlines(stderr,
                        self.stderr.encoding, self.stderr.errors)
    
            return (stdout, stderr)
    
    
        def _cancel_io(self):
            if not hasattr(self, '_fileobj2thread'):
                return
            for fileobj in (self.stdin, self.stdout, self.stderr):
                thread = self._fileobj2thread.get(fileobj)
                if thread is None or not thread.is_alive():
                    continue
                try:
                    handle = _winapi.OpenThread(
                        _winapi.TERMINATE_THREAD, False, thread.ident)
                except OSError:
                    pass
                else:
                    try:
                        try:
                            _winapi.CancelSynchronousIo(handle)
                        except OSError:
                            pass
                    finally:
                        _winapi.CloseHandle(handle)
    
    
        def __exit__(self, exc_type, value, traceback):
            if _mswindows:
                self._cancel_io()
            # rest unchanged...
  4. zware
    added
    3.11only security fixesonly security fixes
    on Jun 28, 2021
  5. ezio-melotti
    transferred this issue fromon Apr 10, 2022
  6. eryksun
    changed the title [-]subprocess.run() sometimes ignores timeout in Windows[/-] [+][subprocess] run() sometimes ignores timeout in Windows[/+] on Aug 8, 2022
  7. gpshead
    added
    3.12only security fixesonly security fixes
    and removed
    3.9 (EOL)end of lifeend of life
    3.8 (EOL)end of lifeend of life
    on Sep 30, 2022
  8. vstinner
    added
    topic-subprocessSubprocess issues.Subprocess issues.
    on Jul 8, 2024
  9. 3 remaining items

  10. zware
    removed
    3.10 (EOL)end of lifeend of life
    3.12only security fixesonly security fixes
    on Aug 12, 2025
  11. gpshead
    added 5 commits that reference this issue on Nov 29, 2025

    pythongh-87512: Fix subprocess communicate() timeout on Windows for l…

    7d55f53

    Add NEWS entry for pythongh-87512

    20d48a9

    pythongh-87512: Fix subprocess communicate() timeout on Windows for l…

    2bb9e56

    Add NEWS entry for pythongh-87512

    908fd43

    gh-87512: Fix `subprocess` using `timeout=` on Windows blocking with …

    5b1862b
  12. miss-islington
    added 2 commits that reference this issue on Nov 29, 2025

    pythongh-87512: Fix `subprocess` using `timeout=` on Windows blocking…

    eeab726

    pythongh-87512: Fix `subprocess` using `timeout=` on Windows blocking…

    63c23ad
  13. gpshead
    added 3 commits that reference this issue on Nov 29, 2025

    [3.14] gh-87512: Fix `subprocess` using `timeout=` on Windows blockin…

    c7f7411

    [3.13] gh-87512: Fix `subprocess` using `timeout=` on Windows blockin…

    1247113

    Fix _communicate_streams_windows to avoid blocking with large input

    d420f29
  14. StanFromIreland
    added a commit that references this issue on Dec 6, 2025

    pythongh-87512: Fix `subprocess` using `timeout=` on Windows blocking…

    9fbb75e
  15. serhiy-storchaka
    added a commit that references this issue on Sep 14, 2026

    Merge remote-tracking branch 'upstream/main' into pythongh-87512-subp…

    f48da93
  16. NekoAsakura
    marked subprocess.run(timeout=N) on Windows can hang forever after the timeout kill: reader-thread join waits on inherited pipe handles #160988 as a duplicate of this issue on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    OS-windowsstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-subprocessSubprocess issues.Subprocess issues.type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402520 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.