From ffe6a81ada82ab0d478024fc3521e0a8781fa38f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 30 Apr 2020 19:32:47 +0200 Subject: [PATCH 1/3] bpo-40453: Add PyConfig._isolated_subinterpreter An isolated subinterpreter cannot spawn threads, spawn a child process or call os.fork(). * Add private _Py_NewInterpreter(isolated_subinterpreter) function. * Add isolated=True keyword-only parameter to _xxsubinterpreters.create(). * Allow again os.fork() in "non-isolated" subinterpreters. --- Doc/c-api/init_config.rst | 2 ++ Include/cpython/initconfig.h | 4 ++++ Include/cpython/pylifecycle.h | 2 ++ Lib/test/test__xxsubinterpreters.py | 3 ++- .../2020-04-30-22-04-58.bpo-40453.ggz7sl.rst | 3 +++ Modules/_posixsubprocess.c | 8 ++++++++ Modules/_threadmodule.c | 8 ++++++++ Modules/_winapi.c | 7 +++++++ Modules/_xxsubinterpretersmodule.c | 14 +++++++++----- Modules/posixmodule.c | 7 ++++--- Python/initconfig.c | 3 +++ Python/pylifecycle.c | 13 ++++++++++--- 12 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-04-30-22-04-58.bpo-40453.ggz7sl.rst diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index 49507c8bad3ed7b..fc82c3eb5902482 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -1004,6 +1004,8 @@ Private provisional API: * :c:member:`PyConfig._init_main`: if set to 0, :c:func:`Py_InitializeFromConfig` stops at the "Core" initialization phase. +* :c:member:`PyConfig._isolated_interpreter`: if non-zero, + disallow threads, subprocesses and fork. .. c:function:: PyStatus _Py_InitializeMain(void) diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 8326c235702bd02..df93a5539d48bde 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -409,6 +409,10 @@ typedef struct { /* If equal to 0, stop Python initialization before the "main" phase */ int _init_main; + + /* If non-zero, disallow threads, subprocesses, and fork. + Default: 0. */ + int _isolated_interpreter; } PyConfig; PyAPI_FUNC(void) PyConfig_InitPythonConfig(PyConfig *config); diff --git a/Include/cpython/pylifecycle.h b/Include/cpython/pylifecycle.h index a01e9c94f12d75f..eb523b82e182d8c 100644 --- a/Include/cpython/pylifecycle.h +++ b/Include/cpython/pylifecycle.h @@ -65,6 +65,8 @@ PyAPI_FUNC(int) _Py_CoerceLegacyLocale(int warn); PyAPI_FUNC(int) _Py_LegacyLocaleDetected(int warn); PyAPI_FUNC(char *) _Py_SetLocaleFromEnv(int category); +PyAPI_FUNC(PyThreadState *) _Py_NewInterpreter(int isolated_subinterpreter); + #ifdef __cplusplus } #endif diff --git a/Lib/test/test__xxsubinterpreters.py b/Lib/test/test__xxsubinterpreters.py index 44f4d3fa0f4c95d..c6cf15494f9ed6a 100644 --- a/Lib/test/test__xxsubinterpreters.py +++ b/Lib/test/test__xxsubinterpreters.py @@ -814,6 +814,7 @@ def f(): self.assertEqual(out, 'it worked!') def test_create_thread(self): + subinterp = interpreters.create(isolated=False) script, file = _captured_script(""" import threading def f(): @@ -824,7 +825,7 @@ def f(): t.join() """) with file: - interpreters.run_string(self.id, script) + interpreters.run_string(subinterp, script) out = file.read() self.assertEqual(out, 'it worked!') diff --git a/Misc/NEWS.d/next/Library/2020-04-30-22-04-58.bpo-40453.ggz7sl.rst b/Misc/NEWS.d/next/Library/2020-04-30-22-04-58.bpo-40453.ggz7sl.rst new file mode 100644 index 000000000000000..f20c666d3e27f99 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-04-30-22-04-58.bpo-40453.ggz7sl.rst @@ -0,0 +1,3 @@ +Add ``isolated=True`` keyword-only parameter to +``_xxsubinterpreters.create()``. An isolated subinterpreter cannot spawn +threads, spawn a child process or call ``os.fork()``. diff --git a/Modules/_posixsubprocess.c b/Modules/_posixsubprocess.c index 60dd78d92a4f524..add2962189b1c88 100644 --- a/Modules/_posixsubprocess.c +++ b/Modules/_posixsubprocess.c @@ -663,6 +663,14 @@ subprocess_fork_exec(PyObject* self, PyObject *args) return NULL; } + PyInterpreterState *interp = PyInterpreterState_Get(); + const PyConfig *config = _PyInterpreterState_GetConfig(interp); + if (config->_isolated_interpreter) { + PyErr_SetString(PyExc_RuntimeError, + "subprocess not supported for isolated subinterpreters"); + return NULL; + } + /* We need to call gc.disable() when we'll be calling preexec_fn */ if (preexec_fn != Py_None) { PyObject *result; diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index b3d90b22c5a665b..77baba4847897b0 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1085,6 +1085,14 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs) "optional 3rd arg must be a dictionary"); return NULL; } + + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (interp->config._isolated_interpreter) { + PyErr_SetString(PyExc_RuntimeError, + "thread is not supported for isolated subinterpreters"); + return NULL; + } + boot = PyMem_NEW(struct bootstate, 1); if (boot == NULL) return PyErr_NoMemory(); diff --git a/Modules/_winapi.c b/Modules/_winapi.c index 1b28adb0b398380..e4a513c04c07cc6 100644 --- a/Modules/_winapi.c +++ b/Modules/_winapi.c @@ -1080,6 +1080,13 @@ _winapi_CreateProcess_impl(PyObject *module, return NULL; } + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (interp->config._isolated_interpreter) { + PyErr_SetString(PyExc_RuntimeError, + "subprocess not supported for isolated subinterpreters"); + return NULL; + } + ZeroMemory(&si, sizeof(si)); si.StartupInfo.cb = sizeof(si); diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index 15e80559ec6f60f..de11c090870f94a 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -1999,16 +1999,20 @@ _global_channels(void) { } static PyObject * -interp_create(PyObject *self, PyObject *args) +interp_create(PyObject *self, PyObject *args, PyObject *kwds) { - if (!PyArg_UnpackTuple(args, "create", 0, 0)) { + + static char *kwlist[] = {"isolated", NULL}; + int isolated = 1; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|$i:create", kwlist, + &isolated)) { return NULL; } // Create and initialize the new interpreter. PyThreadState *save_tstate = PyThreadState_Swap(NULL); // XXX Possible GILState issues? - PyThreadState *tstate = Py_NewInterpreter(); + PyThreadState *tstate = _Py_NewInterpreter(isolated); PyThreadState_Swap(save_tstate); if (tstate == NULL) { /* Since no new thread state was created, there is no exception to @@ -2547,8 +2551,8 @@ channel__channel_id(PyObject *self, PyObject *args, PyObject *kwds) } static PyMethodDef module_functions[] = { - {"create", (PyCFunction)interp_create, - METH_VARARGS, create_doc}, + {"create", (PyCFunction)(void(*)(void))interp_create, + METH_VARARGS | METH_KEYWORDS, create_doc}, {"destroy", (PyCFunction)(void(*)(void))interp_destroy, METH_VARARGS | METH_KEYWORDS, destroy_doc}, {"list_all", interp_list_all, diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 3d3f6ac969926ff..0163b0757aefa58 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -6243,9 +6243,10 @@ os_fork_impl(PyObject *module) /*[clinic end generated code: output=3626c81f98985d49 input=13c956413110eeaa]*/ { pid_t pid; - - if (_PyInterpreterState_GET() != PyInterpreterState_Main()) { - PyErr_SetString(PyExc_RuntimeError, "fork not supported for subinterpreters"); + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (interp->config._isolated_interpreter) { + PyErr_SetString(PyExc_RuntimeError, + "fork not supported for isolated subinterpreters"); return NULL; } if (PySys_Audit("os.fork", NULL) < 0) { diff --git a/Python/initconfig.c b/Python/initconfig.c index 58cca562f336dc2..185935c05fb286d 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -632,6 +632,7 @@ _PyConfig_InitCompatConfig(PyConfig *config) config->check_hash_pycs_mode = NULL; config->pathconfig_warnings = -1; config->_init_main = 1; + config->_isolated_interpreter = 0; #ifdef MS_WINDOWS config->legacy_windows_stdio = -1; #endif @@ -850,6 +851,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2) COPY_WSTR_ATTR(check_hash_pycs_mode); COPY_ATTR(pathconfig_warnings); COPY_ATTR(_init_main); + COPY_ATTR(_isolated_interpreter); #undef COPY_ATTR #undef COPY_WSTR_ATTR @@ -949,6 +951,7 @@ config_as_dict(const PyConfig *config) SET_ITEM_WSTR(check_hash_pycs_mode); SET_ITEM_INT(pathconfig_warnings); SET_ITEM_INT(_init_main); + SET_ITEM_INT(_isolated_interpreter); return dict; diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 7909cdbf5b77204..5726a559cfcb739 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -1526,7 +1526,7 @@ Py_Finalize(void) */ static PyStatus -new_interpreter(PyThreadState **tstate_p) +new_interpreter(PyThreadState **tstate_p, int isolated_subinterpreter) { PyStatus status; @@ -1573,6 +1573,7 @@ new_interpreter(PyThreadState **tstate_p) if (_PyStatus_EXCEPTION(status)) { goto error; } + interp->config._isolated_interpreter = isolated_subinterpreter; status = pycore_interp_init(tstate); if (_PyStatus_EXCEPTION(status)) { @@ -1606,10 +1607,10 @@ new_interpreter(PyThreadState **tstate_p) } PyThreadState * -Py_NewInterpreter(void) +_Py_NewInterpreter(int isolated_subinterpreter) { PyThreadState *tstate = NULL; - PyStatus status = new_interpreter(&tstate); + PyStatus status = new_interpreter(&tstate, isolated_subinterpreter); if (_PyStatus_EXCEPTION(status)) { Py_ExitStatusException(status); } @@ -1617,6 +1618,12 @@ Py_NewInterpreter(void) } +PyThreadState * +Py_NewInterpreter(void) +{ + return _Py_NewInterpreter(0); +} + /* Delete an interpreter and its last thread. This requires that the given thread state is current, that the thread has no remaining frames, and that it is its interpreter's only remaining thread. From 550fbf86551c29506f2ef434493a9f594fa0b6fa Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 30 Apr 2020 22:15:19 +0200 Subject: [PATCH 2/3] Fix compilation on Windows --- Modules/_winapi.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/_winapi.c b/Modules/_winapi.c index e4a513c04c07cc6..e1672c478522e87 100644 --- a/Modules/_winapi.c +++ b/Modules/_winapi.c @@ -1080,8 +1080,9 @@ _winapi_CreateProcess_impl(PyObject *module, return NULL; } - PyInterpreterState *interp = _PyInterpreterState_GET(); - if (interp->config._isolated_interpreter) { + PyInterpreterState *interp = PyInterpreterState_Get(); + const PyConfig *config = _PyInterpreterState_GetConfig(interp); + if (config->_isolated_interpreter) { PyErr_SetString(PyExc_RuntimeError, "subprocess not supported for isolated subinterpreters"); return NULL; From b6d231f94761e21a1d5d3336f85fe3cee7878ed1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 30 Apr 2020 23:07:22 +0200 Subject: [PATCH 3/3] Update test_embed --- Lib/test/test_embed.py | 3 +++ Programs/_testembed.c | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 0bdfae1b7e38730..3d60b2f330c628c 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -406,6 +406,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'check_hash_pycs_mode': 'default', 'pathconfig_warnings': 1, '_init_main': 1, + '_isolated_interpreter': 0, } if MS_WINDOWS: CONFIG_COMPAT.update({ @@ -766,6 +767,8 @@ def test_init_from_config(self): 'check_hash_pycs_mode': 'always', 'pathconfig_warnings': 0, + + '_isolated_interpreter': 1, } self.check_all_configs("test_init_from_config", config, preconfig, api=API_COMPAT) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 2cf0d71b470bf3a..5c83678f650d0bf 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -603,6 +603,8 @@ static int test_init_from_config(void) Py_FrozenFlag = 0; config.pathconfig_warnings = 0; + config._isolated_interpreter = 1; + init_from_config_clear(&config); dump_config();