Skip to content

multiprocessing.Pool fails if command has a long list of arguments (regression in 3.14.3, 3.13.12) #144503

Open

Description

@sjoerdjob

Bug report

Bug description:

A multiprocessing pool can fail to start when the command that starts the pool was ran with a lot of arguments.

Minimal working example:

# mwe.py
import multiprocessing
if __name__ == "__main__":
    with multiprocessing.Pool():
        pass

Run this using the following script:

#!/bin/bash

PAGE_SIZE=$(getconf PAGE_SIZE)
MAX_ARG_STRLEN=$((32 * PAGE_SIZE))
COUNT=$((MAX_ARG_STRLEN / 100 ))
echo $COUNT
yes "$(printf 'a%.0s' {1..100})" | head -n $COUNT | xargs python3 mwe.py

What this will do is effectively run the following:

python3 mwe.py aaa..a aaa..a aaa..a ... aaa.a

The Python process itself launches correctly, as each argument is only 100 bytes long (way under the maximum argument length of MAX_ARG_STRLEN).
The creation of the multiprocessing pool however fails, because the sys.argv variable is taken, and used to build a single string in multiprocessing.forkserver.ForkServer.ensure_running here:

if 'sys_argv' in data:
.

This is caused by a recent change: #143717, which was also backported to 3.13 and 3.14. (It works on 3.14.2, fails on 3.14.3).

CPython versions tested on:

3.15, 3.14

Operating systems tested on:

Linux

Linked PRs

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    3.13only security fixes
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Feb 5, 2026
  2. ZeroIntensity commented on Feb 5, 2026

    @ZeroIntensity
    Member
  3. added a commit that references this issue on Feb 5, 2026
  4. JaapJoris commented on Feb 5, 2026

    @JaapJoris

    Confirmed; when I run the minimal example under Python 3.14.3 I get this traceback:

    $ bash test.sh
    1310
    Traceback (most recent call last):
      File "/home/jj/mwe.py", line 4, in <module>
        with multiprocessing.Pool():
             ~~~~~~~~~~~~~~~~~~~~^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/context.py", line 119, in Pool
        return Pool(processes, initializer, initargs, maxtasksperchild,
                    context=self.get_context())
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/pool.py", line 215, in __init__
        self._repopulate_pool()
        ~~~~~~~~~~~~~~~~~~~~~^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/pool.py", line 306, in _repopulate_pool
        return self._repopulate_pool_static(self._ctx, self.Process,
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
                                            self._processes,
                                            ^^^^^^^^^^^^^^^^
        ...<3 lines>...
                                            self._maxtasksperchild,
                                            ^^^^^^^^^^^^^^^^^^^^^^^
                                            self._wrap_exception)
                                            ^^^^^^^^^^^^^^^^^^^^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/pool.py", line 329, in _repopulate_pool_static
        w.start()
        ~~~~~~~^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/process.py", line 121, in start
        self._popen = self._Popen(self)
                      ~~~~~~~~~~~^^^^^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/context.py", line 300, in _Popen
        return Popen(process_obj)
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/popen_forkserver.py", line 35, in __init__
        super().__init__(process_obj)
        ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/popen_fork.py", line 20, in __init__
        self._launch(process_obj)
        ~~~~~~~~~~~~^^^^^^^^^^^^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/popen_forkserver.py", line 51, in _launch
        self.sentinel, w = forkserver.connect_to_new_process(self._fds)
                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/forkserver.py", line 89, in connect_to_new_process
        self.ensure_running()
        ~~~~~~~~~~~~~~~~~~~^^
      File "/home/jj/.pyenv/versions/3.14.3/lib/python3.14/multiprocessing/forkserver.py", line 190, in ensure_running
        os.write(authkey_w, self._forkserver_authkey)
        ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    BrokenPipeError: [Errno 32] Broken pipe
    
  5. added 3 commits that reference this issue on Feb 5, 2026
  6. duaneg commented on Feb 8, 2026

    @duaneg
    Contributor

    This is tricky. We need to pass sys.argv data, but the current approach obviously won't work given the need to support large argvs. I think we need to rework how forkserver's main works, to read initialisation data from a pipe rather than providing it inline as part of the python command.

    @gpshead (and/or others) does that sound right?

  7. sjoerdjob commented on Feb 9, 2026

    @sjoerdjob
    Author

    Writing initialization data to a pipe instead of providing it inline does indeed sound like a better idea, but it is also a bit more complex idea.

    The maximum number of bytes to be written to a pipe before the other end reads from it is also limited; based on https://www.man7.org/linux/man-pages/man7/pipe.7.html, one can expect a default capacity of 16 pages, or 65356 bytes. That is less than the limit of 32 pages that we currently run into.

    As such, writing to and reading from the initialization pipe would have to have buffering applied, which is certainly feasible, but it does introduce more failure modes (e.g.: if the forkserver process won't launch, the writes will block, switching the symptom from 'exception' to 'crash').

    Also: what data format to use? Using pickles would make sense given the rest of the multiprocessing also works with pickles, however pickles bring with them security risks. To do that safely, gh-97514 introduced authentication keys. Using JSON instead would eliminate at least that (potential) safety concern. Likewise, using repr(some_dict) / ast.literal_eval(some_dict) would also work.

    Looking at the underlying assumptions of the authkey pipe, being that only the main process can write to it, and only the forkserver process can read from it, this should in itself be a secure channel, and any data format should be usable.

    The current system uses %r (effectively repr(...)), so the assumption definitely is that all items can by round-tripped correctly via repr and parsing. Effectively, however, all values are strings or lists of strings, and using json is I think more natural these days.

    I'll try and see if I can get it working by sending the initialization data over the pipe instead of via the CLI arguments.

  8. added a commit that references this issue on Feb 9, 2026
  9. 13 remaining items

  10. gpshead commented on Apr 7, 2026

    @gpshead
    Member

    I put up an alternate PR #148194 that does this in a simpler manner by just using actual command line arguments. If that works out - it feels safer for a bugfix backport. I wouldn't mind changing to doing it via a pipe in 3.15 though.

  11. added a commit that references this issue on Apr 7, 2026
  12. moved this to In Progress in Multiprocessing issueson Apr 7, 2026
  13. added 3 commits that reference this issue on Apr 7, 2026
  14. sjoerdjob commented on Apr 7, 2026

    @sjoerdjob
    Author

    The alternate fix in #148194 is also what I went with in my PR at first, but did switch to using a pipe to send data after considering the case in the description of the alternate PR:

    (well, almost, we consume two more arguments for the -c blah so the forkserver only works if there are slightly less than the maximum consumption of command line arguments the platform allows, 2MiB total a.k.a. 1/4 stack size for argv+envp space on Linux in most default configs... a far less common scenario than sys.argv repr'ing to >128KiB)

    I think that the edge-case triggered by the two extra arguments is very unlikely to be triggered, but it does still exist. For unblocking the 3.13 / 3.14 releases; the alternate fix is "good enough".

  15. self-assigned this
    on Apr 7, 2026
  16. added 2 commits that reference this issue on Apr 7, 2026
  17. gpshead commented on Apr 7, 2026

    @gpshead
    Member

    Okay the regression is Fixed with those merging, removing release blocker. I'll keep this open though to track the further improvement in how this is done for 3.15 in the other PR.

  18. added a commit that references this issue on Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3.13only security fixes3.14bugs and security fixes3.15bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytopic-multiprocessingtype-bugAn unexpected behavior, bug, or error

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions