From 632d95b6c905ef129f9e1a6954d1bfd862449f8f Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Fri, 26 Jun 2026 23:55:50 +0800 Subject: [PATCH 01/14] gh-152298: Fix LazyImportType resolve reification --- Include/internal/pycore_lazyimportobject.h | 18 ++ Lib/test/test_lazy_import/__init__.py | 267 ++++++++++++++++++ ...-06-26-23-58-00.gh-issue-152298.5uT4tW.rst | 3 + Objects/lazyimportobject.c | 123 +++++++- Objects/moduleobject.c | 7 +- Python/bytecodes.c | 58 +++- Python/ceval.c | 9 +- Python/executor_cases.c.h | 29 +- Python/generated_cases.c.h | 106 ++++++- Python/import.c | 9 + 10 files changed, 606 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-23-58-00.gh-issue-152298.5uT4tW.rst diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index b81e4211b08ff3..332744cb680d7f 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -19,6 +19,9 @@ typedef struct { PyObject *lz_builtins; PyObject *lz_from; PyObject *lz_attr; + PyObject *lz_owner_globals; + PyObject *lz_owner_key; + PyObject *lz_resolved; // Frame information for the original import location. PyCodeObject *lz_code; // Code object where the lazy import was created. int lz_instr_offset; // Instruction offset where the lazy import was created. @@ -28,6 +31,21 @@ typedef struct { PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import); PyAPI_FUNC(PyObject *) _PyLazyImport_New( struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr); +PyAPI_FUNC(void) _PyLazyImport_BindGlobal( + PyThreadState *tstate, + PyObject *lazy_import, + PyObject *globals, + PyObject *name); +PyAPI_FUNC(int) _PyLazyImport_CommitIfCurrent( + PyThreadState *tstate, + PyObject *lazy_import, + PyObject *globals, + PyObject *name, + PyObject *resolved); +PyAPI_FUNC(int) _PyLazyImport_CommitStoredIfCurrent( + PyThreadState *tstate, + PyObject *lazy_import, + PyObject *resolved); #ifdef __cplusplus } diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 4658882243d65f..4a243edac3a1d0 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -277,6 +277,273 @@ def test_lazy_import_type_attributes_accessible(self): proc = assert_python_ok("-c", code) self.assertIn(b"lz_builtins = Py_XNewRef(builtins); m->lz_from = Py_NewRef(name); m->lz_attr = Py_XNewRef(fromlist); + m->lz_owner_globals = NULL; + m->lz_owner_key = NULL; + m->lz_resolved = NULL; // Capture frame information for the original import location. m->lz_code = NULL; @@ -58,6 +63,9 @@ lazy_import_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(m->lz_builtins); Py_VISIT(m->lz_from); Py_VISIT(m->lz_attr); + Py_VISIT(m->lz_owner_globals); + Py_VISIT(m->lz_owner_key); + Py_VISIT(m->lz_resolved); Py_VISIT(m->lz_code); return 0; } @@ -69,6 +77,9 @@ lazy_import_clear(PyObject *op) Py_CLEAR(m->lz_builtins); Py_CLEAR(m->lz_from); Py_CLEAR(m->lz_attr); + Py_CLEAR(m->lz_owner_globals); + Py_CLEAR(m->lz_owner_key); + Py_CLEAR(m->lz_resolved); Py_CLEAR(m->lz_code); return 0; } @@ -116,10 +127,120 @@ _PyLazyImport_GetName(PyObject *op) return lazy_import_name(lazy_import); } +void +_PyLazyImport_BindGlobal(PyThreadState *tstate, PyObject *op, + PyObject *globals, PyObject *name) +{ + if (!PyLazyImport_CheckExact(op) || + !PyDict_CheckExact(globals) || + !PyUnicode_CheckExact(name)) + { + return; + } + + PyLazyImportObject *m = PyLazyImportObject_CAST(op); + PyInterpreterState *interp = tstate->interp; + _PyImport_AcquireLock(interp); + if (m->lz_resolved == NULL && + m->lz_owner_globals == NULL && + m->lz_owner_key == NULL) + { + m->lz_owner_globals = Py_NewRef(globals); + m->lz_owner_key = Py_NewRef(name); + } + _PyImport_ReleaseLock(interp); +} + +static void +clear_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, + PyObject *globals, PyObject *name) +{ + PyInterpreterState *interp = tstate->interp; + _PyImport_AcquireLock(interp); + if (m->lz_owner_globals == globals && m->lz_owner_key == name) { + Py_CLEAR(m->lz_owner_globals); + Py_CLEAR(m->lz_owner_key); + } + _PyImport_ReleaseLock(interp); +} + +int +_PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op, + PyObject *globals, PyObject *name, + PyObject *value) +{ + if (!PyLazyImport_CheckExact(op) || + !PyDict_CheckExact(globals) || + !PyUnicode_CheckExact(name)) + { + return 0; + } + + PyObject *current = NULL; + int err = 0; + + Py_BEGIN_CRITICAL_SECTION(globals); + int found = _PyDict_GetItemRef_Unicode_LockHeld( + (PyDictObject *)globals, name, ¤t); + if (found < 0) { + err = -1; + } + else if (found > 0 && current == op) { + err = _PyDict_SetItem_LockHeld((PyDictObject *)globals, name, value); + } + Py_XDECREF(current); + Py_END_CRITICAL_SECTION(); + + if (err == 0) { + clear_owner_if_matches(tstate, PyLazyImportObject_CAST(op), + globals, name); + } + return err; +} + +int +_PyLazyImport_CommitStoredIfCurrent(PyThreadState *tstate, PyObject *op, + PyObject *value) +{ + if (!PyLazyImport_CheckExact(op)) { + return 0; + } + + PyLazyImportObject *m = PyLazyImportObject_CAST(op); + PyObject *globals = NULL; + PyObject *name = NULL; + + PyInterpreterState *interp = tstate->interp; + _PyImport_AcquireLock(interp); + if (m->lz_owner_globals != NULL && m->lz_owner_key != NULL) { + globals = Py_NewRef(m->lz_owner_globals); + name = Py_NewRef(m->lz_owner_key); + } + _PyImport_ReleaseLock(interp); + + int err = 0; + if (globals != NULL && name != NULL) { + err = _PyLazyImport_CommitIfCurrent( + tstate, op, globals, name, value); + } + Py_XDECREF(globals); + Py_XDECREF(name); + return err; +} + static PyObject * lazy_import_resolve(PyObject *self, PyObject *args) { - return _PyImport_LoadLazyImportTstate(PyThreadState_GET(), self); + PyThreadState *tstate = PyThreadState_GET(); + PyObject *resolved = _PyImport_LoadLazyImportTstate(tstate, self); + if (resolved == NULL) { + return NULL; + } + if (_PyLazyImport_CommitStoredIfCurrent(tstate, self, resolved) < 0) { + Py_DECREF(resolved); + return NULL; + } + return resolved; } static PyMethodDef lazy_import_methods[] = { diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index f447403ef31b43..d4d37b833fd3a3 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1353,8 +1353,9 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } PyErr_Clear(); } + PyThreadState *tstate = PyThreadState_GET(); PyObject *new_value = _PyImport_LoadLazyImportTstate( - PyThreadState_GET(), attr); + tstate, attr); if (new_value == NULL) { if (suppress && PyErr_ExceptionMatches(PyExc_ImportCycleError)) { @@ -1368,7 +1369,9 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) return NULL; } - if (PyDict_SetItem(m->md_dict, name, new_value) < 0) { + if (_PyLazyImport_CommitIfCurrent( + tstate, attr, m->md_dict, name, new_value) < 0) + { Py_CLEAR(new_value); } Py_DECREF(attr); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 8afa0be702357d..95e9995a09e971 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2005,7 +2005,11 @@ dummy_func( ERROR_IF(true); } if (PyDict_CheckExact(ns)) { - err = PyDict_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + _PyLazyImport_BindGlobal(tstate, value, ns, name); + } + err = PyDict_SetItem(ns, name, value); } else { err = PyObject_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); @@ -2189,7 +2193,11 @@ dummy_func( inst(STORE_GLOBAL, (v --)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); - int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (PyLazyImport_CheckExact(value)) { + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + } + int err = PyDict_SetItem(GLOBALS(), name, value); PyStackRef_CLOSE(v); ERROR_IF(err); } @@ -2221,7 +2229,9 @@ dummy_func( inst(LOAD_FROM_DICT_OR_GLOBALS, (mod_or_class_dict -- v)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); int err; - PyObject *v_o = _PyMapping_GetOptionalItem2(PyStackRef_AsPyObjectBorrow(mod_or_class_dict), name, &err); + PyObject *source = PyStackRef_AsPyObjectBorrow(mod_or_class_dict); + PyObject *v_o = _PyMapping_GetOptionalItem2(source, name, &err); + int commit_source = (v_o != NULL && source == GLOBALS()); PyStackRef_CLOSE(mod_or_class_dict); ERROR_IF(err < 0); @@ -2244,8 +2254,18 @@ dummy_func( if (PyLazyImport_CheckExact(v_o)) { PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); + if (l_v == NULL) { + Py_DECREF(v_o); + ERROR_IF(true); + } + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); + if (err < 0) { + Py_DECREF(v_o); + Py_DECREF(l_v); + ERROR_IF(true); + } Py_SETREF(v_o, l_v); - ERROR_IF(v_o == NULL); } } else { @@ -2266,11 +2286,36 @@ dummy_func( } if (PyLazyImport_CheckExact(v_o)) { PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); + if (l_v == NULL) { + Py_DECREF(v_o); + ERROR_IF(true); + } + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); + if (err < 0) { + Py_DECREF(v_o); + Py_DECREF(l_v); + ERROR_IF(true); + } Py_SETREF(v_o, l_v); - ERROR_IF(v_o == NULL); } } } + else if (commit_source && PyLazyImport_CheckExact(v_o)) { + PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); + if (l_v == NULL) { + Py_DECREF(v_o); + ERROR_IF(true); + } + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, source, name, l_v); + if (err < 0) { + Py_DECREF(v_o); + Py_DECREF(l_v); + ERROR_IF(true); + } + Py_SETREF(v_o, l_v); + } v = PyStackRef_FromPyObjectSteal(v_o); } @@ -2285,7 +2330,8 @@ dummy_func( Py_DECREF(v_o); ERROR_IF(true); } - int err = PyDict_SetItem(GLOBALS(), name, l_v); + int err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); if (err < 0) { Py_DECREF(v_o); Py_DECREF(l_v); diff --git a/Python/ceval.c b/Python/ceval.c index fbea1f67a36f44..efc82fb4fd6d5a 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3651,14 +3651,17 @@ _PyEval_LoadGlobalStackRef(PyObject *globals, PyObject *builtins, PyObject *name PyObject *res_o = PyStackRef_AsPyObjectBorrow(*writeto); if (res_o != NULL && PyLazyImport_CheckExact(res_o)) { - PyObject *l_v = _PyImport_LoadLazyImportTstate(PyThreadState_GET(), res_o); - PyStackRef_CLOSE(writeto[0]); + PyThreadState *tstate = PyThreadState_GET(); + PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, res_o); if (l_v == NULL) { + PyStackRef_CLOSE(writeto[0]); assert(PyErr_Occurred()); *writeto = PyStackRef_NULL; return; } - int err = PyDict_SetItem(globals, name, l_v); + int err = _PyLazyImport_CommitIfCurrent( + tstate, res_o, globals, name, l_v); + PyStackRef_CLOSE(writeto[0]); if (err < 0) { Py_DECREF(l_v); *writeto = PyStackRef_NULL; diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 63b77c9e152778..89cfa2e1118a36 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9587,12 +9587,23 @@ JUMP_TO_ERROR(); } if (PyDict_CheckExact(ns)) { + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + stack_pointer += -1; + } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); } else { @@ -10076,12 +10087,23 @@ oparg = CURRENT_OPARG(); v = _stack_item_0; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (PyLazyImport_CheckExact(value)) { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + stack_pointer += -1; + } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); + int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -10238,7 +10260,8 @@ } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, l_v); + int err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); _PyFrame_StackPointerInvalidate(frame); if (err < 0) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 2cdf48c559a292..ebe45c9e2e24f4 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -10021,10 +10021,12 @@ mod_or_class_dict = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); int err; + PyObject *source = PyStackRef_AsPyObjectBorrow(mod_or_class_dict); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - PyObject *v_o = _PyMapping_GetOptionalItem2(PyStackRef_AsPyObjectBorrow(mod_or_class_dict), name, &err); + PyObject *v_o = _PyMapping_GetOptionalItem2(source, name, &err); _PyFrame_StackPointerInvalidate(frame); + int commit_source = (v_o != NULL && source == GLOBALS()); stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -10059,13 +10061,33 @@ _PyFrame_StackPointerValidate(frame); PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); _PyFrame_StackPointerInvalidate(frame); + if (l_v == NULL) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + JUMP_TO_LABEL(error); + } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - Py_SETREF(v_o, l_v); + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); _PyFrame_StackPointerInvalidate(frame); - if (v_o == NULL) { + if (err < 0) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(l_v); + _PyFrame_StackPointerInvalidate(frame); JUMP_TO_LABEL(error); } + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_SETREF(v_o, l_v); + _PyFrame_StackPointerInvalidate(frame); } } else { @@ -10099,16 +10121,69 @@ _PyFrame_StackPointerValidate(frame); PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); _PyFrame_StackPointerInvalidate(frame); + if (l_v == NULL) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + JUMP_TO_LABEL(error); + } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - Py_SETREF(v_o, l_v); + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); _PyFrame_StackPointerInvalidate(frame); - if (v_o == NULL) { + if (err < 0) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(l_v); + _PyFrame_StackPointerInvalidate(frame); JUMP_TO_LABEL(error); } + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_SETREF(v_o, l_v); + _PyFrame_StackPointerInvalidate(frame); } } } + else if (commit_source && PyLazyImport_CheckExact(v_o)) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); + _PyFrame_StackPointerInvalidate(frame); + if (l_v == NULL) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + JUMP_TO_LABEL(error); + } + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, source, name, l_v); + _PyFrame_StackPointerInvalidate(frame); + if (err < 0) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(l_v); + _PyFrame_StackPointerInvalidate(frame); + JUMP_TO_LABEL(error); + } + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_SETREF(v_o, l_v); + _PyFrame_StackPointerInvalidate(frame); + } v = PyStackRef_FromPyObjectSteal(v_o); stack_pointer[0] = v; stack_pointer += 1; @@ -10377,7 +10452,8 @@ } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, l_v); + int err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); _PyFrame_StackPointerInvalidate(frame); if (err < 0) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); @@ -12546,9 +12622,16 @@ _PyStackRef v; v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); + int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -12590,9 +12673,16 @@ JUMP_TO_LABEL(error); } if (PyDict_CheckExact(ns)) { + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); } else { diff --git a/Python/import.c b/Python/import.c index 6da6faf5f28cc3..6327ec606085de 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3897,6 +3897,12 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) // Acquire the global import lock to serialize reification _PyImport_AcquireLock(interp); + if (lz->lz_resolved != NULL) { + obj = Py_NewRef(lz->lz_resolved); + _PyImport_ReleaseLock(interp); + return obj; + } + // Check if we are already importing this module, if so, then we want to // return an error that indicates we've hit a cycle which will indicate // the value isn't yet available. @@ -4093,6 +4099,9 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) if (PySet_Discard(importing, lazy_import) < 0) { Py_CLEAR(obj); } + else if (obj != NULL) { + lz->lz_resolved = Py_NewRef(obj); + } // Release the global import lock. _PyImport_ReleaseLock(interp); From a407b55ada84125a9d5fdbd86eda87bc99af6f8d Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 00:02:40 +0800 Subject: [PATCH 02/14] gh-152298: Regenerate lazy import test cases --- ...-06-26-23-58-00.gh-issue-152298.5uT4tW.rst | 2 +- Modules/_testinternalcapi/test_cases.c.h | 106 ++++++++++++++++-- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-23-58-00.gh-issue-152298.5uT4tW.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-23-58-00.gh-issue-152298.5uT4tW.rst index 0d44ed63b6cb73..4e83175735f17b 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-23-58-00.gh-issue-152298.5uT4tW.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-26-23-58-00.gh-issue-152298.5uT4tW.rst @@ -1,3 +1,3 @@ -Make :meth:`types.LazyImportType.resolve` cache successful resolutions and +Make ``types.LazyImportType.resolve()`` cache successful resolutions and replace the original module global only when it still references the same lazy import proxy. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 76493327ca0a00..826e4eaa5ce5b8 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -10023,10 +10023,12 @@ mod_or_class_dict = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); int err; + PyObject *source = PyStackRef_AsPyObjectBorrow(mod_or_class_dict); _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - PyObject *v_o = _PyMapping_GetOptionalItem2(PyStackRef_AsPyObjectBorrow(mod_or_class_dict), name, &err); + PyObject *v_o = _PyMapping_GetOptionalItem2(source, name, &err); _PyFrame_StackPointerInvalidate(frame); + int commit_source = (v_o != NULL && source == GLOBALS()); stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -10061,13 +10063,33 @@ _PyFrame_StackPointerValidate(frame); PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); _PyFrame_StackPointerInvalidate(frame); + if (l_v == NULL) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + JUMP_TO_LABEL(error); + } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - Py_SETREF(v_o, l_v); + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); _PyFrame_StackPointerInvalidate(frame); - if (v_o == NULL) { + if (err < 0) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(l_v); + _PyFrame_StackPointerInvalidate(frame); JUMP_TO_LABEL(error); } + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_SETREF(v_o, l_v); + _PyFrame_StackPointerInvalidate(frame); } } else { @@ -10101,16 +10123,69 @@ _PyFrame_StackPointerValidate(frame); PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); _PyFrame_StackPointerInvalidate(frame); + if (l_v == NULL) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + JUMP_TO_LABEL(error); + } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - Py_SETREF(v_o, l_v); + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); _PyFrame_StackPointerInvalidate(frame); - if (v_o == NULL) { + if (err < 0) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(l_v); + _PyFrame_StackPointerInvalidate(frame); JUMP_TO_LABEL(error); } + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_SETREF(v_o, l_v); + _PyFrame_StackPointerInvalidate(frame); } } } + else if (commit_source && PyLazyImport_CheckExact(v_o)) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o); + _PyFrame_StackPointerInvalidate(frame); + if (l_v == NULL) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + JUMP_TO_LABEL(error); + } + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, source, name, l_v); + _PyFrame_StackPointerInvalidate(frame); + if (err < 0) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(v_o); + _PyFrame_StackPointerInvalidate(frame); + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_DECREF(l_v); + _PyFrame_StackPointerInvalidate(frame); + JUMP_TO_LABEL(error); + } + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + Py_SETREF(v_o, l_v); + _PyFrame_StackPointerInvalidate(frame); + } v = PyStackRef_FromPyObjectSteal(v_o); stack_pointer[0] = v; stack_pointer += 1; @@ -10379,7 +10454,8 @@ } assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, l_v); + int err = _PyLazyImport_CommitIfCurrent( + tstate, v_o, GLOBALS(), name, l_v); _PyFrame_StackPointerInvalidate(frame); if (err < 0) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); @@ -12549,9 +12625,16 @@ _PyStackRef v; v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v)); + int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -12593,9 +12676,16 @@ JUMP_TO_LABEL(error); } if (PyDict_CheckExact(ns)) { + PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); + err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); } else { From 36f1821d6f08b121df1a2dca518af06064f4ba36 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 00:12:02 +0800 Subject: [PATCH 03/14] gh-152298: Cover module attribute lazy reification --- Lib/test/test_lazy_import/__init__.py | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 4a243edac3a1d0..4ecfc400e9d2ed 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -544,6 +544,36 @@ def custom_import(name, globals=None, locals=None, fromlist=None, """) assert_python_ok("-c", code) + @support.requires_subprocess() + def test_module_attribute_reification_does_not_clobber_changed_global(self): + code = textwrap.dedent(""" + import builtins + import sys + import types + + real_import = builtins.__import__ + module = sys.modules[__name__] + + lazy import target_module as target + + def custom_import(name, globals=None, locals=None, fromlist=None, + level=0): + if name == "target_module": + module.__dict__["target"] = "user value" + resolved = types.ModuleType(name) + resolved.VALUE = "resolved" + return resolved + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + assert module.target.VALUE == "resolved" + assert module.__dict__["target"] == "user value" + finally: + builtins.__import__ = real_import + """) + assert_python_ok("-c", code) + class SyntaxRestrictionTests(LazyImportTestCase): """Tests for syntax restrictions on lazy imports.""" From 3ce4bd116fe135739a1a087a1667b978f9b15a01 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 09:55:33 +0800 Subject: [PATCH 04/14] gh-152298: Clarify lazy resolve owner cleanup --- Doc/library/types.rst | 10 ++ Include/internal/pycore_lazyimportobject.h | 2 + Lib/test/test_lazy_import/__init__.py | 160 +++++++++++++++++++++ Objects/lazyimportobject.c | 35 +++-- 4 files changed, 197 insertions(+), 10 deletions(-) diff --git a/Doc/library/types.rst b/Doc/library/types.rst index 38a77119769d72..af3fe47c181776 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -350,6 +350,16 @@ Standard names are defined for the following types: actually accessed. This type can be used to detect lazy imports programmatically. + .. method:: resolve() + + Resolve the lazy import and return the imported object. Successful + resolutions are cached. + + For module-level lazy imports, ``resolve()`` replaces the original global + only while it still refers to this proxy. If that binding was deleted or + rebound, it is left unchanged and later ``resolve()`` calls on the same + proxy do not update it. + .. versionadded:: 3.15 .. seealso:: :pep:`810` diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index 332744cb680d7f..de42e9ff9c52ea 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -19,6 +19,8 @@ typedef struct { PyObject *lz_builtins; PyObject *lz_from; PyObject *lz_attr; + // Protected by the import lock. The owner fields remember the first + // globals/key pair until resolution succeeds or the proxy is cleared. PyObject *lz_owner_globals; PyObject *lz_owner_key; PyObject *lz_resolved; diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 4ecfc400e9d2ed..1f766465eccedb 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -574,6 +574,166 @@ def custom_import(name, globals=None, locals=None, fromlist=None, """) assert_python_ok("-c", code) + @support.requires_subprocess() + def test_module_attr_dynamic_equal_name_clears_owner(self): + code = textwrap.dedent(""" + import builtins + import sys + import types + + real_import = builtins.__import__ + module = sys.modules[__name__] + + lazy import target_module as target + proxy = globals()["target"] + stored_name = next(name for name in globals() if name == "target") + dynamic_name = bytearray(b"target").decode() + assert dynamic_name == stored_name + assert dynamic_name is not stored_name + + def custom_import(name, globals=None, locals=None, fromlist=None, + level=0): + if name == "target_module": + resolved = types.ModuleType(name) + resolved.VALUE = "resolved" + return resolved + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + resolved = getattr(module, dynamic_name) + assert resolved.VALUE == "resolved" + assert module.__dict__["target"] is resolved + + module.__dict__["target"] = proxy + again = proxy.resolve() + assert again is resolved + assert module.__dict__["target"] is proxy + finally: + builtins.__import__ = real_import + """) + assert_python_ok("-c", code) + + @support.requires_subprocess() + def test_rebound_back_to_same_proxy_after_stale_resolve_does_not_replace(self): + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + + lazy import target_module as target + proxy = globals()["target"] + + def custom_import(name, globals=None, locals=None, fromlist=None, + level=0): + if name == "target_module": + calls.append(name) + resolved = types.ModuleType(name) + resolved.VALUE = "resolved" + return resolved + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + sentinel = object() + globals()["target"] = sentinel + resolved = proxy.resolve() + assert globals()["target"] is sentinel + + globals()["target"] = proxy + again = proxy.resolve() + assert again is resolved + assert globals()["target"] is proxy + assert len(calls) == 1, calls + finally: + builtins.__import__ = real_import + """) + assert_python_ok("-c", code) + + @support.requires_subprocess() + def test_resolve_replaces_unaliased_import_global(self): + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + + lazy import target_module + proxy = globals()["target_module"] + + def custom_import(name, globals=None, locals=None, fromlist=None, + level=0): + if name == "target_module": + resolved = types.ModuleType(name) + resolved.VALUE = "resolved" + return resolved + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + resolved = proxy.resolve() + assert resolved.VALUE == "resolved" + assert globals()["target_module"] is resolved + finally: + builtins.__import__ = real_import + """) + assert_python_ok("-c", code) + + @support.requires_subprocess() + def test_resolve_replaces_unaliased_dotted_import_binding(self): + code = textwrap.dedent(""" + import types + + lazy import test.test_lazy_import.data.pkg.bar + proxy = globals()["test"] + + resolved = proxy.resolve() + assert isinstance(resolved, types.ModuleType) + assert resolved.__name__ == "test" + assert globals()["test"] is resolved + assert "test.test_lazy_import.data.pkg" not in globals() + """) + assert_python_ok("-c", code) + + @support.requires_subprocess() + def test_copying_lazy_proxy_to_second_global_keeps_first_owner(self): + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + + lazy import target_module as primary + secondary = globals()["primary"] + proxy = globals()["primary"] + + def custom_import(name, globals=None, locals=None, fromlist=None, + level=0): + if name == "target_module": + calls.append(name) + resolved = types.ModuleType(name) + resolved.VALUE = "resolved" + return resolved + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = custom_import + try: + resolved = proxy.resolve() + assert globals()["primary"] is resolved + assert globals()["secondary"] is proxy + + again = globals()["secondary"].resolve() + assert again is resolved + assert globals()["secondary"] is proxy + assert len(calls) == 1, calls + finally: + builtins.__import__ = real_import + """) + assert_python_ok("-c", code) + class SyntaxRestrictionTests(LazyImportTestCase): """Tests for syntax restrictions on lazy imports.""" diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index 762678b1785c64..6e8c716d066aa1 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -9,6 +9,7 @@ #include "pycore_interpframe.h" #include "pycore_lazyimportobject.h" #include "pycore_modsupport.h" +#include "pycore_unicodeobject.h" #define PyLazyImportObject_CAST(op) ((PyLazyImportObject *)(op)) @@ -141,6 +142,8 @@ _PyLazyImport_BindGlobal(PyThreadState *tstate, PyObject *op, PyLazyImportObject *m = PyLazyImportObject_CAST(op); PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); + // First owner wins: copies of the proxy do not change the original global + // binding that resolve() may replace. if (m->lz_resolved == NULL && m->lz_owner_globals == NULL && m->lz_owner_key == NULL) @@ -157,7 +160,10 @@ clear_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, { PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); - if (m->lz_owner_globals == globals && m->lz_owner_key == name) { + if (m->lz_owner_globals == globals && + m->lz_owner_key != NULL && + _PyUnicode_Equal(m->lz_owner_key, name)) + { Py_CLEAR(m->lz_owner_globals); Py_CLEAR(m->lz_owner_key); } @@ -178,20 +184,27 @@ _PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op, PyObject *current = NULL; int err = 0; - + int clear_owner = 0; Py_BEGIN_CRITICAL_SECTION(globals); int found = _PyDict_GetItemRef_Unicode_LockHeld( (PyDictObject *)globals, name, ¤t); if (found < 0) { err = -1; } - else if (found > 0 && current == op) { - err = _PyDict_SetItem_LockHeld((PyDictObject *)globals, name, value); + else { + // Stop tracking the owner after a successful reification, so a stale + // proxy cannot replace a future rebinding. + clear_owner = 1; + if (found > 0 && current == op) { + err = _PyDict_SetItem_LockHeld( + (PyDictObject *)globals, name, value); + clear_owner = (err == 0); + } } - Py_XDECREF(current); Py_END_CRITICAL_SECTION(); + Py_XDECREF(current); - if (err == 0) { + if (clear_owner) { clear_owner_if_matches(tstate, PyLazyImportObject_CAST(op), globals, name); } @@ -211,6 +224,8 @@ _PyLazyImport_CommitStoredIfCurrent(PyThreadState *tstate, PyObject *op, PyObject *name = NULL; PyInterpreterState *interp = tstate->interp; + // Copy the owner under the import lock, then release it before entering a + // dict critical section in _PyLazyImport_CommitIfCurrent(). _PyImport_AcquireLock(interp); if (m->lz_owner_globals != NULL && m->lz_owner_key != NULL) { globals = Py_NewRef(m->lz_owner_globals); @@ -246,7 +261,7 @@ lazy_import_resolve(PyObject *self, PyObject *args) static PyMethodDef lazy_import_methods[] = { { "resolve", lazy_import_resolve, METH_NOARGS, - PyDoc_STR("resolves the lazy import and returns the actual object") + PyDoc_STR("resolve the lazy import and return the actual object") }, {NULL, NULL} }; @@ -258,9 +273,9 @@ PyDoc_STRVAR(lazy_import_doc, "\n" "Represents a lazy import that will be resolved on first use.\n" "\n" -"Instances of this object accessed from the global scope will be\n" -"automatically imported based upon their name and then replaced with\n" -"the imported value."); +"A successful resolution is cached. Instances of this object accessed\n" +"from the global scope will be automatically imported based upon their\n" +"name and then replaced with the imported value."); PyTypeObject PyLazyImport_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) From 8250d82e70fc33a79016ba44acbaf4e59faa79be Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 09:58:28 +0800 Subject: [PATCH 05/14] gh-152298: Bind lazy owners after global stores --- Modules/_testinternalcapi/test_cases.c.h | 26 +++++++++--------- Python/bytecodes.c | 10 ++++--- Python/executor_cases.c.h | 34 ++++++++++-------------- Python/generated_cases.c.h | 26 +++++++++--------- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 826e4eaa5ce5b8..e99f7f2deb4c0c 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -12626,16 +12626,16 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (PyLazyImport_CheckExact(value)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); - _PyFrame_StackPointerInvalidate(frame); - } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && PyLazyImport_CheckExact(value)) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -12677,16 +12677,18 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); - _PyFrame_StackPointerInvalidate(frame); - } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } } else { _PyFrame_SetStackPointer(frame, stack_pointer); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 95e9995a09e971..0ca1d4c096626d 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2006,10 +2006,12 @@ dummy_func( } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + err = PyDict_SetItem(ns, name, value); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { _PyLazyImport_BindGlobal(tstate, value, ns, name); } - err = PyDict_SetItem(ns, name, value); } else { err = PyObject_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); @@ -2194,10 +2196,10 @@ dummy_func( inst(STORE_GLOBAL, (v --)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (PyLazyImport_CheckExact(value)) { + int err = PyDict_SetItem(GLOBALS(), name, value); + if (err == 0 && PyLazyImport_CheckExact(value)) { _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); } - int err = PyDict_SetItem(GLOBALS(), name, value); PyStackRef_CLOSE(v); ERROR_IF(err); } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 89cfa2e1118a36..bb33d37a470386 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9588,16 +9588,6 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { - stack_pointer[0] = v; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); - _PyFrame_StackPointerInvalidate(frame); - stack_pointer += -1; - } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -9605,6 +9595,14 @@ _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } } else { stack_pointer[0] = v; @@ -10088,16 +10086,6 @@ v = _stack_item_0; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (PyLazyImport_CheckExact(value)) { - stack_pointer[0] = v; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); - _PyFrame_StackPointerInvalidate(frame); - stack_pointer += -1; - } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -10105,6 +10093,12 @@ _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && PyLazyImport_CheckExact(value)) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index ebe45c9e2e24f4..57af619055ff31 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -12623,16 +12623,16 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (PyLazyImport_CheckExact(value)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); - _PyFrame_StackPointerInvalidate(frame); - } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && PyLazyImport_CheckExact(value)) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -12674,16 +12674,18 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); - _PyFrame_StackPointerInvalidate(frame); - } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } } else { _PyFrame_SetStackPointer(frame, stack_pointer); From fc4a2329a3af37b7f153df09ed5f4aaf872e00f4 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 10:01:20 +0800 Subject: [PATCH 06/14] gh-152298: Bind lazy import owner before storing --- Modules/_testinternalcapi/test_cases.c.h | 26 +++++++++--------- Python/bytecodes.c | 10 +++---- Python/executor_cases.c.h | 34 ++++++++++++++---------- Python/generated_cases.c.h | 26 +++++++++--------- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index e99f7f2deb4c0c..826e4eaa5ce5b8 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -12626,16 +12626,16 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, value); - _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && PyLazyImport_CheckExact(value)) { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + if (PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); _PyFrame_StackPointerInvalidate(frame); } + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + int err = PyDict_SetItem(GLOBALS(), name, value); + _PyFrame_StackPointerInvalidate(frame); stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -12677,18 +12677,16 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, value); - _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); _PyLazyImport_BindGlobal(tstate, value, ns, name); _PyFrame_StackPointerInvalidate(frame); } + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + err = PyDict_SetItem(ns, name, value); + _PyFrame_StackPointerInvalidate(frame); } else { _PyFrame_SetStackPointer(frame, stack_pointer); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 0ca1d4c096626d..95e9995a09e971 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2006,12 +2006,10 @@ dummy_func( } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - err = PyDict_SetItem(ns, name, value); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { _PyLazyImport_BindGlobal(tstate, value, ns, name); } + err = PyDict_SetItem(ns, name, value); } else { err = PyObject_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); @@ -2196,10 +2194,10 @@ dummy_func( inst(STORE_GLOBAL, (v --)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - int err = PyDict_SetItem(GLOBALS(), name, value); - if (err == 0 && PyLazyImport_CheckExact(value)) { + if (PyLazyImport_CheckExact(value)) { _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); } + int err = PyDict_SetItem(GLOBALS(), name, value); PyStackRef_CLOSE(v); ERROR_IF(err); } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index bb33d37a470386..89cfa2e1118a36 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9588,6 +9588,16 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + stack_pointer += -1; + } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -9595,14 +9605,6 @@ _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); - _PyFrame_StackPointerInvalidate(frame); - } } else { stack_pointer[0] = v; @@ -10086,6 +10088,16 @@ v = _stack_item_0; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); + if (PyLazyImport_CheckExact(value)) { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + stack_pointer += -1; + } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -10093,12 +10105,6 @@ _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && PyLazyImport_CheckExact(value)) { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); - _PyFrame_StackPointerInvalidate(frame); - } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 57af619055ff31..ebe45c9e2e24f4 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -12623,16 +12623,16 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - int err = PyDict_SetItem(GLOBALS(), name, value); - _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && PyLazyImport_CheckExact(value)) { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + if (PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); _PyFrame_StackPointerInvalidate(frame); } + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + int err = PyDict_SetItem(GLOBALS(), name, value); + _PyFrame_StackPointerInvalidate(frame); stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -12674,18 +12674,16 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - err = PyDict_SetItem(ns, name, value); - _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { - assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); _PyLazyImport_BindGlobal(tstate, value, ns, name); _PyFrame_StackPointerInvalidate(frame); } + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + err = PyDict_SetItem(ns, name, value); + _PyFrame_StackPointerInvalidate(frame); } else { _PyFrame_SetStackPointer(frame, stack_pointer); From 61614b0b584752ddba39bcc474a9c4a6e5101d09 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 10:12:18 +0800 Subject: [PATCH 07/14] gh-152298: Restore post-store lazy owner binding --- Modules/_testinternalcapi/test_cases.c.h | 26 +++++++++--------- Python/bytecodes.c | 10 ++++--- Python/executor_cases.c.h | 34 ++++++++++-------------- Python/generated_cases.c.h | 26 +++++++++--------- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 826e4eaa5ce5b8..e99f7f2deb4c0c 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -12626,16 +12626,16 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (PyLazyImport_CheckExact(value)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); - _PyFrame_StackPointerInvalidate(frame); - } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && PyLazyImport_CheckExact(value)) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -12677,16 +12677,18 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); - _PyFrame_StackPointerInvalidate(frame); - } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } } else { _PyFrame_SetStackPointer(frame, stack_pointer); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 95e9995a09e971..0ca1d4c096626d 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2006,10 +2006,12 @@ dummy_func( } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + err = PyDict_SetItem(ns, name, value); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { _PyLazyImport_BindGlobal(tstate, value, ns, name); } - err = PyDict_SetItem(ns, name, value); } else { err = PyObject_SetItem(ns, name, PyStackRef_AsPyObjectBorrow(v)); @@ -2194,10 +2196,10 @@ dummy_func( inst(STORE_GLOBAL, (v --)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (PyLazyImport_CheckExact(value)) { + int err = PyDict_SetItem(GLOBALS(), name, value); + if (err == 0 && PyLazyImport_CheckExact(value)) { _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); } - int err = PyDict_SetItem(GLOBALS(), name, value); PyStackRef_CLOSE(v); ERROR_IF(err); } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 89cfa2e1118a36..bb33d37a470386 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9588,16 +9588,6 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { - stack_pointer[0] = v; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); - _PyFrame_StackPointerInvalidate(frame); - stack_pointer += -1; - } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -9605,6 +9595,14 @@ _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } } else { stack_pointer[0] = v; @@ -10088,16 +10086,6 @@ v = _stack_item_0; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (PyLazyImport_CheckExact(value)) { - stack_pointer[0] = v; - stack_pointer += 1; - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); - _PyFrame_StackPointerInvalidate(frame); - stack_pointer += -1; - } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -10105,6 +10093,12 @@ _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && PyLazyImport_CheckExact(value)) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index ebe45c9e2e24f4..57af619055ff31 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -12623,16 +12623,16 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (PyLazyImport_CheckExact(value)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); - _PyFrame_StackPointerInvalidate(frame); - } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && PyLazyImport_CheckExact(value)) { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); @@ -12674,16 +12674,18 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); - if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { - _PyFrame_SetStackPointer(frame, stack_pointer); - _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); - _PyFrame_StackPointerInvalidate(frame); - } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); + if (err == 0 && ns == GLOBALS() && + PyLazyImport_CheckExact(value)) + { + assert(stack_pointer == _PyFrame_GetStackPointer(frame)); + _PyFrame_StackPointerValidate(frame); + _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } } else { _PyFrame_SetStackPointer(frame, stack_pointer); From 33d3adec7fd2c9914bc5019150a86b3884918f0f Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 10:32:25 +0800 Subject: [PATCH 08/14] gh-152298: Protect pending lazy owner bindings --- Include/internal/pycore_lazyimportobject.h | 15 ++- Lib/test/test_lazy_import/__init__.py | 64 +++++++---- Modules/_testinternalcapi/test_cases.c.h | 26 +++-- Objects/lazyimportobject.c | 118 +++++++++++++++++---- Python/bytecodes.c | 20 ++-- Python/executor_cases.c.h | 34 ++++-- Python/generated_cases.c.h | 26 +++-- 7 files changed, 232 insertions(+), 71 deletions(-) diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index de42e9ff9c52ea..93617e65c5aed2 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -19,13 +19,16 @@ typedef struct { PyObject *lz_builtins; PyObject *lz_from; PyObject *lz_attr; - // Protected by the import lock. The owner fields remember the first - // globals/key pair until resolution succeeds or the proxy is cleared. + // Protected by the import lock. lz_owner_* records the first module + // global that resolve() may replace. Pending owners are being stored; + // finalized owners cannot be claimed again. PyObject *lz_owner_globals; PyObject *lz_owner_key; PyObject *lz_resolved; // Frame information for the original import location. PyCodeObject *lz_code; // Code object where the lazy import was created. + int lz_owner_pending; + int lz_owner_finalized; int lz_instr_offset; // Instruction offset where the lazy import was created. } PyLazyImportObject; @@ -33,11 +36,17 @@ typedef struct { PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import); PyAPI_FUNC(PyObject *) _PyLazyImport_New( struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr); -PyAPI_FUNC(void) _PyLazyImport_BindGlobal( +PyAPI_FUNC(int) _PyLazyImport_BindGlobal( PyThreadState *tstate, PyObject *lazy_import, PyObject *globals, PyObject *name); +PyAPI_FUNC(void) _PyLazyImport_FinishGlobalBinding( + PyThreadState *tstate, + PyObject *lazy_import, + PyObject *globals, + PyObject *name, + int stored); PyAPI_FUNC(int) _PyLazyImport_CommitIfCurrent( PyThreadState *tstate, PyObject *lazy_import, diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 1f766465eccedb..bd637873fd3af4 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -585,11 +585,6 @@ def test_module_attr_dynamic_equal_name_clears_owner(self): module = sys.modules[__name__] lazy import target_module as target - proxy = globals()["target"] - stored_name = next(name for name in globals() if name == "target") - dynamic_name = bytearray(b"target").decode() - assert dynamic_name == stored_name - assert dynamic_name is not stored_name def custom_import(name, globals=None, locals=None, fromlist=None, level=0): @@ -599,8 +594,14 @@ def custom_import(name, globals=None, locals=None, fromlist=None, return resolved return real_import(name, globals, locals, fromlist, level) - builtins.__import__ = custom_import - try: + def check(): + proxy = globals()["target"] + stored_name = next(name for name in globals() + if name == "target") + dynamic_name = bytearray(b"target").decode() + assert dynamic_name == stored_name + assert dynamic_name is not stored_name + resolved = getattr(module, dynamic_name) assert resolved.VALUE == "resolved" assert module.__dict__["target"] is resolved @@ -609,6 +610,10 @@ def custom_import(name, globals=None, locals=None, fromlist=None, again = proxy.resolve() assert again is resolved assert module.__dict__["target"] is proxy + + builtins.__import__ = custom_import + try: + check() finally: builtins.__import__ = real_import """) @@ -624,7 +629,6 @@ def test_rebound_back_to_same_proxy_after_stale_resolve_does_not_replace(self): calls = [] lazy import target_module as target - proxy = globals()["target"] def custom_import(name, globals=None, locals=None, fromlist=None, level=0): @@ -635,8 +639,8 @@ def custom_import(name, globals=None, locals=None, fromlist=None, return resolved return real_import(name, globals, locals, fromlist, level) - builtins.__import__ = custom_import - try: + def check(): + proxy = globals()["target"] sentinel = object() globals()["target"] = sentinel resolved = proxy.resolve() @@ -646,6 +650,10 @@ def custom_import(name, globals=None, locals=None, fromlist=None, again = proxy.resolve() assert again is resolved assert globals()["target"] is proxy + + builtins.__import__ = custom_import + try: + check() assert len(calls) == 1, calls finally: builtins.__import__ = real_import @@ -661,7 +669,6 @@ def test_resolve_replaces_unaliased_import_global(self): real_import = builtins.__import__ lazy import target_module - proxy = globals()["target_module"] def custom_import(name, globals=None, locals=None, fromlist=None, level=0): @@ -671,11 +678,15 @@ def custom_import(name, globals=None, locals=None, fromlist=None, return resolved return real_import(name, globals, locals, fromlist, level) - builtins.__import__ = custom_import - try: + def check(): + proxy = globals()["target_module"] resolved = proxy.resolve() assert resolved.VALUE == "resolved" assert globals()["target_module"] is resolved + + builtins.__import__ = custom_import + try: + check() finally: builtins.__import__ = real_import """) @@ -687,13 +698,16 @@ def test_resolve_replaces_unaliased_dotted_import_binding(self): import types lazy import test.test_lazy_import.data.pkg.bar - proxy = globals()["test"] - resolved = proxy.resolve() - assert isinstance(resolved, types.ModuleType) - assert resolved.__name__ == "test" - assert globals()["test"] is resolved - assert "test.test_lazy_import.data.pkg" not in globals() + def check(): + proxy = globals()["test"] + resolved = proxy.resolve() + assert isinstance(resolved, types.ModuleType) + assert resolved.__name__ == "test" + assert globals()["test"] is resolved + assert "test.test_lazy_import.data.pkg" not in globals() + + check() """) assert_python_ok("-c", code) @@ -707,8 +721,6 @@ def test_copying_lazy_proxy_to_second_global_keeps_first_owner(self): calls = [] lazy import target_module as primary - secondary = globals()["primary"] - proxy = globals()["primary"] def custom_import(name, globals=None, locals=None, fromlist=None, level=0): @@ -719,8 +731,10 @@ def custom_import(name, globals=None, locals=None, fromlist=None, return resolved return real_import(name, globals, locals, fromlist, level) - builtins.__import__ = custom_import - try: + def check(): + proxy = globals()["primary"] + globals()["secondary"] = proxy + resolved = proxy.resolve() assert globals()["primary"] is resolved assert globals()["secondary"] is proxy @@ -728,6 +742,10 @@ def custom_import(name, globals=None, locals=None, fromlist=None, again = globals()["secondary"].resolve() assert again is resolved assert globals()["secondary"] is proxy + + builtins.__import__ = custom_import + try: + check() assert len(calls) == 1, calls finally: builtins.__import__ = real_import diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index e99f7f2deb4c0c..3498f093c65817 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -12626,14 +12626,22 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int bound = 0; + if (PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + bound = _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && PyLazyImport_CheckExact(value)) { + if (bound) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyLazyImport_FinishGlobalBinding( + tstate, value, GLOBALS(), name, err == 0); _PyFrame_StackPointerInvalidate(frame); } stack_pointer += -1; @@ -12677,16 +12685,22 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int bound = 0; + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + bound = _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { + if (bound) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyLazyImport_FinishGlobalBinding( + tstate, value, ns, name, err == 0); _PyFrame_StackPointerInvalidate(frame); } } diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index 6e8c716d066aa1..f2add31b0c9b89 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -39,6 +39,8 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name m->lz_owner_globals = NULL; m->lz_owner_key = NULL; m->lz_resolved = NULL; + m->lz_owner_pending = 0; + m->lz_owner_finalized = 0; // Capture frame information for the original import location. m->lz_code = NULL; @@ -81,6 +83,8 @@ lazy_import_clear(PyObject *op) Py_CLEAR(m->lz_owner_globals); Py_CLEAR(m->lz_owner_key); Py_CLEAR(m->lz_resolved); + m->lz_owner_pending = 0; + m->lz_owner_finalized = 0; Py_CLEAR(m->lz_code); return 0; } @@ -128,7 +132,7 @@ _PyLazyImport_GetName(PyObject *op) return lazy_import_name(lazy_import); } -void +int _PyLazyImport_BindGlobal(PyThreadState *tstate, PyObject *op, PyObject *globals, PyObject *name) { @@ -136,22 +140,35 @@ _PyLazyImport_BindGlobal(PyThreadState *tstate, PyObject *op, !PyDict_CheckExact(globals) || !PyUnicode_CheckExact(name)) { - return; + return 0; } - PyLazyImportObject *m = PyLazyImportObject_CAST(op); + int bound = 0; PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); // First owner wins: copies of the proxy do not change the original global // binding that resolve() may replace. if (m->lz_resolved == NULL && + !m->lz_owner_pending && + !m->lz_owner_finalized && m->lz_owner_globals == NULL && m->lz_owner_key == NULL) { m->lz_owner_globals = Py_NewRef(globals); m->lz_owner_key = Py_NewRef(name); + m->lz_owner_pending = 1; + bound = 1; } _PyImport_ReleaseLock(interp); + return bound; +} + +static int +owner_matches(PyLazyImportObject *m, PyObject *globals, PyObject *name) +{ + return (m->lz_owner_globals == globals && + m->lz_owner_key != NULL && + _PyUnicode_Equal(m->lz_owner_key, name)); } static void @@ -160,16 +177,72 @@ clear_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, { PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); - if (m->lz_owner_globals == globals && - m->lz_owner_key != NULL && - _PyUnicode_Equal(m->lz_owner_key, name)) - { + if (owner_matches(m, globals, name)) { + m->lz_owner_pending = 0; Py_CLEAR(m->lz_owner_globals); Py_CLEAR(m->lz_owner_key); } _PyImport_ReleaseLock(interp); } +void +_PyLazyImport_FinishGlobalBinding(PyThreadState *tstate, PyObject *op, + PyObject *globals, PyObject *name, + int stored) +{ + if (!PyLazyImport_CheckExact(op) || + !PyDict_CheckExact(globals) || + !PyUnicode_CheckExact(name)) + { + return; + } + + PyLazyImportObject *m = PyLazyImportObject_CAST(op); + PyInterpreterState *interp = tstate->interp; + _PyImport_AcquireLock(interp); + if (m->lz_owner_pending && owner_matches(m, globals, name)) { + if (stored) { + m->lz_owner_pending = 0; + } + else { + m->lz_owner_pending = 0; + Py_CLEAR(m->lz_owner_globals); + Py_CLEAR(m->lz_owner_key); + } + } + _PyImport_ReleaseLock(interp); +} + +static int +finalize_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, + PyObject *globals, PyObject *name) +{ + int finalized = 0; + PyInterpreterState *interp = tstate->interp; + _PyImport_AcquireLock(interp); + if (!m->lz_owner_pending && + !m->lz_owner_finalized && + owner_matches(m, globals, name)) + { + m->lz_owner_finalized = 1; + finalized = 1; + } + _PyImport_ReleaseLock(interp); + return finalized; +} + +static void +restore_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, + PyObject *globals, PyObject *name) +{ + PyInterpreterState *interp = tstate->interp; + _PyImport_AcquireLock(interp); + if (m->lz_owner_finalized && owner_matches(m, globals, name)) { + m->lz_owner_finalized = 0; + } + _PyImport_ReleaseLock(interp); +} + int _PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op, PyObject *globals, PyObject *name, @@ -182,31 +255,30 @@ _PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op, return 0; } + PyLazyImportObject *m = PyLazyImportObject_CAST(op); + int finalized_owner = finalize_owner_if_matches(tstate, m, globals, name); + PyObject *current = NULL; int err = 0; - int clear_owner = 0; Py_BEGIN_CRITICAL_SECTION(globals); int found = _PyDict_GetItemRef_Unicode_LockHeld( (PyDictObject *)globals, name, ¤t); if (found < 0) { err = -1; } - else { - // Stop tracking the owner after a successful reification, so a stale - // proxy cannot replace a future rebinding. - clear_owner = 1; - if (found > 0 && current == op) { - err = _PyDict_SetItem_LockHeld( - (PyDictObject *)globals, name, value); - clear_owner = (err == 0); - } + else if (found > 0 && current == op) { + err = _PyDict_SetItem_LockHeld((PyDictObject *)globals, name, value); } Py_END_CRITICAL_SECTION(); Py_XDECREF(current); - if (clear_owner) { - clear_owner_if_matches(tstate, PyLazyImportObject_CAST(op), - globals, name); + if (finalized_owner) { + if (err < 0) { + restore_owner_if_matches(tstate, m, globals, name); + } + else { + clear_owner_if_matches(tstate, m, globals, name); + } } return err; } @@ -227,7 +299,11 @@ _PyLazyImport_CommitStoredIfCurrent(PyThreadState *tstate, PyObject *op, // Copy the owner under the import lock, then release it before entering a // dict critical section in _PyLazyImport_CommitIfCurrent(). _PyImport_AcquireLock(interp); - if (m->lz_owner_globals != NULL && m->lz_owner_key != NULL) { + if (!m->lz_owner_pending && + !m->lz_owner_finalized && + m->lz_owner_globals != NULL && + m->lz_owner_key != NULL) + { globals = Py_NewRef(m->lz_owner_globals); name = Py_NewRef(m->lz_owner_key); } diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 0ca1d4c096626d..96546a20574c05 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2006,11 +2006,14 @@ dummy_func( } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int bound = 0; + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + bound = _PyLazyImport_BindGlobal(tstate, value, ns, name); + } err = PyDict_SetItem(ns, name, value); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { - _PyLazyImport_BindGlobal(tstate, value, ns, name); + if (bound) { + _PyLazyImport_FinishGlobalBinding( + tstate, value, ns, name, err == 0); } } else { @@ -2196,9 +2199,14 @@ dummy_func( inst(STORE_GLOBAL, (v --)) { PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int bound = 0; + if (PyLazyImport_CheckExact(value)) { + bound = _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + } int err = PyDict_SetItem(GLOBALS(), name, value); - if (err == 0 && PyLazyImport_CheckExact(value)) { - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + if (bound) { + _PyLazyImport_FinishGlobalBinding( + tstate, value, GLOBALS(), name, err == 0); } PyStackRef_CLOSE(v); ERROR_IF(err); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index bb33d37a470386..2fa3a91bec8092 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9588,6 +9588,17 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int bound = 0; + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + bound = _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + stack_pointer += -1; + } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -9595,12 +9606,11 @@ _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { + if (bound) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyLazyImport_FinishGlobalBinding( + tstate, value, ns, name, err == 0); _PyFrame_StackPointerInvalidate(frame); } } @@ -10086,6 +10096,17 @@ v = _stack_item_0; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int bound = 0; + if (PyLazyImport_CheckExact(value)) { + stack_pointer[0] = v; + stack_pointer += 1; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + bound = _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + stack_pointer += -1; + } stack_pointer[0] = v; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); @@ -10093,10 +10114,11 @@ _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && PyLazyImport_CheckExact(value)) { + if (bound) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyLazyImport_FinishGlobalBinding( + tstate, value, GLOBALS(), name, err == 0); _PyFrame_StackPointerInvalidate(frame); } stack_pointer += -1; diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 57af619055ff31..b5ee412e4e073e 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -12623,14 +12623,22 @@ v = stack_pointer[-1]; PyObject *name = GETITEM(FRAME_CO_NAMES, oparg); PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int bound = 0; + if (PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + bound = _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyFrame_StackPointerInvalidate(frame); + } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); int err = PyDict_SetItem(GLOBALS(), name, value); _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && PyLazyImport_CheckExact(value)) { + if (bound) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, GLOBALS(), name); + _PyLazyImport_FinishGlobalBinding( + tstate, value, GLOBALS(), name, err == 0); _PyFrame_StackPointerInvalidate(frame); } stack_pointer += -1; @@ -12674,16 +12682,22 @@ } if (PyDict_CheckExact(ns)) { PyObject *value = PyStackRef_AsPyObjectBorrow(v); + int bound = 0; + if (ns == GLOBALS() && PyLazyImport_CheckExact(value)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + bound = _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyFrame_StackPointerInvalidate(frame); + } _PyFrame_SetStackPointer(frame, stack_pointer); _PyFrame_StackPointerValidate(frame); err = PyDict_SetItem(ns, name, value); _PyFrame_StackPointerInvalidate(frame); - if (err == 0 && ns == GLOBALS() && - PyLazyImport_CheckExact(value)) - { + if (bound) { assert(stack_pointer == _PyFrame_GetStackPointer(frame)); _PyFrame_StackPointerValidate(frame); - _PyLazyImport_BindGlobal(tstate, value, ns, name); + _PyLazyImport_FinishGlobalBinding( + tstate, value, ns, name, err == 0); _PyFrame_StackPointerInvalidate(frame); } } From 2b013d898d521b787de67a424ae6081fb6819387 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 10:35:27 +0800 Subject: [PATCH 09/14] gh-152298: Finalize pending lazy owners on commit --- Objects/lazyimportobject.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index f2add31b0c9b89..e9bcd761c6be8d 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -220,8 +220,7 @@ finalize_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, int finalized = 0; PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); - if (!m->lz_owner_pending && - !m->lz_owner_finalized && + if (!m->lz_owner_finalized && owner_matches(m, globals, name)) { m->lz_owner_finalized = 1; From a97cc10075a10a0241cdf8e3c5c1aee58be70c11 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 10:48:15 +0800 Subject: [PATCH 10/14] gh-152298: Keep pending lazy owners unfinalized --- Objects/lazyimportobject.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index e9bcd761c6be8d..f2add31b0c9b89 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -220,7 +220,8 @@ finalize_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, int finalized = 0; PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); - if (!m->lz_owner_finalized && + if (!m->lz_owner_pending && + !m->lz_owner_finalized && owner_matches(m, globals, name)) { m->lz_owner_finalized = 1; From a3467058de516027e62ad37c28505841ad9ede53 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 10:57:07 +0800 Subject: [PATCH 11/14] gh-152298: Cover reentrant lazy owner finalization --- Doc/library/types.rst | 8 ++-- Include/internal/pycore_lazyimportobject.h | 2 +- Lib/test/test_lazy_import/__init__.py | 52 +++++++++++++++++++++- Objects/lazyimportobject.c | 12 ++--- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/Doc/library/types.rst b/Doc/library/types.rst index af3fe47c181776..7e34d541d84cbe 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -355,10 +355,10 @@ Standard names are defined for the following types: Resolve the lazy import and return the imported object. Successful resolutions are cached. - For module-level lazy imports, ``resolve()`` replaces the original global - only while it still refers to this proxy. If that binding was deleted or - rebound, it is left unchanged and later ``resolve()`` calls on the same - proxy do not update it. + For a module-level lazy import, ``resolve()`` also updates the original + module global if that name still refers to this proxy. If the name was + deleted or rebound, it is left unchanged; later ``resolve()`` calls return + the cached object without updating that name. .. versionadded:: 3.15 diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index 93617e65c5aed2..b588566cfcad02 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -21,7 +21,7 @@ typedef struct { PyObject *lz_attr; // Protected by the import lock. lz_owner_* records the first module // global that resolve() may replace. Pending owners are being stored; - // finalized owners cannot be claimed again. + // pending and finalized may overlap during reentrant store resolution. PyObject *lz_owner_globals; PyObject *lz_owner_key; PyObject *lz_resolved; diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index bd637873fd3af4..3099d67d9338c4 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -732,8 +732,8 @@ def custom_import(name, globals=None, locals=None, fromlist=None, return real_import(name, globals, locals, fromlist, level) def check(): - proxy = globals()["primary"] - globals()["secondary"] = proxy + exec('secondary = globals()["primary"]', globals()) + proxy = globals()["secondary"] resolved = proxy.resolve() assert globals()["primary"] is resolved @@ -752,6 +752,54 @@ def check(): """) assert_python_ok("-c", code) + @support.requires_subprocess() + def test_reentrant_store_resolution_clears_pending_owner(self): + code = textwrap.dedent(""" + import builtins + import types + + real_import = builtins.__import__ + calls = [] + errors = [] + proxies = [] + + def custom_import(name, globals=None, locals=None, fromlist=None, + level=0): + if name == "target_module": + calls.append(name) + resolved = types.ModuleType(name) + resolved.VALUE = "resolved" + return resolved + return real_import(name, globals, locals, fromlist, level) + + class Reenter: + def __del__(self): + try: + proxies.append(globals()["target"]) + assert target.VALUE == "resolved" + except BaseException as exc: + errors.append((type(exc).__name__, str(exc))) + + builtins.__import__ = custom_import + target = Reenter() + lazy import target_module as target + + try: + assert not errors, errors + assert len(proxies) == 1, proxies + resolved = globals()["target"] + proxy = proxies[0] + assert resolved.VALUE == "resolved" + + globals()["target"] = proxy + assert proxy.resolve() is resolved + assert globals()["target"] is proxy + assert len(calls) == 1, calls + finally: + builtins.__import__ = real_import + """) + assert_python_ok("-c", code) + class SyntaxRestrictionTests(LazyImportTestCase): """Tests for syntax restrictions on lazy imports.""" diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index f2add31b0c9b89..dd0c19119ef741 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -201,11 +201,8 @@ _PyLazyImport_FinishGlobalBinding(PyThreadState *tstate, PyObject *op, PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); if (m->lz_owner_pending && owner_matches(m, globals, name)) { - if (stored) { - m->lz_owner_pending = 0; - } - else { - m->lz_owner_pending = 0; + m->lz_owner_pending = 0; + if (!stored) { Py_CLEAR(m->lz_owner_globals); Py_CLEAR(m->lz_owner_key); } @@ -220,10 +217,7 @@ finalize_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, int finalized = 0; PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); - if (!m->lz_owner_pending && - !m->lz_owner_finalized && - owner_matches(m, globals, name)) - { + if (!m->lz_owner_finalized && owner_matches(m, globals, name)) { m->lz_owner_finalized = 1; finalized = 1; } From c9c34412fc81e58bd0118dfc54b7a60316dc5df0 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 11:08:31 +0800 Subject: [PATCH 12/14] gh-152298: Snapshot lazy proxy in reentrant test --- Lib/test/test_lazy_import/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 3099d67d9338c4..5a96486486e046 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -775,7 +775,7 @@ def custom_import(name, globals=None, locals=None, fromlist=None, class Reenter: def __del__(self): try: - proxies.append(globals()["target"]) + proxies.append(globals().copy()["target"]) assert target.VALUE == "resolved" except BaseException as exc: errors.append((type(exc).__name__, str(exc))) From 5301ae60edf16053fb34296aa94673057ccbac5a Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 11:11:45 +0800 Subject: [PATCH 13/14] gh-152298: Require owner claim for explicit lazy resolve --- Include/internal/pycore_lazyimportobject.h | 2 +- Lib/test/test_lazy_import/__init__.py | 14 ++- Objects/lazyimportobject.c | 105 +++++++++++++++------ 3 files changed, 85 insertions(+), 36 deletions(-) diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h index b588566cfcad02..93617e65c5aed2 100644 --- a/Include/internal/pycore_lazyimportobject.h +++ b/Include/internal/pycore_lazyimportobject.h @@ -21,7 +21,7 @@ typedef struct { PyObject *lz_attr; // Protected by the import lock. lz_owner_* records the first module // global that resolve() may replace. Pending owners are being stored; - // pending and finalized may overlap during reentrant store resolution. + // finalized owners cannot be claimed again. PyObject *lz_owner_globals; PyObject *lz_owner_key; PyObject *lz_resolved; diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 5a96486486e046..4eb6fffa0493e9 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -753,7 +753,7 @@ def check(): assert_python_ok("-c", code) @support.requires_subprocess() - def test_reentrant_store_resolution_clears_pending_owner(self): + def test_reentrant_store_resolution_waits_for_store_finish(self): code = textwrap.dedent(""" import builtins import types @@ -775,8 +775,11 @@ def custom_import(name, globals=None, locals=None, fromlist=None, class Reenter: def __del__(self): try: - proxies.append(globals().copy()["target"]) - assert target.VALUE == "resolved" + proxy = globals().copy()["target"] + proxies.append(proxy) + resolved = target + assert resolved.VALUE == "resolved" + assert globals()["target"] is proxy except BaseException as exc: errors.append((type(exc).__name__, str(exc))) @@ -787,9 +790,12 @@ def __del__(self): try: assert not errors, errors assert len(proxies) == 1, proxies - resolved = globals()["target"] proxy = proxies[0] + assert globals()["target"] is proxy + + resolved = proxy.resolve() assert resolved.VALUE == "resolved" + assert globals()["target"] is resolved globals()["target"] = proxy assert proxy.resolve() is resolved diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index dd0c19119ef741..a070c45bfd4c60 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -179,6 +179,8 @@ clear_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, _PyImport_AcquireLock(interp); if (owner_matches(m, globals, name)) { m->lz_owner_pending = 0; + // Keep lz_owner_finalized set: this owner has been committed or + // abandoned, so later stores of the same proxy are not canonical. Py_CLEAR(m->lz_owner_globals); Py_CLEAR(m->lz_owner_key); } @@ -217,7 +219,10 @@ finalize_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, int finalized = 0; PyInterpreterState *interp = tstate->interp; _PyImport_AcquireLock(interp); - if (!m->lz_owner_finalized && owner_matches(m, globals, name)) { + if (!m->lz_owner_pending && + !m->lz_owner_finalized && + owner_matches(m, globals, name)) + { m->lz_owner_finalized = 1; finalized = 1; } @@ -237,21 +242,45 @@ restore_owner_if_matches(PyThreadState *tstate, PyLazyImportObject *m, _PyImport_ReleaseLock(interp); } -int -_PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op, - PyObject *globals, PyObject *name, - PyObject *value) +static int +owner_is_pending(PyThreadState *tstate, PyLazyImportObject *m, + PyObject *globals, PyObject *name) { - if (!PyLazyImport_CheckExact(op) || - !PyDict_CheckExact(globals) || - !PyUnicode_CheckExact(name)) - { - return 0; + int pending = 0; + PyInterpreterState *interp = tstate->interp; + _PyImport_AcquireLock(interp); + if (m->lz_owner_pending && owner_matches(m, globals, name)) { + pending = 1; } + _PyImport_ReleaseLock(interp); + return pending; +} - PyLazyImportObject *m = PyLazyImportObject_CAST(op); - int finalized_owner = finalize_owner_if_matches(tstate, m, globals, name); +static int +claim_stored_owner(PyThreadState *tstate, PyLazyImportObject *m, + PyObject **globals, PyObject **name) +{ + int claimed = 0; + PyInterpreterState *interp = tstate->interp; + _PyImport_AcquireLock(interp); + if (!m->lz_owner_pending && + !m->lz_owner_finalized && + m->lz_owner_globals != NULL && + m->lz_owner_key != NULL) + { + m->lz_owner_finalized = 1; + *globals = Py_NewRef(m->lz_owner_globals); + *name = Py_NewRef(m->lz_owner_key); + claimed = 1; + } + _PyImport_ReleaseLock(interp); + return claimed; +} +static int +commit_if_current(PyObject *op, PyObject *globals, PyObject *name, + PyObject *value) +{ PyObject *current = NULL; int err = 0; Py_BEGIN_CRITICAL_SECTION(globals); @@ -265,7 +294,28 @@ _PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op, } Py_END_CRITICAL_SECTION(); Py_XDECREF(current); + return err; +} +int +_PyLazyImport_CommitIfCurrent(PyThreadState *tstate, PyObject *op, + PyObject *globals, PyObject *name, + PyObject *value) +{ + if (!PyLazyImport_CheckExact(op) || + !PyDict_CheckExact(globals) || + !PyUnicode_CheckExact(name)) + { + return 0; + } + + PyLazyImportObject *m = PyLazyImportObject_CAST(op); + if (owner_is_pending(tstate, m, globals, name)) { + return 0; + } + + int finalized_owner = finalize_owner_if_matches(tstate, m, globals, name); + int err = commit_if_current(op, globals, name, value); if (finalized_owner) { if (err < 0) { restore_owner_if_matches(tstate, m, globals, name); @@ -289,27 +339,19 @@ _PyLazyImport_CommitStoredIfCurrent(PyThreadState *tstate, PyObject *op, PyObject *globals = NULL; PyObject *name = NULL; - PyInterpreterState *interp = tstate->interp; - // Copy the owner under the import lock, then release it before entering a - // dict critical section in _PyLazyImport_CommitIfCurrent(). - _PyImport_AcquireLock(interp); - if (!m->lz_owner_pending && - !m->lz_owner_finalized && - m->lz_owner_globals != NULL && - m->lz_owner_key != NULL) - { - globals = Py_NewRef(m->lz_owner_globals); - name = Py_NewRef(m->lz_owner_key); + if (!claim_stored_owner(tstate, m, &globals, &name)) { + return 0; } - _PyImport_ReleaseLock(interp); - int err = 0; - if (globals != NULL && name != NULL) { - err = _PyLazyImport_CommitIfCurrent( - tstate, op, globals, name, value); + int err = commit_if_current(op, globals, name, value); + if (err < 0) { + restore_owner_if_matches(tstate, m, globals, name); } - Py_XDECREF(globals); - Py_XDECREF(name); + else { + clear_owner_if_matches(tstate, m, globals, name); + } + Py_DECREF(globals); + Py_DECREF(name); return err; } @@ -345,7 +387,8 @@ PyDoc_STRVAR(lazy_import_doc, "\n" "A successful resolution is cached. Instances of this object accessed\n" "from the global scope will be automatically imported based upon their\n" -"name and then replaced with the imported value."); +"name. The original global is replaced only if it still refers to this\n" +"lazy import object."); PyTypeObject PyLazyImport_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) From eb3bcc9852e14086339234ee83698374f470234c Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Sat, 27 Jun 2026 11:21:50 +0800 Subject: [PATCH 14/14] gh-152298: Keep reentrant lazy test proxy local --- Lib/test/test_lazy_import/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 4eb6fffa0493e9..70cffae4bf9de9 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -787,7 +787,7 @@ def __del__(self): target = Reenter() lazy import target_module as target - try: + def check_after_store(): assert not errors, errors assert len(proxies) == 1, proxies proxy = proxies[0] @@ -800,6 +800,9 @@ def __del__(self): globals()["target"] = proxy assert proxy.resolve() is resolved assert globals()["target"] is proxy + + try: + check_after_store() assert len(calls) == 1, calls finally: builtins.__import__ = real_import