Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 41 additions & 42 deletions Lib/multiprocessing/forkserver.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import atexit
import errno
import json
import os
import selectors
import signal
Expand Down Expand Up @@ -162,27 +163,20 @@ def ensure_running(self):
self._forkserver_alive_fd = None
self._forkserver_pid = None

# gh-144503: sys_argv is passed as real argv elements after the
# ``-c cmd`` rather than repr'd into main_kws so that a large
# parent sys.argv cannot push the single ``-c`` command string
# over the OS per-argument length limit (MAX_ARG_STRLEN on Linux).
# The child sees them as sys.argv[1:].
cmd = ('import sys; '
'from multiprocessing.forkserver import main; '
'main(%d, %d, %r, sys_argv=sys.argv[1:], **%r)')

main_kws = {}
sys_argv = None
cmd = ('from multiprocessing.forkserver import main; ' +
'main(listener_fd=%d, alive_r=%d, init_r=%d)')

if self._preload_modules:
data = spawn.get_preparation_data('ignore')
if 'sys_path' in data:
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if ... in data have been removed, because a cursory reading of get_preparation_data shows that the sys_path, sys_argv are always present in the returned dictionary.

The init_main_from_path is not always present, but the default argument in _handle_preload is None, so I think using that same value here is fine.

Similarly to on_error: this was checking if it was equal to the default value of _handle_preload, and if so, would not pass it. If the default value of _handle_preload would be changed, and this statement here would not be, that would cause an inconsistency. I think it's better to just always pass the value here to prevent future drift.

main_kws['sys_path'] = data['sys_path']
if 'init_main_from_path' in data:
main_kws['main_path'] = data['init_main_from_path']
if 'sys_argv' in data:
sys_argv = data['sys_argv']
if self._preload_on_error != 'ignore':
main_kws['on_error'] = self._preload_on_error
preload_kwargs = {
"preload": self._preload_modules,
"sys_path": data.get("sys_path"),
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This data.get("sys_path") can just be data["sys_path"]. I feel quite certain that there is no flow through get_preparation_data where the key "sys_path" is not in the returned dictionary.

"main_path": data.get("init_main_from_path"),
"sys_argv": data.get("sys_argv"),
"on_error": self._preload_on_error,
}
else:
preload_kwargs = None

with socket.socket(socket.AF_UNIX) as listener:
address = connection.arbitrary_address('AF_UNIX')
Expand All @@ -195,32 +189,34 @@ def ensure_running(self):
# when they all terminate the read end becomes ready.
alive_r, alive_w = os.pipe()
# A short lived pipe to initialize the forkserver authkey.
authkey_r, authkey_w = os.pipe()
init_r, init_w = os.pipe()
try:
fds_to_pass = [listener.fileno(), alive_r, authkey_r]
main_kws['authkey_r'] = authkey_r
cmd %= (listener.fileno(), alive_r, self._preload_modules,
main_kws)
fds_to_pass = [listener.fileno(), alive_r, init_r]
cmd %= (listener.fileno(), alive_r, init_r)
exe = spawn.get_executable()
args = [exe] + util._args_from_interpreter_flags()
args += ['-c', cmd]
if sys_argv is not None:
args += sys_argv
pid = util.spawnv_passfds(exe, args, fds_to_pass)
except:
os.close(alive_w)
os.close(authkey_w)
os.close(init_w)
raise
finally:
os.close(alive_r)
os.close(authkey_r)
os.close(init_r)
# Authenticate our control socket to prevent access from
# processes we have not shared this key with.
try:
self._forkserver_authkey = os.urandom(_AUTHKEY_LEN)
os.write(authkey_w, self._forkserver_authkey)
preload_data = json.dumps(preload_kwargs).encode()
# Use a buffered writer so that payloads larger than
# PIPE_BUF are written fully (os.write may short-write).
with os.fdopen(init_w, 'wb', closefd=False) as f:
f.write(self._forkserver_authkey)
f.write(struct.pack("Q", len(preload_data)))
f.write(preload_data)
finally:
os.close(authkey_w)
os.close(init_w)
self._forkserver_address = address
self._forkserver_alive_fd = alive_w
self._forkserver_pid = pid
Expand Down Expand Up @@ -290,19 +286,22 @@ def _handle_preload(preload, main_path=None, sys_path=None, sys_argv=None,
util._flush_std_streams()


def main(listener_fd, alive_r, preload, main_path=None, sys_path=None,
*, sys_argv=None, authkey_r=None, on_error='ignore'):
def main(listener_fd, alive_r, init_r):
"""Run forkserver."""
if authkey_r is not None:
try:
authkey = os.read(authkey_r, _AUTHKEY_LEN)
assert len(authkey) == _AUTHKEY_LEN, f'{len(authkey)} < {_AUTHKEY_LEN}'
finally:
os.close(authkey_r)
else:
authkey = b''

_handle_preload(preload, main_path, sys_path, sys_argv, on_error)
try:
# Buffered reader handles short reads on the length prefix and body.
with os.fdopen(init_r, 'rb', closefd=False) as f:
authkey = f.read(_AUTHKEY_LEN)
assert len(authkey) == _AUTHKEY_LEN, (
f'{len(authkey)} < {_AUTHKEY_LEN}')
preload_data_len, = struct.unpack("Q",
f.read(struct.calcsize("Q")))
preload_kwargs = json.loads(f.read(preload_data_len))
finally:
os.close(init_r)

if preload_kwargs:
_handle_preload(**preload_kwargs)

util._close_stdin()

Expand Down
5 changes: 5 additions & 0 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@
except ImportError:
msvcrt = None

try:
import resource
except ImportError:
resource = None


if support.HAVE_ASAN_FORK_BUG:
# gh-89363: Skip multiprocessing tests if Python is built with ASAN to
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :mod:`multiprocessing` ``forkserver`` bug which prevented starting of
the forkserver if the total length of command line arguments in ``sys.argv``
exceeded the maximum length of a single command line argument.
Loading