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 — Pipe inheritance broken on Windows since 3.13 · Issue #133506 · python/cpython

Pipe inheritance broken on Windows since 3.13 #133506

New issue
Copy link
New issue
Copy link
Open
Open
Pipe inheritance broken on Windows since 3.13#133506
Copy link
Labels
3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixesOS-windowstype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@schlamar
schlamar
opened on May 6, 2025
Issue body actions

Bug report

Bug description:

Passing an inheritable pipe file descriptor to a child process is somehow broken on Windows since Python 3.13. Opening the file descriptor in the child process crashes with [WinError 6] the handle is invalid.

> py -3.12-64 .\main.py
INFO:root:S: Starting
INFO:root:Duration for 10000 requests: 0.37447249999968335 s
INFO:root:S: EOF Quitting
> py -3.13-64 .\main.py
INFO:root:S: Starting
Traceback (most recent call last):
  File "D:\temp\test-pipe-main\sub.py", line 31, in <module>
    main()
    ~~~~^^
  File "D:\temp\test-pipe-main\sub.py", line 14, in main
    reply_write_fobj = os.fdopen(reply_write_fd, "wb", buffering=0)
  File "<frozen os>", line 1068, in fdopen
OSError: [WinError 6] Das Handle ist ungültig

Minimal example

import logging
import os
import pickle
import sys
import time


def main():
    logging.basicConfig(level=logging.INFO)
    reply_read_fd, reply_write_fd = os.pipe()
    request_read_fd, request_write_fd = os.pipe()
    child_fds = [reply_write_fd, request_read_fd]
    for fd in child_fds:
        os.set_inheritable(fd, True)

    reply_read_fobj = os.fdopen(reply_read_fd, "rb", buffering=0)
    request_write_fobj = os.fdopen(request_write_fd, "wb", buffering=0)

    child_pid = os.spawnl(
        os.P_NOWAIT,
        sys.executable,
        os.path.basename(sys.executable),
        "sub.py",
        *(str(fd) for fd in child_fds),
    )
    for fd in child_fds:
        os.close(fd)

    number_requests = 10000
    start = time.perf_counter()
    for i in range(number_requests):
        request = f"test{i}"
        pickle.dump(request, request_write_fobj)
        logging.debug(f"C: Sent {request}")
        reply = pickle.load(reply_read_fobj)
        logging.debug(f"C: Got {reply}")

    duration = time.perf_counter() - start
    logging.info(f"Duration for {number_requests} requests: {duration} s")
    request_write_fobj.close()
    os.waitpid(child_pid, 0)
    time.sleep(1)


if __name__ == "__main__":
    main()
import logging
import os
import pickle
import sys


def main():
    logging.basicConfig(level=logging.INFO)
    reply_write_fd = int(sys.argv[1])
    request_read_fd = int(sys.argv[2])

    logging.info("S: Starting")

    reply_write_fobj = os.fdopen(reply_write_fd, "wb", buffering=0)
    request_read_fobj = os.fdopen(request_read_fd, "rb", buffering=0)
    logging.debug("S: Sent event")
    while True:
        try:
            request: str = pickle.load(request_read_fobj)
        except EOFError:
            logging.info("S: EOF Quitting")
            return
        logging.debug(f"S: Handle {request}")
        reply = "test"
        pickle.dump(reply, reply_write_fobj)

        logging.debug("S: Sent event")


if __name__ == "__main__":
    main()

CPython versions tested on:

3.12, 3.13

Operating systems tested on:

Windows

Activity

  1. schlamar
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on May 6, 2025
  2. ZeroIntensity
    added
    OS-windows
    on May 6, 2025
  3. ZeroIntensity commented on May 6, 2025

    @ZeroIntensity
    ZeroIntensity
    on May 6, 2025
    Member
    More actions

    You close the file descriptors before waiting on the child, so the child process might not have opened them by the time they're closed. It's a race condition. It wasn't correct on prior versions, but I think you just got lucky that it didn't break before. In 3.13, something (probably startup) is slightly slower, which revealed the race.

    I would just wait until you've received a message before closing the pipes in the parent.

  4. ZeroIntensity
    added
    pendingThe issue will be closed if no feedback is providedThe issue will be closed if no feedback is provided
    on May 6, 2025
  5. zooba commented on May 6, 2025

    @zooba
    zooba
    on May 6, 2025
    Member
    More actions

    There are also a few ways that Python may launch that don't properly handle the CRT's emulation of file descriptors (they aren't real things on Windows, so they don't always match POSIX behaviour perfectly). spawnl is also emulated.

    The multiprocessing module is the platform independent way to do this.

    That said, if there's actually a bug causing the change on 3.13, we should fix it. If @ZeroIntensity's analysis is correct then maybe it's worth a doc update (if we can choose a good place to put some notes), but ultimately, low-level POSIX emulation on Windows is going to require testing and tweaking to work well.

  6. schlamar commented on May 6, 2025

    @schlamar
    schlamar
    on May 6, 2025
    ContributorAuthor
    More actions

    You close the file descriptors before waiting on the child, so the child process might not have opened them by the time they're closed. It's a race condition. It wasn't correct on prior versions, but I think you just got lucky that it didn't break before. In 3.13, something (probably startup) is slightly slower, which revealed the race.

    I would just wait until you've received a message before closing the pipes in the parent.

    No, that is how this should be done. Handles are duplicated, so the parent handles should be closed after child process is spawned. Here is a similar Microsoft example doing exactly the same. https://learn.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output

    Plus, there is the same behavior if I don't close the child file descriptors in the parent process.

  7. schlamar commented on May 6, 2025

    @schlamar
    schlamar
    on May 6, 2025
    ContributorAuthor
    More actions

    The multiprocessing module is the platform independent way to do this.

    I need multiples pipes from a 64bit to a 32bit process. So the multiprocessing module doesn't cover my use case at all.

  8. schlamar commented on May 6, 2025

    @schlamar
    schlamar
    on May 6, 2025
    ContributorAuthor
    More actions

    This is very likely related or a regression from #77046 / #113817

  9. ZeroIntensity commented on May 6, 2025

    @ZeroIntensity
    ZeroIntensity
    on May 6, 2025
    Member
    More actions

    No, that is how this should be done. Handles are duplicated, so the parent handles should be closed after child process is spawned.

    Hm, odd. I don't think it works the same on POSIX.

  10. schlamar commented on May 6, 2025

    @schlamar
    schlamar
    on May 6, 2025
    ContributorAuthor
    More actions

    It works as expected on Python 3.12 and 3.13 if I pass handles to child process by wrapping it around msvcrt.get_osfhandle and msvcrt.open_osfhandle

        child_handles = [msvcrt.get_osfhandle(fd) for fd in child_fds]
        child_pid = os.spawnl(
            os.P_NOWAIT,
            sys.executable,
            os.path.basename(sys.executable),
            "sub.py",
            *(str(handle) for handle in child_handles),
        )
        reply_write_handle = int(sys.argv[1])
        reply_write_fd = msvcrt.open_osfhandle(reply_write_handle, os.O_NOINHERIT)
        request_read_handle = int(sys.argv[2])
        request_read_fd = msvcrt.open_osfhandle(
            request_read_handle, os.O_RDONLY | os.O_NOINHERIT
        )
  11. schlamar commented on May 6, 2025

    @schlamar
    schlamar
    on May 6, 2025
    ContributorAuthor
    More actions

    @vstinner asked for a use case in #77046 (comment), so I elaborate a little bit: Pipes in combination with pickle is a really elegant and simple pattern for inter process communication. I am aware that the above example could probably be solved with STDOUT / STDIN as pipes and subprocess. However in my specific case I need a third pipe for out of band event messaging.

  12. ZeroIntensity
    removed
    pendingThe issue will be closed if no feedback is providedThe issue will be closed if no feedback is provided
    on May 6, 2025
  13. zooba commented on May 6, 2025

    @zooba
    zooba
    on May 6, 2025
    Member
    More actions

    Handles are duplicated, so the parent handles should be closed after child process is spawned. Here is a similar Microsoft example doing exactly the same. https://learn.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output

    Again, the problem as I stated is that you aren't dealing with HANDLEs directly here, but you're dealing with the CRT's file descriptor emulation. So the only directly relevant example would be using the Microsoft C Runtime's APIs exclusively, and not touching any native APIs (like the one you showed).

    Skim-reading the discussion at #113817 (I don't have time to reabsorb it all right now) it seems we were aware that pipes inside file descriptors were broken when it comes to spawn, and changing the inheritance behaviour really just moves the point of failure (to hopefully not involve a crash). Someone is going to have to dig deeper to find a full resolution (which probably involves reporting bugs to Microsoft and then waiting for new OS releases to include the fix).

  14. schlamar commented on May 7, 2025

    @schlamar
    schlamar
    on May 7, 2025
    ContributorAuthor
    More actions

    Again, the problem as I stated is that you aren't dealing with HANDLEs directly here, but you're dealing with the CRT's file descriptor emulation. So the only directly relevant example would be using the Microsoft C Runtime's APIs exclusively, and not touching any native APIs (like the one you showed).

    I have just a rough idea what you mean with that. Is there some documentation of these specifics regarding Python API vs. CRT?

  15. schlamar commented on May 7, 2025

    @schlamar
    schlamar
    on May 7, 2025
    ContributorAuthor
    More actions

    I found an interesting comment in #113817 (comment)

    Maybe in Python 3.12, it's possible to inherit some file descriptors on Windows using os.spawnl(), but for me, it's more an accident than a deliberate choice, and the code is not portable.

    So I guess to correct solution would be to use subprocess with handle_list

        child_handles = [msvcrt.get_osfhandle(fd) for fd in child_fds]
        for child_handle in child_handles:
            os.set_handle_inheritable(child_handle, True)
    
        command = [sys.executable, "sub.py"] + [str(handle) for handle in child_handles]
        startup_info = subprocess.STARTUPINFO(
            lpAttributeList={"handle_list": child_handles}
        )
        process = subprocess.Popen(command, startupinfo=startup_info)
    
  16. zooba commented on May 7, 2025

    @zooba
    zooba
    on May 7, 2025
    Member
    More actions

    So I guess to correct solution would be to use subprocess with handle_list

    This is going to work better, yeah. You should be able to open_osfhandle on the other side to get a file descriptor back, but sharing the HANDLE value directly is going to be working with the proper OS APIs. However, do be aware that if this goes via the py.exe redirector or a venv's python.exe (also a redirector) that there's one more step involved that may break it (which would either be our fault, or a platform limitation, but if you notice a difference with a venv then do let us know).

    Is there some documentation of these specifics regarding Python API vs. CRT?

    Unfortunately not. Our documentation is a specification, so if we explain something in docs, we then have to work hard to preserve it, but the main guarantee Python makes is "provides access to your platform's C runtime". So if we specify what the platform's C runtime has to do, and then the platform changes it, now we're wrong. So we tend not to document a lot of this stuff. (And a lot of the documentation that's written is written by people who don't consider this, and so they do document things either incorrectly, too specifically, or just for a single OS without mentioning that they only meant it for a single OS.)

    But if you go searching for something like "msvcrt spawn inherit pipes" then you'll find more generic information about the runtime (though in this case I just found the equivalent bug to this one but for Go...). There doesn't seem to be a lot out there about this one, perhaps because nobody writes this kind of code and expects it to be cross-plat?

  17. schlamar commented on May 7, 2025

    @schlamar
    schlamar
    on May 7, 2025
    ContributorAuthor
    More actions

    You should be able to open_osfhandle on the other side to get a file descriptor back

    Yes, this works with spawn (see #133506 (comment)) and with subprocess.

  18. schlamar commented on May 7, 2025

    @schlamar
    schlamar
    on May 7, 2025
    ContributorAuthor
    More actions

    However, do be aware that if this goes via the py.exe redirector

    This seems to work in my case. Haven't tried a venv.

    Thanks for your thorough explanation!

  19. picnixz
    added
    3.13only security fixesonly security fixes
    3.14bugs and security fixesbugs and security fixes
    on May 7, 2025
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

    3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixesOS-windowstype-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 01402503 — 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.