Skip to content

Commit 13a9425

Browse files
committed
gh-155717: use spawn as the default start method for read-only filesystems
1 parent c92e2fd commit 13a9425

4 files changed

Lines changed: 73 additions & 4 deletions

File tree

Lib/multiprocessing/context.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from . import process
66
from . import reduction
7+
from . import util
78

89
__all__ = ()
910

@@ -333,7 +334,12 @@ def _check_available(self):
333334
# bpo-33725: running arbitrary code after fork() is no longer reliable
334335
# on macOS since macOS 10.14 (Mojave). Use spawn by default instead.
335336
# gh-84559: We changed everyones default to a thread safeish one in 3.14.
336-
if reduction.HAVE_SEND_HANDLE and sys.platform != 'darwin':
337+
if (
338+
reduction.HAVE_SEND_HANDLE
339+
and sys.platform != 'darwin'
340+
# gh-155717: forkserver requires to write temporary files
341+
and util._has_writeable_tempdir()
342+
):
337343
_default_context = DefaultContext(_concrete_contexts['forkserver'])
338344
else:
339345
_default_context = DefaultContext(_concrete_contexts['spawn'])

Lib/multiprocessing/util.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import os
1111
import itertools
1212
import sys
13+
import tempfile
1314
import weakref
1415
import atexit
1516
import threading # we want threading to install it's
@@ -143,6 +144,7 @@ def is_abstract_socket_namespace(address):
143144
# On Windows platforms, we do not create AF_UNIX sockets.
144145
_SUN_PATH_MAX = None if os.name == 'nt' else 92
145146

147+
146148
def _remove_temp_dir(rmtree, tempdir):
147149
rmtree(tempdir)
148150

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

155-
def _get_base_temp_dir(tempfile):
157+
158+
def _get_base_temp_dir():
156159
"""Get a temporary directory where socket files will be created.
157160
158161
To prevent additional imports, pass a pre-imported 'tempfile' module.
@@ -208,12 +211,13 @@ def _get_base_temp_dir(tempfile):
208211
assert len(base_system_tempdir) + 14 + 14 < _SUN_PATH_MAX
209212
return base_system_tempdir
210213

214+
211215
def get_temp_dir():
212216
# get name of a temp directory which will be automatically cleaned up
213217
tempdir = process.current_process()._config.get('tempdir')
214218
if tempdir is None:
215-
import shutil, tempfile
216-
base_tempdir = _get_base_temp_dir(tempfile)
219+
import shutil
220+
base_tempdir = _get_base_temp_dir()
217221
tempdir = tempfile.mkdtemp(prefix='pymp-', dir=base_tempdir)
218222
info('created temp directory %s', tempdir)
219223
# keep a strong reference to shutil.rmtree(), since the finalizer
@@ -223,6 +227,27 @@ def get_temp_dir():
223227
process.current_process()._config['tempdir'] = tempdir
224228
return tempdir
225229

230+
231+
def _has_writeable_tempdir():
232+
# 'forkserver' requires writeable temporary files. This function must
233+
# is called for defining the default context's start method.
234+
#
235+
# See: https://github.com/python/cpython/issues/155717.
236+
237+
path = _get_base_temp_dir()
238+
if path is None:
239+
return False
240+
241+
# os.access() is advisory and racy. It can lie on read-only filesystems,
242+
# NFS/network mounts, containers, and immutable-flag files, so we simply
243+
# try to create a file to check if this works and delete it otherwise.
244+
try:
245+
with tempfile.NamedTemporaryFile(dir=path):
246+
return True
247+
except OSError:
248+
return False
249+
250+
226251
#
227252
# Support for reinitialization of objects when bootstrapping a child process
228253
#

Lib/test/_test_multiprocessing.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import struct
2727
import tempfile
2828
import operator
29+
import pathlib
2930
import pickle
3031
import weakref
3132
import warnings
@@ -6355,6 +6356,40 @@ def test_nested_startmethod(self):
63556356
# there is no synchronization in the test.
63566357
self.assertSetEqual(set(results), set([2, 1]))
63576358

6359+
@support.subTests("mode", [
6360+
os.R_OK, # read-only directory
6361+
os.R_OK | os.X_OK, # read-only directory
6362+
os.W_OK # write-only directory _without_ permissions for creating files
6363+
])
6364+
def test_forkserver_requires_writeable_tempdir(self, mode):
6365+
# Regression test to ensure that the defualt start method is
6366+
# not 'forkserver' when the temporary directory is not writeable.
6367+
#
6368+
# See https://github.com/python/cpython/issues/155717.
6369+
6370+
cmd = '''if 1:
6371+
import os, tempfile
6372+
# We fake the read-onlyiness of /tmp (which is a fallback when
6373+
# the user-defined TMPDIR is not acceptable) by hardcoding the
6374+
# temporary directory for this specific test.
6375+
tempfile.tempdir = os.environ["TMPDIR"]
6376+
6377+
# Imported after patching 'tempfile' so that the default start
6378+
# method is deduced according to the permissions of TMPDIR.
6379+
import multiprocessing
6380+
if __name__ == "__main__":
6381+
print(multiprocessing.get_start_method())
6382+
'''
6383+
6384+
with support.os_helper.temp_dir() as root:
6385+
# read-only directory
6386+
TMPDIR = pathlib.Path(root, "TMPDIR")
6387+
TMPDIR.mkdir(mode=mode)
6388+
file = pathlib.Path(TMPDIR, "file")
6389+
self.assertRaises(OSError, file.touch)
6390+
_, out, err = script_helper.assert_python_ok('-c', cmd, TMPDIR=TMPDIR)
6391+
self.assertEqual(out.decode().strip(), "spawn")
6392+
63586393

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

0 commit comments

Comments
 (0)