Skip to content

multiprocessing: spawn and forkserver leak inheritable fds to further sub-processes #161408

Open

Description

@haampie

Bug report

When using multiprocessing with spawn or forkserver start methods, multiprocessing passes file descriptors to the child that are marked inheritable (including the sentinel pipe, the resource tracker and forkserver pipes, and the fds of pickled Connections and Queues). They leak across exec1 into further sub-processes of the child process, which is problematic for two reasons:

  1. It is inconsistent with PEP 446. These pipes and sockets are meaningful between the parent and the child, but not to further sub-processes of the child. Exposing them is bad practice for all the reasons detailed in PEP 446.
  2. It can cause hangs. If the child leaves a background sub-process behind, that process keeps the fds open, and as a result:
    • Under spawn, p.join(timeout=...) blocks: it doesn't use waitpid but waits for EOF on the sentinel pipe, which the background process keeps open.
    • Under both spawn and forkserver, the resource tracker outlives the main process: it waits for EOF on its pipe, which the background process keeps open, and meanwhile keeps the main process's stdout and stderr open. This blocks pipes like python3 example.py | tee.

A simple reproducer for the hanging issue is this:

import multiprocessing, os, sys, time

def child():
    # Start a background process (note the &) with stdio detached
    os.system("sleep 5 < /dev/null > /dev/null 2>&1 &")

if __name__ == "__main__":
    method = sys.argv[1]
    multiprocessing.set_start_method(method)
    p = multiprocessing.Process(target=child)
    p.start()
    start = time.monotonic()
    p.join(timeout=60)
    print(f"{method=}\njoin: {time.monotonic() - start:.1f}s")

Which can be run like so:

$ for method in fork spawn forkserver; do time sh -c "python3 example.py $method | tee"; done
method='fork'
join: 0.0s
real 0m0.036s

method='spawn'
join: 5.0s      # join blocks
real 0m5.072s

method='forkserver'
join: 0.0s      # join does not block, fine
real 0m5.088s   # but the resource tracker keeps stdout hostage, so tee is blocked

The fix would be to call os.set_inheritable(fd, False) on these fds in the child.

CPython versions tested on:

CPython main branch, 3.14

Operating systems tested on:

Linux, macOS

Footnotes

  1. To be precise, for os.system, os.spawn* or explicit (fork+)exec type of calls, and subprocess.*(close_fds=False). ↩

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Nov 5, 2026
  2. haampie commented on Nov 5, 2026

    @haampie
    ContributorAuthor

    @gpshead this is very similar to your work from #146316, hopefully that's still sufficiently fresh ;) happy to submit a PR myself, I believe it should be about ~10LOC.

    We're hitting this in the Spack package manager where we don't want to leak these fd's to build subprocesses.

  3. divya-git03 commented on Nov 5, 2026

    @divya-git03

    Hi! I'd love to contribute to this issue. I was looking into the file descriptor inheritance behaviour in the spawn and forkserver start methods and would be interested in working on the regression tests to ensure this behaviour is correctly handled. Are you already planning to cover the test cases as part of your PR, or would a separate test-focused contribution be helpful?

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

    stdlibStandard Library Python modules in the Lib/ directorytopic-multiprocessingtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions