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]