Skip to content

Commit

Permalink
gh-125723: Fix crash with f_locals when generator frame outlive their…
Browse files Browse the repository at this point in the history
… generator (#126956)

Co-authored-by: Kirill Podoprigora <kirill.bast9@mail.ru>
Co-authored-by: Alyssa Coghlan <ncoghlan@gmail.com>
  • Loading branch information
3 people authored Jan 22, 2025
1 parent 24c84d8 commit 8e20e42
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,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 frames 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

0 comments on commit 8e20e42

Please sign in to comment.