Skip to content

Commit d0ab10f

Browse files
miss-islingtonpablogsal
authored andcommitted
[3.11] GH-97002: Prevent _PyInterpreterFrames from backing more than one PyFrameObject (GH-98002)
(cherry picked from commit 21a2d9f)
1 parent 154b3cd commit d0ab10f

File tree

3 files changed

+91
-6
lines changed

3 files changed

+91
-6
lines changed

Lib/test/test_frame.py

+65
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import gc
12
import re
23
import sys
34
import types
@@ -258,5 +259,69 @@ def gen():
258259
gen()
259260

260261

262+
@support.cpython_only
263+
def test_sneaky_frame_object(self):
264+
265+
def trace(frame, event, arg):
266+
"""
267+
Don't actually do anything, just force a frame object to be created.
268+
"""
269+
270+
def callback(phase, info):
271+
"""
272+
Yo dawg, I heard you like frames, so I'm allocating a frame while
273+
you're allocating a frame, so you can have a frame while you have a
274+
frame!
275+
"""
276+
nonlocal sneaky_frame_object
277+
sneaky_frame_object = sys._getframe().f_back
278+
# We're done here:
279+
gc.callbacks.remove(callback)
280+
281+
def f():
282+
while True:
283+
yield
284+
285+
old_threshold = gc.get_threshold()
286+
old_callbacks = gc.callbacks[:]
287+
old_enabled = gc.isenabled()
288+
old_trace = sys.gettrace()
289+
try:
290+
# Stop the GC for a second while we set things up:
291+
gc.disable()
292+
# Create a paused generator:
293+
g = f()
294+
next(g)
295+
# Move all objects to the oldest generation, and tell the GC to run
296+
# on the *very next* allocation:
297+
gc.collect()
298+
gc.set_threshold(1, 0, 0)
299+
# Okay, so here's the nightmare scenario:
300+
# - We're tracing the resumption of a generator, which creates a new
301+
# frame object.
302+
# - The allocation of this frame object triggers a collection
303+
# *before* the frame object is actually created.
304+
# - During the collection, we request the exact same frame object.
305+
# This test does it with a GC callback, but in real code it would
306+
# likely be a trace function, weakref callback, or finalizer.
307+
# - The collection finishes, and the original frame object is
308+
# created. We now have two frame objects fighting over ownership
309+
# of the same interpreter frame!
310+
sys.settrace(trace)
311+
gc.callbacks.append(callback)
312+
sneaky_frame_object = None
313+
gc.enable()
314+
next(g)
315+
# g.gi_frame should be the the frame object from the callback (the
316+
# one that was *requested* second, but *created* first):
317+
self.assertIs(g.gi_frame, sneaky_frame_object)
318+
finally:
319+
gc.set_threshold(*old_threshold)
320+
gc.callbacks[:] = old_callbacks
321+
sys.settrace(old_trace)
322+
if old_enabled:
323+
gc.enable()
324+
325+
261326
if __name__ == "__main__":
262327
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix an issue where several frame objects could be backed by the same
2+
interpreter frame, possibly leading to corrupted memory and hard crashes of
3+
the interpreter.

Python/frame.c

+23-6
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,31 @@ _PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame)
3535
Py_XDECREF(error_type);
3636
Py_XDECREF(error_value);
3737
Py_XDECREF(error_traceback);
38+
return NULL;
3839
}
39-
else {
40-
assert(frame->owner != FRAME_OWNED_BY_FRAME_OBJECT);
41-
assert(frame->owner != FRAME_CLEARED);
42-
f->f_frame = frame;
43-
frame->frame_obj = f;
44-
PyErr_Restore(error_type, error_value, error_traceback);
40+
PyErr_Restore(error_type, error_value, error_traceback);
41+
if (frame->frame_obj) {
42+
// GH-97002: How did we get into this horrible situation? Most likely,
43+
// allocating f triggered a GC collection, which ran some code that
44+
// *also* created the same frame... while we were in the middle of
45+
// creating it! See test_sneaky_frame_object in test_frame.py for a
46+
// concrete example.
47+
//
48+
// Regardless, just throw f away and use that frame instead, since it's
49+
// already been exposed to user code. It's actually a bit tricky to do
50+
// this, since we aren't backed by a real _PyInterpreterFrame anymore.
51+
// Just pretend that we have an owned, cleared frame so frame_dealloc
52+
// doesn't make the situation worse:
53+
f->f_frame = (_PyInterpreterFrame *)f->_f_frame_data;
54+
f->f_frame->owner = FRAME_CLEARED;
55+
f->f_frame->frame_obj = f;
56+
Py_DECREF(f);
57+
return frame->frame_obj;
4558
}
59+
assert(frame->owner != FRAME_OWNED_BY_FRAME_OBJECT);
60+
assert(frame->owner != FRAME_CLEARED);
61+
f->f_frame = frame;
62+
frame->frame_obj = f;
4663
return f;
4764
}
4865

0 commit comments

Comments
 (0)