Skip to content

Commit 0b5852c

Browse files
committed
Fix crash when a destructor uses lazy imports during shutdown
Destructors can run after finalization has cleared the interpreter's lazy_modules set and lazy_pending_submodules dict. Declaring or resolving a lazy import from one of them passed NULL to PySet_Add(), PySet_Discard() or the pending-submodules lookup. Treat the bookkeeping as a no-op once the state is gone, as the other readers of this state already do.
1 parent 587b7a5 commit 0b5852c

2 files changed

Lines changed: 53 additions & 3 deletions

File tree

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2807,5 +2807,45 @@ def test_fails_without_lazy(self):
28072807
self.assertIn("ImportError", result.stderr)
28082808

28092809

2810+
@support.requires_subprocess()
2811+
class LazyImportFinalizationTests(unittest.TestCase):
2812+
"""Destructors that use lazy imports after finalization freed their state."""
2813+
2814+
def test_declare(self):
2815+
code = textwrap.dedent("""
2816+
import builtins, os
2817+
2818+
class Canary:
2819+
def __del__(self, write=os.write, exec=exec,
2820+
builtins={"__lazy_import__": builtins.__lazy_import__}):
2821+
exec("lazy import a.b", {"__builtins__": builtins})
2822+
write(1, b"ok")
2823+
2824+
# Only this placeholder keeps the canary alive.
2825+
exec("lazy import pkg.mod", {"__builtins__": {
2826+
"__lazy_import__": builtins.__lazy_import__, "c": Canary()}})
2827+
""")
2828+
self.assertEqual(assert_python_ok("-c", code).out, b"ok")
2829+
2830+
def test_resolve(self):
2831+
code = textwrap.dedent("""
2832+
import contextvars, json, os, types
2833+
2834+
def safe_import(name, *args, allowed={"json": json}):
2835+
return allowed[name]
2836+
2837+
# Not in sys.modules, with builtins that still work at shutdown.
2838+
ns = types.ModuleType("ns")
2839+
ns.json = __lazy_import__("json", {"__builtins__": {"__import__": safe_import}})
2840+
2841+
class Canary:
2842+
def __del__(self, write=os.write, type=type, ns=ns):
2843+
write(1, type(ns.json).__name__.encode())
2844+
2845+
contextvars.ContextVar("v").set(Canary())
2846+
""")
2847+
self.assertEqual(assert_python_ok("-c", code).out, b"module")
2848+
2849+
28102850
if __name__ == '__main__':
28112851
unittest.main()

‎Python/import.c‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ _PyImport_ClearLazyModules(PyInterpreterState *interp)
287287
int
288288
_PyImport_DiscardLazyModule(PyInterpreterState *interp, PyObject *name)
289289
{
290+
// Destructors can still run after finalization cleared the set.
291+
if (LAZY_MODULES(interp) == NULL) {
292+
return 0;
293+
}
290294
return PySet_Discard(LAZY_MODULES(interp), name);
291295
}
292296

@@ -4179,7 +4183,10 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name,
41794183
else {
41804184
Py_XDECREF(mod);
41814185
}
4182-
return loaded ? 0 : PySet_Add(LAZY_MODULES(tstate->interp), name);
4186+
if (loaded || LAZY_MODULES(tstate->interp) == NULL) {
4187+
return 0;
4188+
}
4189+
return PySet_Add(LAZY_MODULES(tstate->interp), name);
41834190
}
41844191

41854192
// Ensure a dict of pending submodule names exists for the parent.
@@ -4213,7 +4220,10 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *source)
42134220
{
42144221
PyDictObject *pending =
42154222
(PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp);
4216-
assert(pending != NULL);
4223+
// Destructors can still run after finalization cleared the dict.
4224+
if (pending == NULL) {
4225+
return 0;
4226+
}
42174227
Py_ssize_t end = PyUnicode_GET_LENGTH(name);
42184228
while (true) {
42194229
Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, end, -1);
@@ -5575,7 +5585,7 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
55755585
/*[clinic end generated code: output=3369bb3242b1f043 input=900339e013ab2b82]*/
55765586
{
55775587
PyInterpreterState *interp = _PyInterpreterState_GET();
5578-
if (PySet_Discard(LAZY_MODULES(interp), name) < 0) {
5588+
if (_PyImport_DiscardLazyModule(interp, name) < 0) {
55795589
return NULL;
55805590
}
55815591
if (_PyImport_ClearLazySubmodule(_PyThreadState_GET(), name, 0) < 0) {

0 commit comments

Comments
 (0)