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
8 changes: 7 additions & 1 deletion Lib/multiprocessing/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from . import process
from . import reduction
from . import util

__all__ = ()

Expand Down Expand Up @@ -333,7 +334,12 @@ def _check_available(self):
# bpo-33725: running arbitrary code after fork() is no longer reliable
# on macOS since macOS 10.14 (Mojave). Use spawn by default instead.
# gh-84559: We changed everyones default to a thread safeish one in 3.14.
if reduction.HAVE_SEND_HANDLE and sys.platform != 'darwin':
if (
reduction.HAVE_SEND_HANDLE
and sys.platform != 'darwin'
# gh-155717: forkserver requires to write temporary files
and util._has_writeable_tempdir()
):
_default_context = DefaultContext(_concrete_contexts['forkserver'])
else:
_default_context = DefaultContext(_concrete_contexts['spawn'])
Expand Down
31 changes: 28 additions & 3 deletions Lib/multiprocessing/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import itertools
import sys
import tempfile
import weakref
import atexit
import threading # we want threading to install it's
Expand Down Expand Up @@ -143,6 +144,7 @@ def is_abstract_socket_namespace(address):
# On Windows platforms, we do not create AF_UNIX sockets.
_SUN_PATH_MAX = None if os.name == 'nt' else 92


def _remove_temp_dir(rmtree, tempdir):
rmtree(tempdir)

Expand All @@ -152,7 +154,8 @@ def _remove_temp_dir(rmtree, tempdir):
if current_process is not None:
current_process._config['tempdir'] = None

def _get_base_temp_dir(tempfile):

def _get_base_temp_dir():
"""Get a temporary directory where socket files will be created.

To prevent additional imports, pass a pre-imported 'tempfile' module.
Expand Down Expand Up @@ -208,12 +211,13 @@ def _get_base_temp_dir(tempfile):
assert len(base_system_tempdir) + 14 + 14 < _SUN_PATH_MAX
return base_system_tempdir


def get_temp_dir():
# get name of a temp directory which will be automatically cleaned up
tempdir = process.current_process()._config.get('tempdir')
if tempdir is None:
import shutil, tempfile
base_tempdir = _get_base_temp_dir(tempfile)
import shutil
base_tempdir = _get_base_temp_dir()
tempdir = tempfile.mkdtemp(prefix='pymp-', dir=base_tempdir)
info('created temp directory %s', tempdir)
# keep a strong reference to shutil.rmtree(), since the finalizer
Expand All @@ -223,6 +227,27 @@ def get_temp_dir():
process.current_process()._config['tempdir'] = tempdir
return tempdir


def _has_writeable_tempdir():
# 'forkserver' requires writeable temporary files. This function must
# is called for defining the default context's start method.
#
# See: https://github.com/python/cpython/issues/155717.

path = _get_base_temp_dir()
if path is None:
return False

# os.access() is advisory and racy. It can lie on read-only filesystems,
# NFS/network mounts, containers, and immutable-flag files, so we simply
# try to create a file to check if this works and delete it otherwise.
try:
with tempfile.NamedTemporaryFile(dir=path):
return True
except OSError:
return False


#
# Support for reinitialization of objects when bootstrapping a child process
#
Expand Down
35 changes: 35 additions & 0 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import struct
import tempfile
import operator
import pathlib
import pickle
import weakref
import warnings
Expand Down Expand Up @@ -6355,6 +6356,40 @@ def test_nested_startmethod(self):
# there is no synchronization in the test.
self.assertSetEqual(set(results), set([2, 1]))

@unittest.skipIf(os.name == "nt", "requires POSIX")
@support.subTests("mode", [
os.R_OK, # read-only directory
os.R_OK | os.X_OK, # read-only directory
os.W_OK # write-only directory _without_ permissions for creating files
])
Comment thread
picnixz marked this conversation as resolved.
def test_forkserver_requires_writeable_tempdir(self, mode):
# Regression test to ensure that the defualt start method is
# not 'forkserver' when the temporary directory is not writeable.
#
# See https://github.com/python/cpython/issues/155717.

cmd = '''if 1:
import os, tempfile
# We fake the read-onlyiness of /tmp (which is a fallback when
# the user-defined TMPDIR is not acceptable) by hardcoding the
# temporary directory for this specific test.
tempfile.tempdir = os.environ["TMPDIR"]

# Imported after patching 'tempfile' so that the default start
# method is deduced according to the permissions of TMPDIR.
import multiprocessing
if __name__ == "__main__":
print(multiprocessing.get_start_method())
'''

with support.os_helper.temp_dir() as root:
TMPDIR = pathlib.Path(root, "TMPDIR")
TMPDIR.mkdir(mode=mode)
file = pathlib.Path(TMPDIR, "file")
self.assertRaises(OSError, file.touch)
_, out, err = script_helper.assert_python_ok('-c', cmd, TMPDIR=TMPDIR)
self.assertEqual(out.decode().strip(), "spawn")


@unittest.skipIf(sys.platform == "win32",
"test semantics don't make sense on Windows")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:mod:`multiprocessing`'s default start method on systems with non-writeable
tempfile filesystem is now :ref:`"spawn" <multiprocessing-start-methods>`
instead of ``"forkserver"``. Patch by Bénédikt Tran.
Loading