Skip to content

Commit e753a7c

Browse files
[3.13] gh-125723: Fix crash with f_locals when generator frame outlive their generator (GH-135453)
Backport of 8e20e42 from GH-126956 Closes GH-125723
1 parent 22cf289 commit e753a7c

File tree

4 files changed

+101
-9
lines changed

4 files changed

+101
-9
lines changed

Include/internal/pycore_object.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ extern void _Py_ForgetReference(PyObject *);
7272
PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *);
7373

7474
/* We need to maintain an internal copy of Py{Var}Object_HEAD_INIT to avoid
75-
designated initializer conflicts in C++20. If we use the deinition in
75+
designated initializer conflicts in C++20. If we use the definition in
7676
object.h, we will be mixing designated and non-designated initializers in
7777
pycore objects which is forbiddent in C++20. However, if we then use
7878
designated initializers in object.h then Extensions without designated break.

Lib/test/test_generators.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,89 @@ def genfn():
630630
self.assertIsNone(f_wr())
631631

632632

633+
# See https://github.com/python/cpython/issues/125723
634+
class GeneratorDeallocTest(unittest.TestCase):
635+
def test_frame_outlives_generator(self):
636+
def g1():
637+
a = 42
638+
yield sys._getframe()
639+
640+
def g2():
641+
a = 42
642+
yield
643+
644+
def g3(obj):
645+
a = 42
646+
obj.frame = sys._getframe()
647+
yield
648+
649+
class ObjectWithFrame():
650+
def __init__(self):
651+
self.frame = None
652+
653+
def get_frame(index):
654+
if index == 1:
655+
return next(g1())
656+
elif index == 2:
657+
gen = g2()
658+
next(gen)
659+
return gen.gi_frame
660+
elif index == 3:
661+
obj = ObjectWithFrame()
662+
next(g3(obj))
663+
return obj.frame
664+
else:
665+
return None
666+
667+
for index in (1, 2, 3):
668+
with self.subTest(index=index):
669+
frame = get_frame(index)
670+
frame_locals = frame.f_locals
671+
self.assertIn('a', frame_locals)
672+
self.assertEqual(frame_locals['a'], 42)
673+
674+
def test_frame_locals_outlive_generator(self):
675+
frame_locals1 = None
676+
677+
def g1():
678+
nonlocal frame_locals1
679+
frame_locals1 = sys._getframe().f_locals
680+
a = 42
681+
yield
682+
683+
def g2():
684+
a = 42
685+
yield sys._getframe().f_locals
686+
687+
def get_frame_locals(index):
688+
if index == 1:
689+
nonlocal frame_locals1
690+
next(g1())
691+
return frame_locals1
692+
if index == 2:
693+
return next(g2())
694+
else:
695+
return None
696+
697+
for index in (1, 2):
698+
with self.subTest(index=index):
699+
frame_locals = get_frame_locals(index)
700+
self.assertIn('a', frame_locals)
701+
self.assertEqual(frame_locals['a'], 42)
702+
703+
def test_frame_locals_outlive_generator_with_exec(self):
704+
def g():
705+
a = 42
706+
yield locals(), sys._getframe().f_locals
707+
708+
locals_ = {'g': g}
709+
for i in range(10):
710+
exec("snapshot, live_locals = next(g())", locals=locals_)
711+
for l in (locals_['snapshot'], locals_['live_locals']):
712+
self.assertIn('a', l)
713+
self.assertEqual(l['a'], 42)
714+
715+
633716
class GeneratorThrowTest(unittest.TestCase):
634717

635718
def test_exception_context_with_yield(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix crash with ``gi_frame.f_locals`` when generator frames outlive their
2+
generator. Patch by Mikhail Efimov.

Objects/genobject.c

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,19 @@ _PyGen_Finalize(PyObject *self)
118118
PyErr_SetRaisedException(exc);
119119
}
120120

121+
static void
122+
gen_clear_frame(PyGenObject *gen)
123+
{
124+
if (gen->gi_frame_state == FRAME_CLEARED)
125+
return;
126+
127+
gen->gi_frame_state = FRAME_CLEARED;
128+
_PyInterpreterFrame *frame = (_PyInterpreterFrame *)gen->gi_iframe;
129+
frame->previous = NULL;
130+
_PyFrame_ClearExceptCode(frame);
131+
_PyErr_ClearExcState(&gen->gi_exc_state);
132+
}
133+
121134
static void
122135
gen_dealloc(PyGenObject *gen)
123136
{
@@ -140,13 +153,7 @@ gen_dealloc(PyGenObject *gen)
140153
and GC_Del. */
141154
Py_CLEAR(((PyAsyncGenObject*)gen)->ag_origin_or_finalizer);
142155
}
143-
if (gen->gi_frame_state != FRAME_CLEARED) {
144-
_PyInterpreterFrame *frame = (_PyInterpreterFrame *)gen->gi_iframe;
145-
gen->gi_frame_state = FRAME_CLEARED;
146-
frame->previous = NULL;
147-
_PyFrame_ClearExceptCode(frame);
148-
_PyErr_ClearExcState(&gen->gi_exc_state);
149-
}
156+
gen_clear_frame(gen);
150157
assert(gen->gi_exc_state.exc_value == NULL);
151158
if (_PyGen_GetCode(gen)->co_flags & CO_COROUTINE) {
152159
Py_CLEAR(((PyCoroObject *)gen)->cr_origin_or_finalizer);
@@ -382,7 +389,7 @@ gen_close(PyGenObject *gen, PyObject *args)
382389
// RESUME after YIELD_VALUE and exception depth is 1
383390
assert((oparg & RESUME_OPARG_LOCATION_MASK) != RESUME_AT_FUNC_START);
384391
gen->gi_frame_state = FRAME_COMPLETED;
385-
_PyFrame_ClearLocals((_PyInterpreterFrame *)gen->gi_iframe);
392+
gen_clear_frame(gen);
386393
Py_RETURN_NONE;
387394
}
388395
}

0 commit comments

Comments
 (0)