Skip to content
Draft
Show file tree
Hide file tree
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
27 changes: 27 additions & 0 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
Py_GIL_DISABLED,
no_rerun,
force_not_colorized_test_class,
catch_unraisable_exception
)
from test.support.import_helper import (
forget, make_legacy_pyc, unlink, unload, ready_to_import,
Expand Down Expand Up @@ -2540,6 +2541,32 @@ def test_disallowed_reimport(self):
excsnap = _interpreters.run_string(interpid, script)
self.assertIsNot(excsnap, None)

@requires_subinterpreters
def test_pyinit_function_raises_exception(self):
# gh-144601: PyInit functions that raised exceptions would cause a
# crash when imported from a subinterpreter.
import _testsinglephase
filename = _testsinglephase.__file__
script = f"""if True:
from test.test_import import import_extension_from_file

import_extension_from_file('_testsinglephase_raise_exception', {filename!r})"""

interp = _interpreters.create()
try:
with catch_unraisable_exception() as cm:
exception = _interpreters.run_string(interp, script)
unraisable = cm.unraisable
finally:
_interpreters.destroy(interp)

self.assertIsNotNone(exception)
self.assertIsNotNone(exception.type.__name__, "ImportError")
self.assertIsNotNone(exception.msg, "failed to import from subinterpreter due to exception")
self.assertIsNotNone(unraisable)
self.assertIs(unraisable.exc_type, RuntimeError)
self.assertEqual(str(unraisable.exc_value), "evil")


class TestSinglePhaseSnapshot(ModuleSnapshot):
"""A representation of a single-phase init module for testing.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix crash when importing a module whose ``PyInit`` function raises an
exception from a subinterpreter.
8 changes: 8 additions & 0 deletions Modules/_testsinglephase.c
Original file line number Diff line number Diff line change
Expand Up @@ -799,3 +799,11 @@ PyInit__testsinglephase_circular(void)
}
return Py_NewRef(static_module_circular);
}


PyMODINIT_FUNC
PyInit__testsinglephase_raise_exception(void)
{
PyErr_SetString(PyExc_RuntimeError, "evil");
return NULL;
}
17 changes: 16 additions & 1 deletion Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -2127,13 +2127,29 @@ import_run_extension(PyThreadState *tstate, PyModInitFunction p0,
}

main_finally:
if (rc < 0) {
_Py_ext_module_loader_result_apply_error(&res, name_buf);
}

/* Switch back to the subinterpreter. */
if (switched) {
// gh-144601: The exception object can't be transferred across
// interpreters. Instead, we print out an unraisable exception, and
// then raise a different exception for the calling interpreter.
if (rc < 0) {
assert(PyErr_Occurred());
PyErr_FormatUnraisable("Exception while importing from subinterpreter");
}
assert(main_tstate != tstate);
switch_back_from_main_interpreter(tstate, main_tstate, mod);
/* Any module we got from the init function will have to be
* reloaded in the subinterpreter. */
mod = NULL;
if (rc < 0) {
PyErr_SetString(PyExc_ImportError,
"failed to import from subinterpreter due to exception");
goto error;
}
}

/*****************************************************************/
Expand All @@ -2142,7 +2158,6 @@ import_run_extension(PyThreadState *tstate, PyModInitFunction p0,

/* Finally we handle the error return from _PyImport_RunModInitFunc(). */
if (rc < 0) {
_Py_ext_module_loader_result_apply_error(&res, name_buf);
goto error;
}

Expand Down
Loading