https://github.com/python/cpython/commit/f8375c23b9470496473bf2139dbe4be2e2b59b07 commit: f8375c23b9470496473bf2139dbe4be2e2b59b07 branch: 3.15 author: Miss Islington (bot) <[email protected]> committer: JelleZijlstra <[email protected]> date: 2026-10-09T12:14:23-07:00 summary:
[3.15] gh-158820: Add null checks to avoid segfaults with lazy imports at shutdown (GH-158821) (#159062) 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. (cherry picked from commit a6e636148e96dc3430b50b40e5b3e92173337130) Co-authored-by: Brittany Reynoso <[email protected]> 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 4c116ca0c0a5c1..aea0d84ee685fe 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -2822,5 +2822,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 00000000000000..9eb11205e452fa --- /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 062f7009ae1255..daa9049486e181 100644 --- a/Python/import.c +++ b/Python/import.c @@ -289,6 +289,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); } @@ -4176,7 +4179,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. @@ -4210,7 +4216,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); @@ -5572,7 +5581,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]
