Skip to content

Commit cd5726f

Browse files
authored
gh-91401: Add a failsafe way to disable vfork. (#91490)
Just in case there is ever an issue with _posixsubprocess's use of vfork() due to the complexity of using it properly and potential directions that Linux platforms where it defaults to on could take, this adds a failsafe so that users can disable its use entirely by setting a global flag. No known reason to disable it exists. But it'd be a shame to encounter one and not be able to use CPython without patching and rebuilding it. See the linked issue for some discussion on reasoning. Also documents the existing way to disable posix_spawn.
1 parent eddd07f commit cd5726f

File tree

7 files changed

+70
-10
lines changed

7 files changed

+70
-10
lines changed

Doc/library/subprocess.rst

+36
Original file line numberDiff line numberDiff line change
@@ -1529,3 +1529,39 @@ runtime):
15291529

15301530
:mod:`shlex`
15311531
Module which provides function to parse and escape command lines.
1532+
1533+
1534+
.. _disable_vfork:
1535+
.. _disable_posix_spawn:
1536+
1537+
Disabling use of ``vfork()`` or ``posix_spawn()``
1538+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1539+
1540+
On Linux, :mod:`subprocess` defaults to using the ``vfork()`` system call
1541+
internally when it is safe to do so rather than ``fork()``. This greatly
1542+
improves performance.
1543+
1544+
If you ever encounter a presumed highly-unusual situation where you need to
1545+
prevent ``vfork()`` from being used by Python, you can set the
1546+
:attr:`subprocess._USE_VFORK` attribute to a false value.
1547+
1548+
subprocess._USE_VFORK = False # See CPython issue gh-NNNNNN.
1549+
1550+
Setting this has no impact on use of ``posix_spawn()`` which could use
1551+
``vfork()`` internally within its libc implementation. There is a similar
1552+
:attr:`subprocess._USE_POSIX_SPAWN` attribute if you need to prevent use of
1553+
that.
1554+
1555+
subprocess._USE_POSIX_SPAWN = False # See CPython issue gh-NNNNNN.
1556+
1557+
It is safe to set these to false on any Python version. They will have no
1558+
effect on older versions when unsupported. Do not assume the attributes are
1559+
available to read. Despite their names, a true value does not indicate that the
1560+
corresponding function will be used, only that that it may be.
1561+
1562+
Please file issues any time you have to use these private knobs with a way to
1563+
reproduce the issue you were seeing. Link to that issue from a comment in your
1564+
code.
1565+
1566+
.. versionadded:: 3.8 ``_USE_POSIX_SPAWN``
1567+
.. versionadded:: 3.11 ``_USE_VFORK``

Lib/multiprocessing/util.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,15 @@ def _flush_std_streams():
446446

447447
def spawnv_passfds(path, args, passfds):
448448
import _posixsubprocess
449+
import subprocess
449450
passfds = tuple(sorted(map(int, passfds)))
450451
errpipe_read, errpipe_write = os.pipe()
451452
try:
452453
return _posixsubprocess.fork_exec(
453454
args, [path], True, passfds, None, None,
454455
-1, -1, -1, -1, -1, -1, errpipe_read, errpipe_write,
455-
False, False, None, None, None, -1, None)
456+
False, False, None, None, None, -1, None,
457+
subprocess._USE_VFORK)
456458
finally:
457459
os.close(errpipe_read)
458460
os.close(errpipe_write)

Lib/subprocess.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,10 @@ def _use_posix_spawn():
702702
return False
703703

704704

705+
# These are primarily fail-safe knobs for negatives. A True value does not
706+
# guarantee the given libc/syscall API will be used.
705707
_USE_POSIX_SPAWN = _use_posix_spawn()
708+
_USE_VFORK = True
706709

707710

708711
class Popen:
@@ -1792,7 +1795,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
17921795
errpipe_read, errpipe_write,
17931796
restore_signals, start_new_session,
17941797
gid, gids, uid, umask,
1795-
preexec_fn)
1798+
preexec_fn, _USE_VFORK)
17961799
self._child_created = True
17971800
finally:
17981801
# be sure the FD is closed no matter what

Lib/test/test_capi.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,15 @@ class Z(object):
140140
def __len__(self):
141141
return 1
142142
self.assertRaises(TypeError, _posixsubprocess.fork_exec,
143-
1,Z(),3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)
143+
1,Z(),3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)
144144
# Issue #15736: overflow in _PySequence_BytesToCharpArray()
145145
class Z(object):
146146
def __len__(self):
147147
return sys.maxsize
148148
def __getitem__(self, i):
149149
return b'x'
150150
self.assertRaises(MemoryError, _posixsubprocess.fork_exec,
151-
1,Z(),3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)
151+
1,Z(),3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)
152152

153153
@unittest.skipUnless(_posixsubprocess, '_posixsubprocess required for this test.')
154154
def test_subprocess_fork_exec(self):
@@ -158,7 +158,7 @@ def __len__(self):
158158

159159
# Issue #15738: crash in subprocess_fork_exec()
160160
self.assertRaises(TypeError, _posixsubprocess.fork_exec,
161-
Z(),[b'1'],3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)
161+
Z(),[b'1'],3,(1, 2),5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)
162162

163163
@unittest.skipIf(MISSING_C_DOCSTRINGS,
164164
"Signature information for builtins requires docstrings")

Lib/test/test_subprocess.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,22 @@ def test_class_getitems(self):
15431543
self.assertIsInstance(subprocess.Popen[bytes], types.GenericAlias)
15441544
self.assertIsInstance(subprocess.CompletedProcess[str], types.GenericAlias)
15451545

1546+
@unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
1547+
"vfork() not enabled by configure.")
1548+
@mock.patch("subprocess._fork_exec")
1549+
def test__use_vfork(self, mock_fork_exec):
1550+
self.assertTrue(subprocess._USE_VFORK) # The default value regardless.
1551+
mock_fork_exec.side_effect = RuntimeError("just testing args")
1552+
with self.assertRaises(RuntimeError):
1553+
subprocess.run([sys.executable, "-c", "pass"])
1554+
mock_fork_exec.assert_called_once()
1555+
self.assertTrue(mock_fork_exec.call_args.args[-1])
1556+
with mock.patch.object(subprocess, '_USE_VFORK', False):
1557+
with self.assertRaises(RuntimeError):
1558+
subprocess.run([sys.executable, "-c", "pass"])
1559+
self.assertFalse(mock_fork_exec.call_args_list[-1].args[-1])
1560+
1561+
15461562
class RunFuncTestCase(BaseTestCase):
15471563
def run_python(self, code, **kwargs):
15481564
"""Run Python code in a subprocess using subprocess.run"""
@@ -3107,7 +3123,7 @@ def test_fork_exec(self):
31073123
1, 2, 3, 4,
31083124
True, True,
31093125
False, [], 0, -1,
3110-
func)
3126+
func, False)
31113127
# Attempt to prevent
31123128
# "TypeError: fork_exec() takes exactly N arguments (M given)"
31133129
# from passing the test. More refactoring to have us start
@@ -3156,7 +3172,7 @@ def __int__(self):
31563172
1, 2, 3, 4,
31573173
True, True,
31583174
None, None, None, -1,
3159-
None)
3175+
None, "no vfork")
31603176
self.assertIn('fds_to_keep', str(c.exception))
31613177
finally:
31623178
if not gc_enabled:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Provide a way to disable :mod:`subprocess` use of ``vfork()`` just in case
2+
it is ever needed and document the existing mechanism for ``posix_spawn()``.

Modules/_posixsubprocess.c

+4-3
Original file line numberDiff line numberDiff line change
@@ -751,17 +751,18 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
751751
Py_ssize_t arg_num, num_groups = 0;
752752
int need_after_fork = 0;
753753
int saved_errno = 0;
754+
int allow_vfork;
754755

755756
if (!PyArg_ParseTuple(
756-
args, "OOpO!OOiiiiiiiiiiOOOiO:fork_exec",
757+
args, "OOpO!OOiiiiiiiiiiOOOiOp:fork_exec",
757758
&process_args, &executable_list,
758759
&close_fds, &PyTuple_Type, &py_fds_to_keep,
759760
&cwd_obj, &env_list,
760761
&p2cread, &p2cwrite, &c2pread, &c2pwrite,
761762
&errread, &errwrite, &errpipe_read, &errpipe_write,
762763
&restore_signals, &call_setsid,
763764
&gid_object, &groups_list, &uid_object, &child_umask,
764-
&preexec_fn))
765+
&preexec_fn, &allow_vfork))
765766
return NULL;
766767

767768
if ((preexec_fn != Py_None) &&
@@ -940,7 +941,7 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
940941
#ifdef VFORK_USABLE
941942
/* Use vfork() only if it's safe. See the comment above child_exec(). */
942943
sigset_t old_sigs;
943-
if (preexec_fn == Py_None &&
944+
if (preexec_fn == Py_None && allow_vfork &&
944945
!call_setuid && !call_setgid && !call_setgroups) {
945946
/* Block all signals to ensure that no signal handlers are run in the
946947
* child process while it shares memory with us. Note that signals

0 commit comments

Comments
 (0)