Skip to content

Commit a6e6361

Browse files
authored
gh-158820: Add null checks to avoid segfaults with lazy imports at shutdown (GH-158821)
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 1b81f4f commit a6e6361

3 files changed

Lines changed: 69 additions & 3 deletions

File tree

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2820,5 +2820,60 @@ def test_fails_without_lazy(self):
28202820
self.assertIn("ImportError", result.stderr)
28212821

28222822

2823+
@support.requires_subprocess()
2824+
class LazyImportFinalizationTests(unittest.TestCase):
2825+
"""Destructors that use lazy imports after finalization freed their state."""
2826+
2827+
def test_declare(self):
2828+
code = textwrap.dedent("""
2829+
import builtins, os
2830+
2831+
class Canary:
2832+
def __del__(self, write=os.write, exec=exec,
2833+
builtins={"__lazy_import__": builtins.__lazy_import__}):
2834+
exec("lazy import a.b", {"__builtins__": builtins})
2835+
write(1, b"ok")
2836+
2837+
# Only this placeholder keeps the canary alive.
2838+
exec("lazy import pkg.mod", {"__builtins__": {
2839+
"__lazy_import__": builtins.__lazy_import__, "c": Canary()}})
2840+
""")
2841+
self.assertEqual(assert_python_ok("-c", code).out, b"ok")
2842+
2843+
def test_resolve(self):
2844+
code = textwrap.dedent("""
2845+
import contextvars, json, os, types
2846+
2847+
def safe_import(name, *args, allowed={"json": json}):
2848+
return allowed[name]
2849+
2850+
# Not in sys.modules, with builtins that still work at shutdown.
2851+
ns = types.ModuleType("ns")
2852+
ns.json = __lazy_import__("json", {"__builtins__": {"__import__": safe_import}})
2853+
2854+
class Canary:
2855+
def __del__(self, write=os.write, type=type, ns=ns):
2856+
write(1, type(ns.json).__name__.encode())
2857+
2858+
contextvars.ContextVar("v").set(Canary())
2859+
""")
2860+
self.assertEqual(assert_python_ok("-c", code).out, b"module")
2861+
2862+
def test_set_lazy_attributes(self):
2863+
code = textwrap.dedent("""
2864+
import _imp, os, sys
2865+
2866+
class Canary:
2867+
def __del__(self, write=os.write,
2868+
set_lazy=_imp._set_lazy_attributes):
2869+
set_lazy(None, "mod")
2870+
write(1, b"ok")
2871+
2872+
# Freed while sys.lazy_modules is being cleared.
2873+
sys.lazy_modules.add(Canary())
2874+
""")
2875+
self.assertEqual(assert_python_ok("-c", code).out, b"ok")
2876+
2877+
28232878
if __name__ == '__main__':
28242879
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a crash when a :ref:`lazy import <lazy-imports>` is declared or resolved
2+
by a finalizer running during interpreter shutdown.

‎Python/import.c‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ _PyImport_ClearLazyModules(PyInterpreterState *interp)
287287
int
288288
_PyImport_DiscardLazyModule(PyInterpreterState *interp, PyObject *name)
289289
{
290+
if (LAZY_MODULES(interp) == NULL) {
291+
return 0;
292+
}
290293
return PySet_Discard(LAZY_MODULES(interp), name);
291294
}
292295

@@ -4179,7 +4182,10 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name,
41794182
else {
41804183
Py_XDECREF(mod);
41814184
}
4182-
return loaded ? 0 : PySet_Add(LAZY_MODULES(tstate->interp), name);
4185+
if (loaded || LAZY_MODULES(tstate->interp) == NULL) {
4186+
return 0;
4187+
}
4188+
return PySet_Add(LAZY_MODULES(tstate->interp), name);
41834189
}
41844190

41854191
// Ensure a dict of pending submodule names exists for the parent.
@@ -4213,7 +4219,10 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *source)
42134219
{
42144220
PyDictObject *pending =
42154221
(PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp);
4216-
assert(pending != NULL);
4222+
// Finalizers can still run after finalize_modules() cleared the dict.
4223+
if (pending == NULL) {
4224+
return 0;
4225+
}
42174226
Py_ssize_t end = PyUnicode_GET_LENGTH(name);
42184227
while (true) {
42194228
Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, end, -1);
@@ -5575,7 +5584,7 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
55755584
/*[clinic end generated code: output=3369bb3242b1f043 input=900339e013ab2b82]*/
55765585
{
55775586
PyInterpreterState *interp = _PyInterpreterState_GET();
5578-
if (PySet_Discard(LAZY_MODULES(interp), name) < 0) {
5587+
if (_PyImport_DiscardLazyModule(interp, name) < 0) {
55795588
return NULL;
55805589
}
55815590
if (_PyImport_ClearLazySubmodule(_PyThreadState_GET(), name, 0) < 0) {

0 commit comments

Comments
 (0)