https://github.com/python/cpython/commit/e753a7c5f0ed0dc7e23237662daca57d92124c84
commit: e753a7c5f0ed0dc7e23237662daca57d92124c84
branch: 3.13
author: Mikhail Efimov <[email protected]>
committer: ncoghlan <[email protected]>
date: 2025-06-14T04:08:03+10:00
summary:
[3.13] gh-125723: Fix crash with f_locals when generator frame outlive their
generator (GH-135453)
Backport of 8e20e42cc63321dacc500d7670bfc225ca04e78b from GH-126956
Closes GH-125723
files:
A Misc/NEWS.d/next/Core and
Builtins/2024-11-18-12-17-45.gh-issue-125723.tW_hFG.rst
M Include/internal/pycore_object.h
M Lib/test/test_generators.py
M Objects/genobject.c
diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h
index bed8b9b171ba02..5877d43f4fd5a4 100644
--- a/Include/internal/pycore_object.h
+++ b/Include/internal/pycore_object.h
@@ -72,7 +72,7 @@ extern void _Py_ForgetReference(PyObject *);
PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *);
/* We need to maintain an internal copy of Py{Var}Object_HEAD_INIT to avoid
- designated initializer conflicts in C++20. If we use the deinition in
+ designated initializer conflicts in C++20. If we use the definition in
object.h, we will be mixing designated and non-designated initializers in
pycore objects which is forbiddent in C++20. However, if we then use
designated initializers in object.h then Extensions without designated
break.
diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py
index 515fe7407f1d80..e48d79d34f479f 100644
--- a/Lib/test/test_generators.py
+++ b/Lib/test/test_generators.py
@@ -630,6 +630,89 @@ def genfn():
self.assertIsNone(f_wr())
+# See https://github.com/python/cpython/issues/125723
+class GeneratorDeallocTest(unittest.TestCase):
+ def test_frame_outlives_generator(self):
+ def g1():
+ a = 42
+ yield sys._getframe()
+
+ def g2():
+ a = 42
+ yield
+
+ def g3(obj):
+ a = 42
+ obj.frame = sys._getframe()
+ yield
+
+ class ObjectWithFrame():
+ def __init__(self):
+ self.frame = None
+
+ def get_frame(index):
+ if index == 1:
+ return next(g1())
+ elif index == 2:
+ gen = g2()
+ next(gen)
+ return gen.gi_frame
+ elif index == 3:
+ obj = ObjectWithFrame()
+ next(g3(obj))
+ return obj.frame
+ else:
+ return None
+
+ for index in (1, 2, 3):
+ with self.subTest(index=index):
+ frame = get_frame(index)
+ frame_locals = frame.f_locals
+ self.assertIn('a', frame_locals)
+ self.assertEqual(frame_locals['a'], 42)
+
+ def test_frame_locals_outlive_generator(self):
+ frame_locals1 = None
+
+ def g1():
+ nonlocal frame_locals1
+ frame_locals1 = sys._getframe().f_locals
+ a = 42
+ yield
+
+ def g2():
+ a = 42
+ yield sys._getframe().f_locals
+
+ def get_frame_locals(index):
+ if index == 1:
+ nonlocal frame_locals1
+ next(g1())
+ return frame_locals1
+ if index == 2:
+ return next(g2())
+ else:
+ return None
+
+ for index in (1, 2):
+ with self.subTest(index=index):
+ frame_locals = get_frame_locals(index)
+ self.assertIn('a', frame_locals)
+ self.assertEqual(frame_locals['a'], 42)
+
+ def test_frame_locals_outlive_generator_with_exec(self):
+ def g():
+ a = 42
+ yield locals(), sys._getframe().f_locals
+
+ locals_ = {'g': g}
+ for i in range(10):
+ exec("snapshot, live_locals = next(g())", locals=locals_)
+ for l in (locals_['snapshot'], locals_['live_locals']):
+ self.assertIn('a', l)
+ self.assertEqual(l['a'], 42)
+
+
class GeneratorThrowTest(unittest.TestCase):
def test_exception_context_with_yield(self):
diff --git a/Misc/NEWS.d/next/Core and
Builtins/2024-11-18-12-17-45.gh-issue-125723.tW_hFG.rst b/Misc/NEWS.d/next/Core
and Builtins/2024-11-18-12-17-45.gh-issue-125723.tW_hFG.rst
new file mode 100644
index 00000000000000..62ca6f62f521a8
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and
Builtins/2024-11-18-12-17-45.gh-issue-125723.tW_hFG.rst
@@ -0,0 +1,2 @@
+Fix crash with ``gi_frame.f_locals`` when generator frames outlive their
+generator. Patch by Mikhail Efimov.
diff --git a/Objects/genobject.c b/Objects/genobject.c
index 3a9af4d4c182a3..dedf9e21e9a089 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -118,6 +118,19 @@ _PyGen_Finalize(PyObject *self)
PyErr_SetRaisedException(exc);
}
+static void
+gen_clear_frame(PyGenObject *gen)
+{
+ if (gen->gi_frame_state == FRAME_CLEARED)
+ return;
+
+ gen->gi_frame_state = FRAME_CLEARED;
+ _PyInterpreterFrame *frame = (_PyInterpreterFrame *)gen->gi_iframe;
+ frame->previous = NULL;
+ _PyFrame_ClearExceptCode(frame);
+ _PyErr_ClearExcState(&gen->gi_exc_state);
+}
+
static void
gen_dealloc(PyGenObject *gen)
{
@@ -140,13 +153,7 @@ gen_dealloc(PyGenObject *gen)
and GC_Del. */
Py_CLEAR(((PyAsyncGenObject*)gen)->ag_origin_or_finalizer);
}
- if (gen->gi_frame_state != FRAME_CLEARED) {
- _PyInterpreterFrame *frame = (_PyInterpreterFrame *)gen->gi_iframe;
- gen->gi_frame_state = FRAME_CLEARED;
- frame->previous = NULL;
- _PyFrame_ClearExceptCode(frame);
- _PyErr_ClearExcState(&gen->gi_exc_state);
- }
+ gen_clear_frame(gen);
assert(gen->gi_exc_state.exc_value == NULL);
if (_PyGen_GetCode(gen)->co_flags & CO_COROUTINE) {
Py_CLEAR(((PyCoroObject *)gen)->cr_origin_or_finalizer);
@@ -382,7 +389,7 @@ gen_close(PyGenObject *gen, PyObject *args)
// RESUME after YIELD_VALUE and exception depth is 1
assert((oparg & RESUME_OPARG_LOCATION_MASK) !=
RESUME_AT_FUNC_START);
gen->gi_frame_state = FRAME_COMPLETED;
- _PyFrame_ClearLocals((_PyInterpreterFrame *)gen->gi_iframe);
+ gen_clear_frame(gen);
Py_RETURN_NONE;
}
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]