https://github.com/python/cpython/commit/c1447994a42004a81a8caca026218daf69d451c8
commit: c1447994a42004a81a8caca026218daf69d451c8
branch: main
author: Bénédikt Tran <[email protected]>
committer: picnixz <[email protected]>
date: 2026-08-16T10:23:02+02:00
summary:

gh-155717: use `spawn` as the default start method for read-only filesystems 
(#155827)

The "forkserver" start method (the default start method on non-Windows systems)
requires the ability to write temporary files, which is not possible if TMPDIR
is read-only (e.g., k8s containers mounted with `readOnlyRootFilesystem=True`). 

On such filesystems, the default start method changes from "forkserver" to 
"spawn".

files:
A Misc/NEWS.d/next/Library/2026-08-15-09-47-23.gh-issue-155717.jWFLR2.rst
M Lib/multiprocessing/context.py
M Lib/multiprocessing/util.py
M Lib/test/_test_multiprocessing.py

diff --git a/Lib/multiprocessing/context.py b/Lib/multiprocessing/context.py
index 45c393798deaca2..e94e6c8690bf506 100644
--- a/Lib/multiprocessing/context.py
+++ b/Lib/multiprocessing/context.py
@@ -4,6 +4,7 @@
 
 from . import process
 from . import reduction
+from . import util
 
 __all__ = ()
 
@@ -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'])
diff --git a/Lib/multiprocessing/util.py b/Lib/multiprocessing/util.py
index 549fb07c27549e0..cf7e0b2990598b7 100644
--- a/Lib/multiprocessing/util.py
+++ b/Lib/multiprocessing/util.py
@@ -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
@@ -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)
 
@@ -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.
@@ -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
@@ -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 is
+    # called to determine 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
 #
diff --git a/Lib/test/_test_multiprocessing.py 
b/Lib/test/_test_multiprocessing.py
index ba1c0de5d283323..4aaaa22f4274f03 100644
--- a/Lib/test/_test_multiprocessing.py
+++ b/Lib/test/_test_multiprocessing.py
@@ -26,6 +26,7 @@
 import struct
 import tempfile
 import operator
+import pathlib
 import pickle
 import weakref
 import warnings
@@ -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
+    ])
+    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")
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-15-09-47-23.gh-issue-155717.jWFLR2.rst 
b/Misc/NEWS.d/next/Library/2026-08-15-09-47-23.gh-issue-155717.jWFLR2.rst
new file mode 100644
index 000000000000000..0994dc8d03051f4
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-15-09-47-23.gh-issue-155717.jWFLR2.rst
@@ -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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to