From 0b5852c5140085bb756e0a23fa37231a53afed87 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Sun, 4 Oct 2026 18:41:10 -0700 Subject: [PATCH 1/4] 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. --- Lib/test/test_lazy_import/__init__.py | 40 +++++++++++++++++++++++++++ Python/import.c | 16 +++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index e0c5552fe11c53..0ff1b22dd6ea73 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -2807,5 +2807,45 @@ def test_fails_without_lazy(self): self.assertIn("ImportError", result.stderr) +@support.requires_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") + + if __name__ == '__main__': unittest.main() diff --git a/Python/import.c b/Python/import.c index 4d3e74f45e5e36..113fae7b6789e8 100644 --- a/Python/import.c +++ b/Python/import.c @@ -287,6 +287,10 @@ _PyImport_ClearLazyModules(PyInterpreterState *interp) int _PyImport_DiscardLazyModule(PyInterpreterState *interp, PyObject *name) { + // Destructors can still run after finalization cleared the set. + if (LAZY_MODULES(interp) == NULL) { + return 0; + } return PySet_Discard(LAZY_MODULES(interp), name); } @@ -4179,7 +4183,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 +4220,10 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *source) { PyDictObject *pending = (PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp); - assert(pending != NULL); + // Destructors can still run after finalization 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 +5585,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) { From cc628a401795c621ad69a6c33daf94bd428d0f5d Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Sun, 4 Oct 2026 18:44:25 -0700 Subject: [PATCH 2/4] Remove comments on the lazy import state NULL checks --- Python/import.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Python/import.c b/Python/import.c index 113fae7b6789e8..332a5aafed9610 100644 --- a/Python/import.c +++ b/Python/import.c @@ -287,7 +287,6 @@ _PyImport_ClearLazyModules(PyInterpreterState *interp) int _PyImport_DiscardLazyModule(PyInterpreterState *interp, PyObject *name) { - // Destructors can still run after finalization cleared the set. if (LAZY_MODULES(interp) == NULL) { return 0; } @@ -4220,7 +4219,6 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *source) { PyDictObject *pending = (PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp); - // Destructors can still run after finalization cleared the dict. if (pending == NULL) { return 0; } From a13e2d55af425c87e991850adcba7f11ca408666 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 01:59:24 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst 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 00000000000000..cda1fe18e03549 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst @@ -0,0 +1 @@ +Fix a segfault triggered by declaring or resolving a lazy import within a finalizer during shutdown. From b1651ea5e422c1a6137c0bab552ec6e41563390b Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Mon, 5 Oct 2026 13:12:53 -0700 Subject: [PATCH 4/4] Test _imp._set_lazy_attributes() at shutdown, add a comment and reword NEWS --- Lib/test/test_lazy_import/__init__.py | 15 +++++++++++++++ ...2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst | 3 ++- Python/import.c | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 0ff1b22dd6ea73..53d5cbaf17e6b4 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -2846,6 +2846,21 @@ def __del__(self, write=os.write, type=type, ns=ns): """) 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 index cda1fe18e03549..9eb11205e452fa 100644 --- 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 @@ -1 +1,2 @@ -Fix a segfault triggered by declaring or resolving a lazy import within a finalizer during shutdown. +Fix a crash when a :ref:`lazy import ` is declared or resolved +by a finalizer running during interpreter shutdown. diff --git a/Python/import.c b/Python/import.c index 332a5aafed9610..7538575fbf7520 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4219,6 +4219,7 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *source) { PyDictObject *pending = (PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp); + // Finalizers can still run after finalize_modules() cleared the dict. if (pending == NULL) { return 0; }