Repository navigation
Open
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-multiprocessingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
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:
- 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.
- 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 usewaitpidbut waits for EOF on the sentinel pipe, which the background process keeps open. - Under both
spawnandforkserver, 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 likepython3 example.py | tee.
- Under
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
-
To be precise, for
os.system,os.spawn*or explicit (fork+)exec type of calls, andsubprocess.*(close_fds=False). ↩
Reacted by Massimiliano Culpo
Metadata
Metadata
Assignees
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-multiprocessingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
haampie commentedon Nov 5, 2026
@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.
divya-git03 commentedon Nov 5, 2026
Hi! I'd love to contribute to this issue. I was looking into the file descriptor inheritance behaviour in the
spawnandforkserverstart 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?