Skip to content

Commit 5391304

Browse files
Support disallowing os.exec*() in an interpreter.
1 parent 8f2178d commit 5391304

File tree

10 files changed

+59
-7
lines changed

10 files changed

+59
-7
lines changed

Include/cpython/initconfig.h

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

246246
typedef struct {
247247
int allow_fork;
248+
int allow_exec;
248249
int allow_threads;
249250
int allow_daemon_threads;
250251
} _PyInterpreterConfig;
251252

252253
#define _PyInterpreterConfig_LEGACY_INIT \
253254
{ \
254255
.allow_fork = 1, \
256+
.allow_exec = 1, \
255257
.allow_threads = 1, \
256258
.allow_daemon_threads = 1, \
257259
}

Include/cpython/pystate.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ might not be allowed in the current interpreter (i.e. os.fork() would fail).
2020
/* Set if os.fork() is allowed. */
2121
#define Py_RTFLAGS_FORK (1UL << 15)
2222

23+
/* Set if os.exec*() is allowed. */
24+
#define Py_RTFLAGS_EXEC (1UL << 16)
25+
2326

2427
PyAPI_FUNC(int) _PyInterpreterState_HasFeature(PyInterpreterState *interp,
2528
unsigned long feature);

Lib/test/test__xxsubinterpreters.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,22 @@ def f():
856856

857857
self.assertEqual(out, 'it worked!')
858858

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+
859875
@support.requires_fork()
860876
def test_fork(self):
861877
import tempfile

Lib/test/test_capi.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,13 +1096,14 @@ def test_configured_settings(self):
10961096
THREADS = 1<<10
10971097
DAEMON_THREADS = 1<<11
10981098
FORK = 1<<15
1099+
EXEC = 1<<16
10991100

1100-
features = ['fork', 'threads', 'daemon_threads']
1101+
features = ['fork', 'exec', 'threads', 'daemon_threads']
11011102
kwlist = [f'allow_{n}' for n in features]
11021103
for config, expected in {
1103-
(True, True, True): FORK | THREADS | DAEMON_THREADS,
1104-
(False, False, False): 0,
1105-
(False, True, False): THREADS,
1104+
(True, True, True, True): FORK | EXEC | THREADS | DAEMON_THREADS,
1105+
(False, False, False, False): 0,
1106+
(False, False, True, False): THREADS,
11061107
}.items():
11071108
kwargs = dict(zip(kwlist, config))
11081109
expected = {

Lib/test/test_embed.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1651,9 +1651,10 @@ def test_init_main_interpreter_settings(self):
16511651
THREADS = 1<<10
16521652
DAEMON_THREADS = 1<<11
16531653
FORK = 1<<15
1654+
EXEC = 1<<16
16541655
expected = {
16551656
# All optional features should be enabled.
1656-
'feature_flags': FORK | THREADS | DAEMON_THREADS,
1657+
'feature_flags': FORK | EXEC | THREADS | DAEMON_THREADS,
16571658
}
16581659
out, err = self.run_embedded_interpreter(
16591660
'test_init_main_interpreter_settings',

Lib/test/test_threading.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,7 @@ def func():
13241324
test.support.run_in_subinterp_with_config(
13251325
{subinterp_code!r},
13261326
allow_fork=True,
1327+
allow_exec=True,
13271328
allow_threads={allowed},
13281329
allow_daemon_threads={daemon_allowed},
13291330
)

Modules/_testcapimodule.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3231,6 +3231,7 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
32313231
{
32323232
const char *code;
32333233
int allow_fork = -1;
3234+
int allow_exec = -1;
32343235
int allow_threads = -1;
32353236
int allow_daemon_threads = -1;
32363237
int r;
@@ -3240,19 +3241,24 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
32403241

32413242
static char *kwlist[] = {"code",
32423243
"allow_fork",
3244+
"allow_exec",
32433245
"allow_threads",
32443246
"allow_daemon_threads",
32453247
NULL};
32463248
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
3247-
"s$ppp:run_in_subinterp_with_config", kwlist,
3248-
&code, &allow_fork,
3249+
"s$pppp:run_in_subinterp_with_config", kwlist,
3250+
&code, &allow_fork, &allow_exec,
32493251
&allow_threads, &allow_daemon_threads)) {
32503252
return NULL;
32513253
}
32523254
if (allow_fork < 0) {
32533255
PyErr_SetString(PyExc_ValueError, "missing allow_fork");
32543256
return NULL;
32553257
}
3258+
if (allow_exec < 0) {
3259+
PyErr_SetString(PyExc_ValueError, "missing allow_exec");
3260+
return NULL;
3261+
}
32563262
if (allow_threads < 0) {
32573263
PyErr_SetString(PyExc_ValueError, "missing allow_threads");
32583264
return NULL;
@@ -3268,6 +3274,7 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
32683274

32693275
const _PyInterpreterConfig config = {
32703276
.allow_fork = allow_fork,
3277+
.allow_exec = allow_exec,
32713278
.allow_threads = allow_threads,
32723279
.allow_daemon_threads = allow_daemon_threads,
32733280
};

Modules/_xxsubinterpretersmodule.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,6 +2005,7 @@ interp_create(PyObject *self, PyObject *args, PyObject *kwds)
20052005
PyThreadState *save_tstate = _PyThreadState_GET();
20062006
const _PyInterpreterConfig config = {
20072007
.allow_fork = !isolated,
2008+
.allow_exec = !isolated,
20082009
.allow_threads = !isolated,
20092010
.allow_daemon_threads = !isolated,
20102011
};

Modules/posixmodule.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5773,6 +5773,13 @@ os_execv_impl(PyObject *module, path_t *path, PyObject *argv)
57735773
EXECV_CHAR **argvlist;
57745774
Py_ssize_t argc;
57755775

5776+
PyInterpreterState *interp = _PyInterpreterState_GET();
5777+
if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_EXEC)) {
5778+
PyErr_SetString(PyExc_RuntimeError,
5779+
"exec not supported for isolated subinterpreters");
5780+
return NULL;
5781+
}
5782+
57765783
/* execv has two arguments: (path, argv), where
57775784
argv is a list or tuple of strings. */
57785785

@@ -5839,6 +5846,13 @@ os_execve_impl(PyObject *module, path_t *path, PyObject *argv, PyObject *env)
58395846
EXECV_CHAR **envlist;
58405847
Py_ssize_t argc, envc;
58415848

5849+
PyInterpreterState *interp = _PyInterpreterState_GET();
5850+
if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_EXEC)) {
5851+
PyErr_SetString(PyExc_RuntimeError,
5852+
"exec not supported for isolated subinterpreters");
5853+
return NULL;
5854+
}
5855+
58425856
/* execve has three arguments: (path, argv, env), where
58435857
argv is a list or tuple of strings and env is a dictionary
58445858
like posix.environ. */

Python/pylifecycle.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,9 +615,15 @@ static void
615615
init_interp_settings(PyInterpreterState *interp, const _PyInterpreterConfig *config)
616616
{
617617
assert(interp->feature_flags == 0);
618+
618619
if (config->allow_fork) {
619620
interp->feature_flags |= Py_RTFLAGS_FORK;
620621
}
622+
if (config->allow_exec) {
623+
interp->feature_flags |= Py_RTFLAGS_EXEC;
624+
}
625+
// Note that fork+exec is always allowed.
626+
621627
if (config->allow_threads) {
622628
interp->feature_flags |= Py_RTFLAGS_THREADS;
623629
}

0 commit comments

Comments
 (0)