Skip to content

Commit 4702552

Browse files
gh-98610: Adjust the Optional Restrictions on Subinterpreters (GH-98618)
Previously, the optional restrictions on subinterpreters were: disallow fork, subprocess, and threads. By default, we were disallowing all three for "isolated" interpreters. We always allowed all three for the main interpreter and those created through the legacy `Py_NewInterpreter()` API. Those settings were a bit conservative, so here we've adjusted the optional restrictions to: fork, exec, threads, and daemon threads. The default for "isolated" interpreters disables fork, exec, and daemon threads. Regular threads are allowed by default. We continue always allowing everything For the main interpreter and the legacy API. In the code, we add `_PyInterpreterConfig.allow_exec` and `_PyInterpreterConfig.allow_daemon_threads`. We also add `Py_RTFLAGS_DAEMON_THREADS` and `Py_RTFLAGS_EXEC`.
1 parent 3b86538 commit 4702552

15 files changed

+220
-47
lines changed

Include/cpython/initconfig.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,15 +245,25 @@ PyAPI_FUNC(PyStatus) PyConfig_SetWideStringList(PyConfig *config,
245245

246246
typedef struct {
247247
int allow_fork;
248-
int allow_subprocess;
248+
int allow_exec;
249249
int allow_threads;
250+
int allow_daemon_threads;
250251
} _PyInterpreterConfig;
251252

253+
#define _PyInterpreterConfig_INIT \
254+
{ \
255+
.allow_fork = 0, \
256+
.allow_exec = 0, \
257+
.allow_threads = 1, \
258+
.allow_daemon_threads = 0, \
259+
}
260+
252261
#define _PyInterpreterConfig_LEGACY_INIT \
253262
{ \
254263
.allow_fork = 1, \
255-
.allow_subprocess = 1, \
264+
.allow_exec = 1, \
256265
.allow_threads = 1, \
266+
.allow_daemon_threads = 1, \
257267
}
258268

259269
/* --- Helper functions --------------------------------------- */

Include/cpython/pystate.h

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ is available in a given context. For example, forking the process
1111
might not be allowed in the current interpreter (i.e. os.fork() would fail).
1212
*/
1313

14-
// We leave the first 10 for less-specific features.
15-
1614
/* Set if threads are allowed. */
17-
#define Py_RTFLAGS_THREADS (1UL << 10)
15+
#define Py_RTFLAGS_THREADS (1UL << 10)
16+
17+
/* Set if daemon threads are allowed. */
18+
#define Py_RTFLAGS_DAEMON_THREADS (1UL << 11)
1819

1920
/* Set if os.fork() is allowed. */
20-
#define Py_RTFLAGS_FORK (1UL << 15)
21+
#define Py_RTFLAGS_FORK (1UL << 15)
2122

22-
/* Set if subprocesses are allowed. */
23-
#define Py_RTFLAGS_SUBPROCESS (1UL << 16)
23+
/* Set if os.exec*() is allowed. */
24+
#define Py_RTFLAGS_EXEC (1UL << 16)
2425

2526

2627
PyAPI_FUNC(int) _PyInterpreterState_HasFeature(PyInterpreterState *interp,

Lib/test/test__xxsubinterpreters.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ def f():
801801
self.assertEqual(out, 'it worked!')
802802

803803
def test_create_thread(self):
804-
subinterp = interpreters.create(isolated=False)
804+
subinterp = interpreters.create()
805805
script, file = _captured_script("""
806806
import threading
807807
def f():
@@ -817,6 +817,61 @@ def f():
817817

818818
self.assertEqual(out, 'it worked!')
819819

820+
def test_create_daemon_thread(self):
821+
with self.subTest('isolated'):
822+
expected = 'spam spam spam spam spam'
823+
subinterp = interpreters.create(isolated=True)
824+
script, file = _captured_script(f"""
825+
import threading
826+
def f():
827+
print('it worked!', end='')
828+
829+
try:
830+
t = threading.Thread(target=f, daemon=True)
831+
t.start()
832+
t.join()
833+
except RuntimeError:
834+
print('{expected}', end='')
835+
""")
836+
with file:
837+
interpreters.run_string(subinterp, script)
838+
out = file.read()
839+
840+
self.assertEqual(out, expected)
841+
842+
with self.subTest('not isolated'):
843+
subinterp = interpreters.create(isolated=False)
844+
script, file = _captured_script("""
845+
import threading
846+
def f():
847+
print('it worked!', end='')
848+
849+
t = threading.Thread(target=f, daemon=True)
850+
t.start()
851+
t.join()
852+
""")
853+
with file:
854+
interpreters.run_string(subinterp, script)
855+
out = file.read()
856+
857+
self.assertEqual(out, 'it worked!')
858+
859+
def test_os_exec(self):
860+
expected = 'spam spam spam spam spam'
861+
subinterp = interpreters.create()
862+
script, file = _captured_script(f"""
863+
import os, sys
864+
try:
865+
os.execl(sys.executable)
866+
except RuntimeError:
867+
print('{expected}', end='')
868+
""")
869+
with file:
870+
interpreters.run_string(subinterp, script)
871+
out = file.read()
872+
873+
self.assertEqual(out, expected)
874+
820875
@support.requires_fork()
821876
def test_fork(self):
822877
import tempfile

Lib/test/test_capi.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,15 +1148,16 @@ def test_configured_settings(self):
11481148
import json
11491149

11501150
THREADS = 1<<10
1151+
DAEMON_THREADS = 1<<11
11511152
FORK = 1<<15
1152-
SUBPROCESS = 1<<16
1153+
EXEC = 1<<16
11531154

1154-
features = ['fork', 'subprocess', 'threads']
1155+
features = ['fork', 'exec', 'threads', 'daemon_threads']
11551156
kwlist = [f'allow_{n}' for n in features]
11561157
for config, expected in {
1157-
(True, True, True): FORK | SUBPROCESS | THREADS,
1158-
(False, False, False): 0,
1159-
(False, True, True): SUBPROCESS | THREADS,
1158+
(True, True, True, True): FORK | EXEC | THREADS | DAEMON_THREADS,
1159+
(False, False, False, False): 0,
1160+
(False, False, True, False): THREADS,
11601161
}.items():
11611162
kwargs = dict(zip(kwlist, config))
11621163
expected = {

Lib/test/test_embed.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,11 +1655,12 @@ def test_init_use_frozen_modules(self):
16551655

16561656
def test_init_main_interpreter_settings(self):
16571657
THREADS = 1<<10
1658+
DAEMON_THREADS = 1<<11
16581659
FORK = 1<<15
1659-
SUBPROCESS = 1<<16
1660+
EXEC = 1<<16
16601661
expected = {
16611662
# All optional features should be enabled.
1662-
'feature_flags': THREADS | FORK | SUBPROCESS,
1663+
'feature_flags': FORK | EXEC | THREADS | DAEMON_THREADS,
16631664
}
16641665
out, err = self.run_embedded_interpreter(
16651666
'test_init_main_interpreter_settings',

Lib/test/test_threading.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,62 @@ def f():
13051305
self.assertIn("Fatal Python error: Py_EndInterpreter: "
13061306
"not the last thread", err.decode())
13071307

1308+
def _check_allowed(self, before_start='', *,
1309+
allowed=True,
1310+
daemon_allowed=True,
1311+
daemon=False,
1312+
):
1313+
subinterp_code = textwrap.dedent(f"""
1314+
import test.support
1315+
import threading
1316+
def func():
1317+
print('this should not have run!')
1318+
t = threading.Thread(target=func, daemon={daemon})
1319+
{before_start}
1320+
t.start()
1321+
""")
1322+
script = textwrap.dedent(f"""
1323+
import test.support
1324+
test.support.run_in_subinterp_with_config(
1325+
{subinterp_code!r},
1326+
allow_fork=True,
1327+
allow_exec=True,
1328+
allow_threads={allowed},
1329+
allow_daemon_threads={daemon_allowed},
1330+
)
1331+
""")
1332+
with test.support.SuppressCrashReport():
1333+
_, _, err = assert_python_ok("-c", script)
1334+
return err.decode()
1335+
1336+
@cpython_only
1337+
def test_threads_not_allowed(self):
1338+
err = self._check_allowed(
1339+
allowed=False,
1340+
daemon_allowed=False,
1341+
daemon=False,
1342+
)
1343+
self.assertIn('RuntimeError', err)
1344+
1345+
@cpython_only
1346+
def test_daemon_threads_not_allowed(self):
1347+
with self.subTest('via Thread()'):
1348+
err = self._check_allowed(
1349+
allowed=True,
1350+
daemon_allowed=False,
1351+
daemon=True,
1352+
)
1353+
self.assertIn('RuntimeError', err)
1354+
1355+
with self.subTest('via Thread.daemon setter'):
1356+
err = self._check_allowed(
1357+
't.daemon = True',
1358+
allowed=True,
1359+
daemon_allowed=False,
1360+
daemon=False,
1361+
)
1362+
self.assertIn('RuntimeError', err)
1363+
13081364

13091365
class ThreadingExceptionTests(BaseTestCase):
13101366
# A RuntimeError should be raised if Thread.start() is called

Lib/threading.py

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

3434
# Rename some stuff so "from threading import *" is safe
3535
_start_new_thread = _thread.start_new_thread
36+
_daemon_threads_allowed = _thread.daemon_threads_allowed
3637
_allocate_lock = _thread.allocate_lock
3738
_set_sentinel = _thread._set_sentinel
3839
get_ident = _thread.get_ident
@@ -899,6 +900,8 @@ class is implemented.
899900
self._args = args
900901
self._kwargs = kwargs
901902
if daemon is not None:
903+
if daemon and not _daemon_threads_allowed():
904+
raise RuntimeError('daemon threads are disabled in this (sub)interpreter')
902905
self._daemonic = daemon
903906
else:
904907
self._daemonic = current_thread().daemon
@@ -1226,6 +1229,8 @@ def daemon(self):
12261229
def daemon(self, daemonic):
12271230
if not self._initialized:
12281231
raise RuntimeError("Thread.__init__() not called")
1232+
if daemonic and not _daemon_threads_allowed():
1233+
raise RuntimeError('daemon threads are disabled in this interpreter')
12291234
if self._started.is_set():
12301235
raise RuntimeError("cannot set daemon status of active thread")
12311236
self._daemonic = daemonic
@@ -1432,7 +1437,8 @@ def __init__(self):
14321437
class _DummyThread(Thread):
14331438

14341439
def __init__(self):
1435-
Thread.__init__(self, name=_newname("Dummy-%d"), daemon=True)
1440+
Thread.__init__(self, name=_newname("Dummy-%d"),
1441+
daemon=_daemon_threads_allowed())
14361442

14371443
self._started.set()
14381444
self._set_ident()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Some configurable capabilities of sub-interpreters have changed.
2+
They always allow subprocesses (:mod:`subprocess`) now, whereas before
3+
subprocesses could be optionally disaallowed for a sub-interpreter.
4+
Instead :func:`os.exec` can now be disallowed.
5+
Disallowing daemon threads is now supported. Disallowing all threads
6+
is still allowed, but is never done by default.
7+
Note that the optional restrictions are only available through
8+
``_Py_NewInterpreterFromConfig()``, which isn't a public API.
9+
They do not affect the main interpreter, nor :c:func:`Py_NewInterpreter`.

Modules/_posixsubprocess.c

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -825,8 +825,8 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
825825
&preexec_fn, &allow_vfork))
826826
return NULL;
827827

828-
if ((preexec_fn != Py_None) &&
829-
(PyInterpreterState_Get() != PyInterpreterState_Main())) {
828+
PyInterpreterState *interp = PyInterpreterState_Get();
829+
if ((preexec_fn != Py_None) && (interp != PyInterpreterState_Main())) {
830830
PyErr_SetString(PyExc_RuntimeError,
831831
"preexec_fn not supported within subinterpreters");
832832
return NULL;
@@ -841,13 +841,6 @@ subprocess_fork_exec(PyObject *module, PyObject *args)
841841
return NULL;
842842
}
843843

844-
PyInterpreterState *interp = PyInterpreterState_Get();
845-
if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_SUBPROCESS)) {
846-
PyErr_SetString(PyExc_RuntimeError,
847-
"subprocess not supported for isolated subinterpreters");
848-
return NULL;
849-
}
850-
851844
/* We need to call gc.disable() when we'll be calling preexec_fn */
852845
if (preexec_fn != Py_None) {
853846
need_to_reenable_gc = PyGC_Disable();

Modules/_testcapimodule.c

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3231,42 +3231,52 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
32313231
{
32323232
const char *code;
32333233
int allow_fork = -1;
3234-
int allow_subprocess = -1;
3234+
int allow_exec = -1;
32353235
int allow_threads = -1;
3236+
int allow_daemon_threads = -1;
32363237
int r;
32373238
PyThreadState *substate, *mainstate;
32383239
/* only initialise 'cflags.cf_flags' to test backwards compatibility */
32393240
PyCompilerFlags cflags = {0};
32403241

32413242
static char *kwlist[] = {"code",
3242-
"allow_fork", "allow_subprocess", "allow_threads",
3243+
"allow_fork",
3244+
"allow_exec",
3245+
"allow_threads",
3246+
"allow_daemon_threads",
32433247
NULL};
32443248
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
3245-
"s$ppp:run_in_subinterp_with_config", kwlist,
3246-
&code, &allow_fork, &allow_subprocess, &allow_threads)) {
3249+
"s$pppp:run_in_subinterp_with_config", kwlist,
3250+
&code, &allow_fork, &allow_exec,
3251+
&allow_threads, &allow_daemon_threads)) {
32473252
return NULL;
32483253
}
32493254
if (allow_fork < 0) {
32503255
PyErr_SetString(PyExc_ValueError, "missing allow_fork");
32513256
return NULL;
32523257
}
3253-
if (allow_subprocess < 0) {
3254-
PyErr_SetString(PyExc_ValueError, "missing allow_subprocess");
3258+
if (allow_exec < 0) {
3259+
PyErr_SetString(PyExc_ValueError, "missing allow_exec");
32553260
return NULL;
32563261
}
32573262
if (allow_threads < 0) {
32583263
PyErr_SetString(PyExc_ValueError, "missing allow_threads");
32593264
return NULL;
32603265
}
3266+
if (allow_daemon_threads < 0) {
3267+
PyErr_SetString(PyExc_ValueError, "missing allow_daemon_threads");
3268+
return NULL;
3269+
}
32613270

32623271
mainstate = PyThreadState_Get();
32633272

32643273
PyThreadState_Swap(NULL);
32653274

32663275
const _PyInterpreterConfig config = {
32673276
.allow_fork = allow_fork,
3268-
.allow_subprocess = allow_subprocess,
3277+
.allow_exec = allow_exec,
32693278
.allow_threads = allow_threads,
3279+
.allow_daemon_threads = allow_daemon_threads,
32703280
};
32713281
substate = _Py_NewInterpreterFromConfig(&config);
32723282
if (substate == NULL) {

Modules/_threadmodule.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,24 @@ thread_run(void *boot_raw)
11021102
// to open the libgcc_s.so library (ex: EMFILE error).
11031103
}
11041104

1105+
static PyObject *
1106+
thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored))
1107+
{
1108+
PyInterpreterState *interp = _PyInterpreterState_Get();
1109+
if (interp->feature_flags & Py_RTFLAGS_DAEMON_THREADS) {
1110+
Py_RETURN_TRUE;
1111+
}
1112+
else {
1113+
Py_RETURN_FALSE;
1114+
}
1115+
}
1116+
1117+
PyDoc_STRVAR(daemon_threads_allowed_doc,
1118+
"daemon_threads_allowed()\n\
1119+
\n\
1120+
Return True if daemon threads are allowed in the current interpreter,\n\
1121+
and False otherwise.\n");
1122+
11051123
static PyObject *
11061124
thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
11071125
{
@@ -1543,6 +1561,8 @@ static PyMethodDef thread_methods[] = {
15431561
METH_VARARGS, start_new_doc},
15441562
{"start_new", (PyCFunction)thread_PyThread_start_new_thread,
15451563
METH_VARARGS, start_new_doc},
1564+
{"daemon_threads_allowed", (PyCFunction)thread_daemon_threads_allowed,
1565+
METH_NOARGS, daemon_threads_allowed_doc},
15461566
{"allocate_lock", thread_PyThread_allocate_lock,
15471567
METH_NOARGS, allocate_doc},
15481568
{"allocate", thread_PyThread_allocate_lock,

0 commit comments

Comments
 (0)