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]

Reply via email to