Skip to content

Commit a53cc3f

Browse files
GH-116098: Remove dead frame object creation code (GH-116687)
1 parent 149f7f7 commit a53cc3f

2 files changed

Lines changed: 9 additions & 83 deletions

File tree

‎Lib/test/test_frame.py‎

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -293,71 +293,6 @@ def gen():
293293
""")
294294
assert_python_ok("-c", code)
295295

296-
@support.cpython_only
297-
@unittest.skipIf(Py_GIL_DISABLED, "test requires precise GC scheduling")
298-
deftest_sneaky_frame_object(self):
299-
300-
deftrace(frame, event, arg):
301-
"""
302-
Don't actually do anything, just force a frame object to be created.
303-
"""
304-
305-
defcallback(phase, info):
306-
"""
307-
Yo dawg, I heard you like frames, so I'm allocating a frame while
308-
you're allocating a frame, so you can have a frame while you have a
309-
frame!
310-
"""
311-
nonlocalsneaky_frame_object
312-
sneaky_frame_object=sys._getframe().f_back.f_back
313-
# We're done here:
314-
gc.callbacks.remove(callback)
315-
316-
deff():
317-
whileTrue:
318-
yield
319-
320-
old_threshold=gc.get_threshold()
321-
old_callbacks=gc.callbacks[:]
322-
old_enabled=gc.isenabled()
323-
old_trace=sys.gettrace()
324-
try:
325-
# Stop the GC for a second while we set things up:
326-
gc.disable()
327-
# Create a paused generator:
328-
g=f()
329-
next(g)
330-
# Move all objects to the oldest generation, and tell the GC to run
331-
# on the *very next* allocation:
332-
gc.collect()
333-
gc.set_threshold(1, 0, 0)
334-
sys._clear_internal_caches()
335-
# Okay, so here's the nightmare scenario:
336-
# - We're tracing the resumption of a generator, which creates a new
337-
# frame object.
338-
# - The allocation of this frame object triggers a collection
339-
# *before* the frame object is actually created.
340-
# - During the collection, we request the exact same frame object.
341-
# This test does it with a GC callback, but in real code it would
342-
# likely be a trace function, weakref callback, or finalizer.
343-
# - The collection finishes, and the original frame object is
344-
# created. We now have two frame objects fighting over ownership
345-
# of the same interpreter frame!
346-
sys.settrace(trace)
347-
gc.callbacks.append(callback)
348-
sneaky_frame_object=None
349-
gc.enable()
350-
next(g)
351-
# g.gi_frame should be the frame object from the callback (the
352-
# one that was *requested* second, but *created* first):
353-
self.assertIs(g.gi_frame, sneaky_frame_object)
354-
finally:
355-
gc.set_threshold(*old_threshold)
356-
gc.callbacks[:] =old_callbacks
357-
sys.settrace(old_trace)
358-
ifold_enabled:
359-
gc.enable()
360-
361296
@support.cpython_only
362297
@threading_helper.requires_working_threading()
363298
deftest_sneaky_frame_object_teardown(self):

‎Python/frame.c‎

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,15 @@ _PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame)
3737
returnNULL;
3838
}
3939
PyErr_SetRaisedException(exc);
40-
if (frame->frame_obj) {
41-
// GH-97002: How did we get into this horrible situation? Most likely,
42-
// allocating f triggered a GC collection, which ran some code that
43-
// *also* created the same frame... while we were in the middle of
44-
// creating it! See test_sneaky_frame_object in test_frame.py for a
45-
// concrete example.
46-
//
47-
// Regardless, just throw f away and use that frame instead, since it's
48-
// already been exposed to user code. It's actually a bit tricky to do
49-
// this, since we aren't backed by a real _PyInterpreterFrame anymore.
50-
// Just pretend that we have an owned, cleared frame so frame_dealloc
51-
// doesn't make the situation worse:
52-
f->f_frame= (_PyInterpreterFrame*)f->_f_frame_data;
53-
f->f_frame->owner=FRAME_CLEARED;
54-
f->f_frame->frame_obj=f;
55-
Py_DECREF(f);
56-
returnframe->frame_obj;
57-
}
40+
41+
// GH-97002: There was a time when a frame object could be created when we
42+
// are allocating the new frame object f above, so frame->frame_obj would
43+
// be assigned already. That path does not exist anymore. We won't call any
44+
// Python code in this function and garbage collection will not run.
45+
// Notice that _PyFrame_New_NoTrack() can potentially raise a MemoryError,
46+
// but it won't allocate a traceback until the frame unwinds, so we are safe
47+
// here.
48+
assert(frame->frame_obj==NULL);
5849
assert(frame->owner!=FRAME_OWNED_BY_FRAME_OBJECT);
5950
assert(frame->owner!=FRAME_CLEARED);
6051
f->f_frame=frame;

0 commit comments

Comments
 (0)