Skip to content

gh-125723: Fix crash with f_locals when generator frame outlive their generator #126956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,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.
Expand Down
83 changes: 83 additions & 0 deletions Lib/test/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix crash with ``gi_frame.f_locals`` when generator frame outlive their
generator. Patch by Mikhail Efimov.
23 changes: 15 additions & 8 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,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 = &gen->gi_iframe;
frame->previous = NULL;
_PyFrame_ClearExceptCode(frame);
_PyErr_ClearExcState(&gen->gi_exc_state);
}

static void
gen_dealloc(PyObject *self)
{
Expand All @@ -159,13 +172,7 @@ gen_dealloc(PyObject *self)
if (PyCoro_CheckExact(gen)) {
Py_CLEAR(((PyCoroObject *)gen)->cr_origin_or_finalizer);
}
if (gen->gi_frame_state != FRAME_CLEARED) {
_PyInterpreterFrame *frame = &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);
PyStackRef_CLEAR(gen->gi_iframe.f_executable);
Py_CLEAR(gen->gi_name);
Expand Down Expand Up @@ -400,7 +407,7 @@ gen_close(PyObject *self, 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(&gen->gi_iframe);
gen_clear_frame(gen);
Py_RETURN_NONE;
}
}
Expand Down
Loading