Skip to content

Commit 8e20e42

Browse files
efimov-mikhailEclips4ncoghlan
authored
gh-125723: Fix crash with f_locals when generator frame outlive their generator (#126956)
Co-authored-by: Kirill Podoprigora <[email protected]> Co-authored-by: Alyssa Coghlan <[email protected]>
1 parent 24c84d8 commit 8e20e42

File tree

4 files changed

+101
-9
lines changed

4 files changed

+101
-9
lines changed

Include/internal/pycore_object.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ extern void _Py_ForgetReference(PyObject *);
6262
PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *);
6363

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

Lib/test/test_generators.py

+83
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,89 @@ def genfn():
652652
self.assertIsNone(f_wr())
653653

654654

655+
# See https://github.com/python/cpython/issues/125723
656+
class GeneratorDeallocTest(unittest.TestCase):
657+
def test_frame_outlives_generator(self):
658+
def g1():
659+
a = 42
660+
yield sys._getframe()
661+
662+
def g2():
663+
a = 42
664+
yield
665+
666+
def g3(obj):
667+
a = 42
668+
obj.frame = sys._getframe()
669+
yield
670+
671+
class ObjectWithFrame():
672+
def __init__(self):
673+
self.frame = None
674+
675+
def get_frame(index):
676+
if index == 1:
677+
return next(g1())
678+
elif index == 2:
679+
gen = g2()
680+
next(gen)
681+
return gen.gi_frame
682+
elif index == 3:
683+
obj = ObjectWithFrame()
684+
next(g3(obj))
685+
return obj.frame
686+
else:
687+
return None
688+
689+
for index in (1, 2, 3):
690+
with self.subTest(index=index):
691+
frame = get_frame(index)
692+
frame_locals = frame.f_locals
693+
self.assertIn('a', frame_locals)
694+
self.assertEqual(frame_locals['a'], 42)
695+
696+
def test_frame_locals_outlive_generator(self):
697+
frame_locals1 = None
698+
699+
def g1():
700+
nonlocal frame_locals1
701+
frame_locals1 = sys._getframe().f_locals
702+
a = 42
703+
yield
704+
705+
def g2():
706+
a = 42
707+
yield sys._getframe().f_locals
708+
709+
def get_frame_locals(index):
710+
if index == 1:
711+
nonlocal frame_locals1
712+
next(g1())
713+
return frame_locals1
714+
if index == 2:
715+
return next(g2())
716+
else:
717+
return None
718+
719+
for index in (1, 2):
720+
with self.subTest(index=index):
721+
frame_locals = get_frame_locals(index)
722+
self.assertIn('a', frame_locals)
723+
self.assertEqual(frame_locals['a'], 42)
724+
725+
def test_frame_locals_outlive_generator_with_exec(self):
726+
def g():
727+
a = 42
728+
yield locals(), sys._getframe().f_locals
729+
730+
locals_ = {'g': g}
731+
for i in range(10):
732+
exec("snapshot, live_locals = next(g())", locals=locals_)
733+
for l in (locals_['snapshot'], locals_['live_locals']):
734+
self.assertIn('a', l)
735+
self.assertEqual(l['a'], 42)
736+
737+
655738
class GeneratorThrowTest(unittest.TestCase):
656739

657740
def test_exception_context_with_yield(self):
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

+15-8
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,19 @@ _PyGen_Finalize(PyObject *self)
134134
PyErr_SetRaisedException(exc);
135135
}
136136

137+
static void
138+
gen_clear_frame(PyGenObject *gen)
139+
{
140+
if (gen->gi_frame_state == FRAME_CLEARED)
141+
return;
142+
143+
gen->gi_frame_state = FRAME_CLEARED;
144+
_PyInterpreterFrame *frame = &gen->gi_iframe;
145+
frame->previous = NULL;
146+
_PyFrame_ClearExceptCode(frame);
147+
_PyErr_ClearExcState(&gen->gi_exc_state);
148+
}
149+
137150
static void
138151
gen_dealloc(PyObject *self)
139152
{
@@ -159,13 +172,7 @@ gen_dealloc(PyObject *self)
159172
if (PyCoro_CheckExact(gen)) {
160173
Py_CLEAR(((PyCoroObject *)gen)->cr_origin_or_finalizer);
161174
}
162-
if (gen->gi_frame_state != FRAME_CLEARED) {
163-
_PyInterpreterFrame *frame = &gen->gi_iframe;
164-
gen->gi_frame_state = FRAME_CLEARED;
165-
frame->previous = NULL;
166-
_PyFrame_ClearExceptCode(frame);
167-
_PyErr_ClearExcState(&gen->gi_exc_state);
168-
}
175+
gen_clear_frame(gen);
169176
assert(gen->gi_exc_state.exc_value == NULL);
170177
PyStackRef_CLEAR(gen->gi_iframe.f_executable);
171178
Py_CLEAR(gen->gi_name);
@@ -400,7 +407,7 @@ gen_close(PyObject *self, PyObject *args)
400407
// RESUME after YIELD_VALUE and exception depth is 1
401408
assert((oparg & RESUME_OPARG_LOCATION_MASK) != RESUME_AT_FUNC_START);
402409
gen->gi_frame_state = FRAME_COMPLETED;
403-
_PyFrame_ClearLocals(&gen->gi_iframe);
410+
gen_clear_frame(gen);
404411
Py_RETURN_NONE;
405412
}
406413
}

0 commit comments

Comments
 (0)