From eff805b7a7a9678639bbcebe804864406cc4eab2 Mon Sep 17 00:00:00 2001 From: AN Long Date: Thu, 18 Jun 2026 03:09:51 +0900 Subject: [PATCH 1/2] gh-151510: Fix __lazy_import__ without frame (#151511) --- Lib/test/test_lazy_import/__init__.py | 11 ++++++++++ ...-06-16-00-45-42.gh-issue-151510.HJ-kGn.rst | 2 ++ Modules/_testcapi/import.c | 22 +++++++++++++++++++ Python/bltinmodule.c | 6 +++++ 4 files changed, 41 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-06-16-00-45-42.gh-issue-151510.HJ-kGn.rst diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 1724beb8ce6951..1c5ab4ef73da2f 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -1949,6 +1949,17 @@ def filter(*args): def test_set_bad_filter(self): self.assertRaises(ValueError, _testcapi.PyImport_SetLazyImportsFilter, 42) + def test_dunder_lazy_import_without_frame(self): + # gh-151510: __lazy_import__() called with no globals and no running + # Python frame must raise TypeError instead of crashing. + with self.assertRaisesRegex( + TypeError, + r"__lazy_import__\(\) missing globals when called without a frame", + ): + _testcapi.lazy_import_without_frame( + "test.test_lazy_import.data.basic2" + ) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-16-00-45-42.gh-issue-151510.HJ-kGn.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-16-00-45-42.gh-issue-151510.HJ-kGn.rst new file mode 100644 index 00000000000000..cfa5ee8d3839c1 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-16-00-45-42.gh-issue-151510.HJ-kGn.rst @@ -0,0 +1,2 @@ +Fix a crash in :func:`!__lazy_import__` when called without an explicit +``globals`` argument and without a current Python frame. diff --git a/Modules/_testcapi/import.c b/Modules/_testcapi/import.c index 384a8f52da4b98..11d0e6acaebe1f 100644 --- a/Modules/_testcapi/import.c +++ b/Modules/_testcapi/import.c @@ -1,6 +1,27 @@ #include "parts.h" #include "util.h" +static PyObject * +pyimport_lazyimportwithoutframe(PyObject *self, PyObject *name) +{ + PyObject *lazy_import = PyImport_ImportModuleAttrString("builtins", + "__lazy_import__"); + if (lazy_import == NULL) { + return NULL; + } + + // Simulate being called with no running Python frame (e.g. from a freshly + // attached C thread), so that PyEval_GetGlobals() returns NULL. + PyThreadState *tstate = PyThreadState_Get(); + struct _PyInterpreterFrame *saved = tstate->current_frame; + tstate->current_frame = NULL; + PyObject *res = PyObject_CallOneArg(lazy_import, name); + tstate->current_frame = saved; + + Py_DECREF(lazy_import); + return res; +} + // Test PyImport_ImportModuleAttr() static PyObject * pyimport_importmoduleattr(PyObject *self, PyObject *args) @@ -95,6 +116,7 @@ static PyMethodDef test_methods[] = { {"PyImport_GetLazyImportsMode", pyimport_getlazyimportsmode, METH_NOARGS}, {"PyImport_SetLazyImportsFilter", pyimport_setlazyimportsfilter, METH_VARARGS}, {"PyImport_GetLazyImportsFilter", pyimport_getlazyimportsfilter, METH_NOARGS}, + {"lazy_import_without_frame", pyimport_lazyimportwithoutframe, METH_O}, {NULL}, }; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index d5129bf6a5a6bc..fa64255be00e75 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -313,6 +313,12 @@ builtin___lazy_import___impl(PyObject *module, PyObject *name, PyThreadState *tstate = PyThreadState_GET(); if (globals == NULL) { globals = PyEval_GetGlobals(); + if (globals == NULL) { + PyErr_SetString(PyExc_TypeError, + "__lazy_import__() missing globals " + "when called without a frame"); + return NULL; + } } if (locals == NULL) { locals = globals; From a8d74c062fe3c5cb2962dde8bee83704fcfa1bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?= Date: Wed, 17 Jun 2026 22:49:23 +0200 Subject: [PATCH 2/2] gh-151436: Fix missing `tstate->last_profiled_frame` updates (#151437) --- Include/internal/pycore_interpframe.h | 14 ++++++++++++++ .../2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst | 4 ++++ Modules/_testinternalcapi/test_cases.c.h | 3 +++ Objects/genobject.c | 3 +++ Python/bytecodes.c | 2 ++ Python/ceval.c | 10 ++-------- Python/executor_cases.c.h | 2 ++ Python/generated_cases.c.h | 3 +++ 8 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst diff --git a/Include/internal/pycore_interpframe.h b/Include/internal/pycore_interpframe.h index ee90ca3a9b5900..0f4bf7d8a2f2f0 100644 --- a/Include/internal/pycore_interpframe.h +++ b/Include/internal/pycore_interpframe.h @@ -287,6 +287,20 @@ _PyThreadState_GetFrame(PyThreadState *tstate) return _PyFrame_GetFirstComplete(tstate->current_frame); } +// Update last_profiled_frame for remote profiler frame caching. +// Only update if we're removing the exact frame that was last profiled. +// This avoids corrupting the cache when transient frames (called and returned +// between profiler samples) update last_profiled_frame to addresses the +// profiler never saw. +#define _PyThreadState_UpdateLastProfiledFrame(tstate, frame, previous) \ + do { \ + PyThreadState *tstate_ = (tstate); \ + _PyInterpreterFrame *frame_ = (frame); \ + if (tstate_->last_profiled_frame == frame_) { \ + tstate_->last_profiled_frame = (previous); \ + } \ + } while (0) + /* For use by _PyFrame_GetFrameObject Do not call directly. */ PyAPI_FUNC(PyFrameObject *) diff --git a/Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst b/Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst new file mode 100644 index 00000000000000..1d1aadbf57be48 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst @@ -0,0 +1,4 @@ +Fix skewed stack trackes in the Tachyon profiler when caching is enabled and +when generators and coroutines are profiled, by updating +``tstate->last_profiled_frame`` at every frame-removal site. The issue resulted +in total erasure of some callers. Patch by Maurycy Pawłowski-Wieroński. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 62d08826a2faea..f2935310ffbdb9 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -7939,6 +7939,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -11000,6 +11001,7 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); @@ -13029,6 +13031,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; diff --git a/Objects/genobject.c b/Objects/genobject.c index 38d493343454fc..3cdc06733363d3 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -168,6 +168,7 @@ gen_clear_frame(PyGenObject *gen) { assert(FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state) == FRAME_CLEARED); _PyInterpreterFrame *frame = &gen->gi_iframe; + _PyThreadState_UpdateLastProfiledFrame(_PyThreadState_GET(), frame, frame->previous); frame->previous = NULL; _PyFrame_ClearExceptCode(frame); _PyErr_ClearExcState(&gen->gi_exc_state); @@ -681,6 +682,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, 'yield from' or awaiting on with 'await'. */ ret = _gen_throw((PyGenObject *)yf, close_on_genexit, typ, val, tb); + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); tstate->current_frame = prev; frame->previous = NULL; } @@ -701,6 +703,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, frame->previous = prev; tstate->current_frame = frame; ret = PyObject_CallFunctionObjArgs(meth, typ, val, tb, NULL); + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); tstate->current_frame = prev; frame->previous = NULL; Py_DECREF(meth); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index beaf6752b87ea2..51a188a6120fa7 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1860,6 +1860,7 @@ dummy_func( gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -5874,6 +5875,7 @@ dummy_func( gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); diff --git a/Python/ceval.c b/Python/ceval.c index d6dd7f9a82c431..32d31bee660a50 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1974,15 +1974,8 @@ clear_gen_frame(PyThreadState *tstate, _PyInterpreterFrame * frame) void _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame * frame) { - // Update last_profiled_frame for remote profiler frame caching. // By this point, tstate->current_frame is already set to the parent frame. - // Only update if we're popping the exact frame that was last profiled. - // This avoids corrupting the cache when transient frames (called and returned - // between profiler samples) update last_profiled_frame to addresses the - // profiler never saw. - if (tstate->last_profiled_frame != NULL && tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = tstate->current_frame; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, tstate->current_frame); if (frame->owner == FRAME_OWNED_BY_THREAD) { clear_thread_frame(tstate, frame); @@ -2008,6 +2001,7 @@ _PyEvalFramePushAndInit(PyThreadState *tstate, _PyStackRef func, _PyFrame_Initialize(tstate, frame, func, locals, code, 0, previous); if (initialize_locals(tstate, func_obj, frame->localsplus, args, argcount, kwnames)) { assert(frame->owner == FRAME_OWNED_BY_THREAD); + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, tstate->current_frame); clear_thread_frame(tstate, frame); return NULL; } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index d5bfe60cd23473..e3d13f9f9c61c3 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9340,6 +9340,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -20346,6 +20347,7 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index a6e0f90d8c1ce2..a5590d992b39cf 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -7938,6 +7938,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -10997,6 +10998,7 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); @@ -13026,6 +13028,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD;