Skip to content

Commit 3947910

Browse files
Support disallowing os.exec*() in an interpreter.
1 parent 16923eb commit 3947910

File tree

9 files changed

+61
-10
lines changed

9 files changed

+61
-10
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: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,13 +1095,16 @@ def test_configured_settings(self):
10951095
THREADS = 1<<10
10961096
DAEMON_THREADS = 1<<11
10971097
FORK = 1<<15
1098+
EXEC = 1<<16
10981099

1100+
features = ['fork', 'exec', 'threads', 'daemon_threads']
1101+
kwlist = [f'allow_{n}' for n in features]
10991102
for config, expected in {
1100-
(True, True, True): FORK | THREADS | DAEMON_THREADS,
1101-
(False, False, False): 0,
1102-
(False, True, False): THREADS,
1103+
(True, True, True, True): FORK | EXEC | THREADS | DAEMON_THREADS,
1104+
(False, True, False, False): 0,
1105+
(False, False, True, False): THREADS,
11031106
}.items():
1104-
allow_fork, allow_threads, allow_daemon_threads = config
1107+
kwargs = dict(zip(kwlist, config))
11051108
expected = {
11061109
'feature_flags': expected,
11071110
}
@@ -1115,9 +1118,7 @@ def test_configured_settings(self):
11151118
with os.fdopen({w}, "w") as stdin:
11161119
json.dump(settings, stdin)
11171120
'''),
1118-
allow_fork=allow_fork,
1119-
allow_threads=allow_threads,
1120-
allow_daemon_threads=allow_daemon_threads,
1121+
**kwargs
11211122
)
11221123
out = stdout.read()
11231124
settings = json.loads(out)

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',

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
_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
@@ -2006,6 +2006,7 @@ interp_create(PyObject *self, PyObject *args, PyObject *kwds)
20062006
PyThreadState *save_tstate = _PyThreadState_GET();
20072007
_PyInterpreterConfig config = {
20082008
.allow_fork = !isolated,
2009+
.allow_exec = !isolated,
20092010
.allow_threads = 1,
20102011
.allow_daemon_threads = !isolated,
20112012
};

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)