Skip to content

Commit 544531d

Browse files
authored
GH-94262: Don't create frame objects for frames that aren't yet complete. (GH-94371)
1 parent 1df9449 commit 544531d

File tree

7 files changed

+87
-13
lines changed

7 files changed

+87
-13
lines changed

Include/internal/pycore_frame.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@ _PyFrame_SetStackPointer(_PyInterpreterFrame *frame, PyObject **stack_pointer)
134134
frame->stacktop = (int)(stack_pointer - frame->localsplus);
135135
}
136136

137+
/* Determine whether a frame is incomplete.
138+
* A frame is incomplete if it is part way through
139+
* creating cell objects or a generator or coroutine.
140+
*
141+
* Frames on the frame stack are incomplete until the
142+
* first RESUME instruction.
143+
* Frames owned by a generator are always complete.
144+
*/
145+
static inline bool
146+
_PyFrame_IsIncomplete(_PyInterpreterFrame *frame)
147+
{
148+
return frame->owner != FRAME_OWNED_BY_GENERATOR &&
149+
frame->prev_instr < _PyCode_CODE(frame->f_code) + frame->f_code->_co_firsttraceable;
150+
}
151+
137152
/* For use by _PyFrame_GetFrameObject
138153
Do not call directly. */
139154
PyFrameObject *
@@ -145,6 +160,8 @@ _PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame);
145160
static inline PyFrameObject *
146161
_PyFrame_GetFrameObject(_PyInterpreterFrame *frame)
147162
{
163+
164+
assert(!_PyFrame_IsIncomplete(frame));
148165
PyFrameObject *res = frame->frame_obj;
149166
if (res != NULL) {
150167
return res;

Lib/test/test_generators.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,43 @@ def f():
170170
g.send(0)
171171
self.assertEqual(next(g), 1)
172172

173+
def test_handle_frame_object_in_creation(self):
174+
175+
#Attempt to expose partially constructed frames
176+
#See https://github.com/python/cpython/issues/94262
177+
178+
def cb(*args):
179+
inspect.stack()
180+
181+
def gen():
182+
yield 1
183+
184+
thresholds = gc.get_threshold()
185+
186+
gc.callbacks.append(cb)
187+
gc.set_threshold(1, 0, 0)
188+
try:
189+
gen()
190+
finally:
191+
gc.set_threshold(*thresholds)
192+
gc.callbacks.pop()
193+
194+
class Sneaky:
195+
def __del__(self):
196+
inspect.stack()
197+
198+
sneaky = Sneaky()
199+
sneaky._s = Sneaky()
200+
sneaky._s._s = sneaky
201+
202+
gc.set_threshold(1, 0, 0)
203+
try:
204+
del sneaky
205+
gen()
206+
finally:
207+
gc.set_threshold(*thresholds)
208+
209+
173210

174211
class ExceptionTest(unittest.TestCase):
175212
# Tests for the issue #23353: check that the currently handled exception
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Don't create frame objects for incomplete frames. Prevents the creation of
2+
generators and closures from being observable to Python and C extensions,
3+
restoring the behavior of 3.10 and earlier.

Objects/codeobject.c

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -638,11 +638,10 @@ PyCode_New(int argcount, int kwonlyargcount,
638638
exceptiontable);
639639
}
640640

641-
static const char assert0[4] = {
642-
LOAD_ASSERTION_ERROR,
643-
0,
644-
RAISE_VARARGS,
645-
1
641+
static const char assert0[6] = {
642+
RESUME, 0,
643+
LOAD_ASSERTION_ERROR, 0,
644+
RAISE_VARARGS, 1
646645
};
647646

648647
PyCodeObject *
@@ -666,7 +665,7 @@ PyCode_NewEmpty(const char *filename, const char *funcname, int firstlineno)
666665
if (filename_ob == NULL) {
667666
goto failed;
668667
}
669-
code_ob = PyBytes_FromStringAndSize(assert0, 4);
668+
code_ob = PyBytes_FromStringAndSize(assert0, 6);
670669
if (code_ob == NULL) {
671670
goto failed;
672671
}

Objects/frameobject.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,8 +1169,14 @@ PyFrame_GetBack(PyFrameObject *frame)
11691169
{
11701170
assert(frame != NULL);
11711171
PyFrameObject *back = frame->f_back;
1172-
if (back == NULL && frame->f_frame->previous != NULL) {
1173-
back = _PyFrame_GetFrameObject(frame->f_frame->previous);
1172+
if (back == NULL) {
1173+
_PyInterpreterFrame *prev = frame->f_frame->previous;
1174+
while (prev && _PyFrame_IsIncomplete(prev)) {
1175+
prev = prev->previous;
1176+
}
1177+
if (prev) {
1178+
back = _PyFrame_GetFrameObject(prev);
1179+
}
11741180
}
11751181
Py_XINCREF(back);
11761182
return back;

Python/frame.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,13 @@ take_ownership(PyFrameObject *f, _PyInterpreterFrame *frame)
6868
f->f_frame = frame;
6969
frame->owner = FRAME_OWNED_BY_FRAME_OBJECT;
7070
assert(f->f_back == NULL);
71-
if (frame->previous != NULL) {
71+
_PyInterpreterFrame *prev = frame->previous;
72+
while (prev && _PyFrame_IsIncomplete(prev)) {
73+
prev = prev->previous;
74+
}
75+
if (prev) {
7276
/* Link PyFrameObjects.f_back and remove link through _PyInterpreterFrame.previous */
73-
PyFrameObject *back = _PyFrame_GetFrameObject(frame->previous);
77+
PyFrameObject *back = _PyFrame_GetFrameObject(prev);
7478
if (back == NULL) {
7579
/* Memory error here. */
7680
assert(PyErr_ExceptionMatches(PyExc_MemoryError));

Python/sysmodule.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,9 +1776,17 @@ sys__getframe_impl(PyObject *module, int depth)
17761776
return NULL;
17771777
}
17781778

1779-
while (depth > 0 && frame != NULL) {
1780-
frame = frame->previous;
1781-
--depth;
1779+
if (frame != NULL) {
1780+
while (depth > 0) {
1781+
frame = frame->previous;
1782+
if (frame == NULL) {
1783+
break;
1784+
}
1785+
if (_PyFrame_IsIncomplete(frame)) {
1786+
continue;
1787+
}
1788+
--depth;
1789+
}
17821790
}
17831791
if (frame == NULL) {
17841792
_PyErr_SetString(tstate, PyExc_ValueError,

0 commit comments

Comments
 (0)