Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-100227: Make the Global PyModuleDef Cache Safe for Isolated Interpreters #103084

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions Include/internal/pycore_import.h
Original file line number Diff line number Diff line change
@@ -14,13 +14,19 @@ struct _import_runtime_state {
which is just about every time an extension module is imported.
See PyInterpreterState.modules_by_index for more info. */
Py_ssize_t last_module_index;
/* A dict mapping (filename, name) to PyModuleDef for modules.
Only legacy (single-phase init) extension modules are added
and only if they support multiple initialization (m_size >- 0)
or are imported in the main interpreter.
This is initialized lazily in _PyImport_FixupExtensionObject().
Modules are added there and looked up in _imp.find_extension(). */
PyObject *extensions;
struct {
/* A thread state tied to the main interpreter,
used exclusively for when the extensions dict is access/modified
from an arbitrary thread. */
PyThreadState main_tstate;
/* A dict mapping (filename, name) to PyModuleDef for modules.
Only legacy (single-phase init) extension modules are added
and only if they support multiple initialization (m_size >- 0)
or are imported in the main interpreter.
This is initialized lazily in _PyImport_FixupExtensionObject().
Modules are added there and looked up in _imp.find_extension(). */
PyObject *dict;
} extensions;
/* Package context -- the full module name for package imports */
const char * pkgcontext;
};
5 changes: 5 additions & 0 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
@@ -127,6 +127,11 @@ PyAPI_FUNC(void) _PyThreadState_Init(
PyThreadState *tstate);
PyAPI_FUNC(void) _PyThreadState_DeleteExcept(PyThreadState *tstate);

extern void _PyThreadState_InitDetached(PyThreadState *, PyInterpreterState *);
extern void _PyThreadState_ClearDetached(PyThreadState *);
extern void _PyThreadState_BindDetached(PyThreadState *);
extern void _PyThreadState_UnbindDetached(PyThreadState *);


static inline void
_PyThreadState_UpdateTracingState(PyThreadState *tstate)
5 changes: 5 additions & 0 deletions Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
@@ -41,6 +41,11 @@ extern PyTypeObject _PyExc_MemoryError;
in accordance with the specification. */ \
.autoTSSkey = Py_tss_NEEDS_INIT, \
.parser = _parser_runtime_state_INIT, \
.imports = { \
.extensions = { \
.main_tstate = _PyThreadState_INIT, \
}, \
}, \
.ceval = { \
.perf = _PyEval_RUNTIME_PERF_INIT, \
}, \
175 changes: 145 additions & 30 deletions Python/import.c
Original file line number Diff line number Diff line change
@@ -862,6 +862,18 @@ Generally, when multiple interpreters are involved, some of the above
gets even messier.
*/

static inline void
extensions_lock_acquire(void)
{
// XXX For now the GIL is sufficient.
}

static inline void
extensions_lock_release(void)
{
// XXX For now the GIL is sufficient.
}

/* Magic for extension modules (built-in as well as dynamically
loaded). To prevent initializing an extension module more than
once, we keep a static dictionary 'extensions' keyed by the tuple
@@ -878,71 +890,170 @@ gets even messier.
dictionary, to avoid loading shared libraries twice.
*/

static void
_extensions_cache_init(void)
{
/* The runtime (i.e. main interpreter) must be initializing,
so we don't need to worry about the lock. */
_PyThreadState_InitDetached(&EXTENSIONS.main_tstate,
_PyInterpreterState_Main());
}

static PyModuleDef *
_extensions_cache_get(PyObject *filename, PyObject *name)
{
PyObject *extensions = EXTENSIONS;
if (extensions == NULL) {
return NULL;
}
PyModuleDef *def = NULL;
extensions_lock_acquire();

PyObject *key = PyTuple_Pack(2, filename, name);
if (key == NULL) {
return NULL;
goto finally;
}

PyObject *extensions = EXTENSIONS.dict;
if (extensions == NULL) {
goto finally;
}
PyModuleDef *def = (PyModuleDef *)PyDict_GetItemWithError(extensions, key);
Py_DECREF(key);
def = (PyModuleDef *)PyDict_GetItemWithError(extensions, key);

finally:
Py_XDECREF(key);
extensions_lock_release();
return def;
}

static int
_extensions_cache_set(PyObject *filename, PyObject *name, PyModuleDef *def)
{
PyObject *extensions = EXTENSIONS;
int res = -1;
PyThreadState *oldts = NULL;
extensions_lock_acquire();

/* Swap to the main interpreter, if necessary. This matters if
the dict hasn't been created yet or if the item isn't in the
dict yet. In both cases we must ensure the relevant objects
are created using the main interpreter. */
PyThreadState *main_tstate = &EXTENSIONS.main_tstate;
PyInterpreterState *interp = _PyInterpreterState_GET();
if (!_Py_IsMainInterpreter(interp)) {
_PyThreadState_BindDetached(main_tstate);
oldts = _PyThreadState_Swap(interp->runtime, main_tstate);
assert(!_Py_IsMainInterpreter(oldts->interp));

/* Make sure the name and filename objects are owned
by the main interpreter. */
name = PyUnicode_InternFromString(PyUnicode_AsUTF8(name));
assert(name != NULL);
filename = PyUnicode_InternFromString(PyUnicode_AsUTF8(filename));
assert(filename != NULL);
}

PyObject *key = PyTuple_Pack(2, filename, name);
if (key == NULL) {
goto finally;
}

PyObject *extensions = EXTENSIONS.dict;
if (extensions == NULL) {
extensions = PyDict_New();
if (extensions == NULL) {
return -1;
goto finally;
}
EXTENSIONS = extensions;
EXTENSIONS.dict = extensions;
}
PyObject *key = PyTuple_Pack(2, filename, name);
if (key == NULL) {
return -1;

PyModuleDef *actual = (PyModuleDef *)PyDict_GetItemWithError(extensions, key);
if (PyErr_Occurred()) {
goto finally;
}
else if (actual != NULL) {
/* We expect it to be static, so it must be the same pointer. */
assert(def == actual);
res = 0;
goto finally;
}
int res = PyDict_SetItem(extensions, key, (PyObject *)def);
Py_DECREF(key);

/* This might trigger a resize, which is why we must switch
to the main interpreter. */
res = PyDict_SetItem(extensions, key, (PyObject *)def);
if (res < 0) {
return -1;
res = -1;
goto finally;
}
return 0;
res = 0;

finally:
if (oldts != NULL) {
_PyThreadState_Swap(interp->runtime, oldts);
_PyThreadState_UnbindDetached(main_tstate);
Py_DECREF(name);
Py_DECREF(filename);
}
Py_XDECREF(key);
extensions_lock_release();
return res;
}

static int
_extensions_cache_delete(PyObject *filename, PyObject *name)
{
PyObject *extensions = EXTENSIONS;
if (extensions == NULL) {
return 0;
}
int res = -1;
PyThreadState *oldts = NULL;
extensions_lock_acquire();

PyObject *key = PyTuple_Pack(2, filename, name);
if (key == NULL) {
return -1;
goto finally;
}

PyObject *extensions = EXTENSIONS.dict;
if (extensions == NULL) {
res = 0;
goto finally;
}

PyModuleDef *actual = (PyModuleDef *)PyDict_GetItemWithError(extensions, key);
if (PyErr_Occurred()) {
goto finally;
}
else if (actual == NULL) {
/* It was already removed or never added. */
res = 0;
goto finally;
}

/* Swap to the main interpreter, if necessary. */
PyThreadState *main_tstate = &EXTENSIONS.main_tstate;
PyInterpreterState *interp = _PyInterpreterState_GET();
if (!_Py_IsMainInterpreter(interp)) {
_PyThreadState_BindDetached(main_tstate);
oldts = _PyThreadState_Swap(interp->runtime, main_tstate);
assert(!_Py_IsMainInterpreter(oldts->interp));
}

if (PyDict_DelItem(extensions, key) < 0) {
if (!PyErr_ExceptionMatches(PyExc_KeyError)) {
Py_DECREF(key);
return -1;
}
PyErr_Clear();
goto finally;
}
Py_DECREF(key);
return 0;
res = 0;

finally:
if (oldts != NULL) {
_PyThreadState_Swap(interp->runtime, oldts);
_PyThreadState_UnbindDetached(main_tstate);
}
Py_XDECREF(key);
extensions_lock_release();
return res;
}

static void
_extensions_cache_clear_all(void)
{
Py_CLEAR(EXTENSIONS);
/* The runtime (i.e. main interpreter) must be finalizing,
so we don't need to worry about the lock. */
// XXX assert(_Py_IsMainInterpreter(_PyInterpreterState_GET()));
Py_CLEAR(EXTENSIONS.dict);
_PyThreadState_ClearDetached(&EXTENSIONS.main_tstate);
}


@@ -2941,6 +3052,10 @@ _PyImport_Fini2(void)
PyStatus
_PyImport_InitCore(PyThreadState *tstate, PyObject *sysmod, int importlib)
{
if (_Py_IsMainInterpreter(tstate->interp)) {
_extensions_cache_init();
}

// XXX Initialize here: interp->modules and interp->import_func.
// XXX Initialize here: sys.modules and sys.meta_path.

128 changes: 106 additions & 22 deletions Python/pystate.c
Original file line number Diff line number Diff line change
@@ -1217,8 +1217,7 @@ free_threadstate(PyThreadState *tstate)

static void
init_threadstate(PyThreadState *tstate,
PyInterpreterState *interp, uint64_t id,
PyThreadState *next)
PyInterpreterState *interp, uint64_t id)
{
if (tstate->_status.initialized) {
Py_FatalError("thread state already initialized");
@@ -1227,18 +1226,13 @@ init_threadstate(PyThreadState *tstate,
assert(interp != NULL);
tstate->interp = interp;

// next/prev are set in add_threadstate().
assert(tstate->next == NULL);
assert(tstate->prev == NULL);

assert(id > 0);
tstate->id = id;

assert(interp->threads.head == tstate);
assert((next != NULL && id != 1) || (next == NULL && id == 1));
if (next != NULL) {
assert(next->prev == NULL || next->prev == tstate);
next->prev = tstate;
}
tstate->next = next;
assert(tstate->prev == NULL);

// thread_id and native_thread_id are set in bind_tstate().

tstate->py_recursion_limit = interp->ceval.recursion_limit,
@@ -1259,6 +1253,22 @@ init_threadstate(PyThreadState *tstate,
tstate->_status.initialized = 1;
}

static void
add_threadstate(PyInterpreterState *interp, PyThreadState *tstate,
PyThreadState *next)
{
assert(interp->threads.head != tstate);
assert((next != NULL && tstate->id != 1) ||
(next == NULL && tstate->id == 1));
if (next != NULL) {
assert(next->prev == NULL || next->prev == tstate);
next->prev = tstate;
}
tstate->next = next;
assert(tstate->prev == NULL);
interp->threads.head = tstate;
}

static PyThreadState *
new_threadstate(PyInterpreterState *interp)
{
@@ -1298,9 +1308,9 @@ new_threadstate(PyInterpreterState *interp)
&initial._main_interpreter._initial_thread,
sizeof(*tstate));
}
interp->threads.head = tstate;

init_threadstate(tstate, interp, id, old_head);
init_threadstate(tstate, interp, id);
add_threadstate(interp, tstate, old_head);

HEAD_UNLOCK(runtime);
if (!used_newtstate) {
@@ -1347,6 +1357,19 @@ _PyThreadState_Init(PyThreadState *tstate)
Py_FatalError("_PyThreadState_Init() is for internal use only");
}


static void
clear_datastack(PyThreadState *tstate)
{
_PyStackChunk *chunk = tstate->datastack_chunk;
tstate->datastack_chunk = NULL;
while (chunk != NULL) {
_PyStackChunk *prev = chunk->previous;
_PyObject_VirtualFree(chunk, chunk->size);
chunk = prev;
}
}

void
PyThreadState_Clear(PyThreadState *tstate)
{
@@ -1421,7 +1444,6 @@ PyThreadState_Clear(PyThreadState *tstate)
// XXX Do it as early in the function as possible.
}


/* Common code for PyThreadState_Delete() and PyThreadState_DeleteCurrent() */
static void
tstate_delete_common(PyThreadState *tstate)
@@ -1454,18 +1476,11 @@ tstate_delete_common(PyThreadState *tstate)
unbind_tstate(tstate);

// XXX Move to PyThreadState_Clear()?
_PyStackChunk *chunk = tstate->datastack_chunk;
tstate->datastack_chunk = NULL;
while (chunk != NULL) {
_PyStackChunk *prev = chunk->previous;
_PyObject_VirtualFree(chunk, chunk->size);
chunk = prev;
}
clear_datastack(tstate);

tstate->_status.finalized = 1;
}


static void
zapthreads(PyInterpreterState *interp)
{
@@ -1552,6 +1567,75 @@ _PyThreadState_DeleteExcept(PyThreadState *tstate)
}


//-------------------------
// "detached" thread states
//-------------------------

void
_PyThreadState_InitDetached(PyThreadState *tstate, PyInterpreterState *interp)
{
_PyRuntimeState *runtime = interp->runtime;

HEAD_LOCK(runtime);
interp->threads.next_unique_id += 1;
uint64_t id = interp->threads.next_unique_id;
HEAD_UNLOCK(runtime);

init_threadstate(tstate, interp, id);
// We do not call add_threadstate().
}

void
_PyThreadState_ClearDetached(PyThreadState *tstate)
{
assert(!tstate->_status.bound);
assert(!tstate->_status.bound_gilstate);
assert(tstate->datastack_chunk == NULL);
assert(tstate->thread_id == 0);
assert(tstate->native_thread_id == 0);
assert(tstate->next == NULL);
assert(tstate->prev == NULL);

PyThreadState_Clear(tstate);
clear_datastack(tstate);
}

void
_PyThreadState_BindDetached(PyThreadState *tstate)
{
assert(!_Py_IsMainInterpreter(
current_fast_get(tstate->interp->runtime)->interp));
assert(_Py_IsMainInterpreter(tstate->interp));
bind_tstate(tstate);
/* Unlike _PyThreadState_Bind(), we do not modify gilstate TSS. */
}

void
_PyThreadState_UnbindDetached(PyThreadState *tstate)
{
assert(!_Py_IsMainInterpreter(
current_fast_get(tstate->interp->runtime)->interp));
assert(_Py_IsMainInterpreter(tstate->interp));
assert(tstate_is_alive(tstate));
assert(!tstate->_status.active);
assert(gilstate_tss_get(tstate->interp->runtime) != tstate);

unbind_tstate(tstate);

/* This thread state may be bound/unbound repeatedly,
so we must erase evidence that it was ever bound (or unbound). */
tstate->_status.bound = 0;
tstate->_status.unbound = 0;

/* We must fully unlink the thread state from any OS thread,
to allow it to be bound more than once. */
tstate->thread_id = 0;
#ifdef PY_HAVE_THREAD_NATIVE_ID
tstate->native_thread_id = 0;
#endif
}


//----------
// accessors
//----------