https://github.com/python/cpython/commit/a6e636148e96dc3430b50b40e5b3e92173337130
commit: a6e636148e96dc3430b50b40e5b3e92173337130
branch: main
author: Brittany Reynoso <[email protected]>
committer: encukou <[email protected]>
date: 2026-10-09T13:57:05+02:00
summary:

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.

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst
M Lib/test/test_lazy_import/__init__.py
M Python/import.c

diff --git a/Lib/test/test_lazy_import/__init__.py 
b/Lib/test/test_lazy_import/__init__.py
index 8a90ffa662e6e23..f1169a2aa206bfb 100644
--- a/Lib/test/test_lazy_import/__init__.py
+++ b/Lib/test/test_lazy_import/__init__.py
@@ -2820,5 +2820,60 @@ def test_fails_without_lazy(self):
         self.assertIn("ImportError", result.stderr)
 
 
[email protected]_subprocess()
+class LazyImportFinalizationTests(unittest.TestCase):
+    """Destructors that use lazy imports after finalization freed their 
state."""
+
+    def test_declare(self):
+        code = textwrap.dedent("""
+            import builtins, os
+
+            class Canary:
+                def __del__(self, write=os.write, exec=exec,
+                            builtins={"__lazy_import__": 
builtins.__lazy_import__}):
+                    exec("lazy import a.b", {"__builtins__": builtins})
+                    write(1, b"ok")
+
+            # Only this placeholder keeps the canary alive.
+            exec("lazy import pkg.mod", {"__builtins__": {
+                "__lazy_import__": builtins.__lazy_import__, "c": Canary()}})
+        """)
+        self.assertEqual(assert_python_ok("-c", code).out, b"ok")
+
+    def test_resolve(self):
+        code = textwrap.dedent("""
+            import contextvars, json, os, types
+
+            def safe_import(name, *args, allowed={"json": json}):
+                return allowed[name]
+
+            # Not in sys.modules, with builtins that still work at shutdown.
+            ns = types.ModuleType("ns")
+            ns.json = __lazy_import__("json", {"__builtins__": {"__import__": 
safe_import}})
+
+            class Canary:
+                def __del__(self, write=os.write, type=type, ns=ns):
+                    write(1, type(ns.json).__name__.encode())
+
+            contextvars.ContextVar("v").set(Canary())
+        """)
+        self.assertEqual(assert_python_ok("-c", code).out, b"module")
+
+    def test_set_lazy_attributes(self):
+        code = textwrap.dedent("""
+            import _imp, os, sys
+
+            class Canary:
+                def __del__(self, write=os.write,
+                            set_lazy=_imp._set_lazy_attributes):
+                    set_lazy(None, "mod")
+                    write(1, b"ok")
+
+            # Freed while sys.lazy_modules is being cleared.
+            sys.lazy_modules.add(Canary())
+        """)
+        self.assertEqual(assert_python_ok("-c", code).out, b"ok")
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst
new file mode 100644
index 000000000000000..9eb11205e452fae
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst
@@ -0,0 +1,2 @@
+Fix a crash when a :ref:`lazy import <lazy-imports>` is declared or resolved
+by a finalizer running during interpreter shutdown.
diff --git a/Python/import.c b/Python/import.c
index 4d3e74f45e5e366..7538575fbf7520d 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -287,6 +287,9 @@ _PyImport_ClearLazyModules(PyInterpreterState *interp)
 int
 _PyImport_DiscardLazyModule(PyInterpreterState *interp, PyObject *name)
 {
+    if (LAZY_MODULES(interp) == NULL) {
+        return 0;
+    }
     return PySet_Discard(LAZY_MODULES(interp), name);
 }
 
@@ -4179,7 +4182,10 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name,
     else {
         Py_XDECREF(mod);
     }
-    return loaded ? 0 : PySet_Add(LAZY_MODULES(tstate->interp), name);
+    if (loaded || LAZY_MODULES(tstate->interp) == NULL) {
+        return 0;
+    }
+    return PySet_Add(LAZY_MODULES(tstate->interp), name);
 }
 
 // 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)
 {
     PyDictObject *pending =
         (PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp);
-    assert(pending != NULL);
+    // Finalizers can still run after finalize_modules() cleared the dict.
+    if (pending == NULL) {
+        return 0;
+    }
     Py_ssize_t end = PyUnicode_GET_LENGTH(name);
     while (true) {
         Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, end, -1);
@@ -5575,7 +5584,7 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject 
*modobj,
 /*[clinic end generated code: output=3369bb3242b1f043 input=900339e013ab2b82]*/
 {
     PyInterpreterState *interp = _PyInterpreterState_GET();
-    if (PySet_Discard(LAZY_MODULES(interp), name) < 0) {
+    if (_PyImport_DiscardLazyModule(interp, name) < 0) {
         return NULL;
     }
     if (_PyImport_ClearLazySubmodule(_PyThreadState_GET(), name, 0) < 0) {

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to