summaryrefslogtreecommitdiffstats
path: root/Lib/test/test_frame.py
diff options
context:
space:
mode:
authorBrandt Bucher <brandtbucher@microsoft.com>2022-10-06 23:20:01 (GMT)
committerGitHub <noreply@github.com>2022-10-06 23:20:01 (GMT)
commit21a2d9ff550977f2668e2cf1cc15793bf27fa109 (patch)
treedd65296818dec2b91b48292b04d022929967a406 /Lib/test/test_frame.py
parentcbf0afd8a1474d68310331af9218606959d4cc22 (diff)
downloadcpython-21a2d9ff550977f2668e2cf1cc15793bf27fa109.zip
cpython-21a2d9ff550977f2668e2cf1cc15793bf27fa109.tar.gz
cpython-21a2d9ff550977f2668e2cf1cc15793bf27fa109.tar.bz2
GH-97002: Prevent `_PyInterpreterFrame`s from backing more than one `PyFrameObject` (GH-97996)
Diffstat (limited to 'Lib/test/test_frame.py')
-rw-r--r--Lib/test/test_frame.py65
1 files changed, 65 insertions, 0 deletions
diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py
index 5dda2fb..4b86a60 100644
--- a/Lib/test/test_frame.py
+++ b/Lib/test/test_frame.py
@@ -1,3 +1,4 @@
+import gc
import re
import sys
import textwrap
@@ -261,5 +262,69 @@ class TestIncompleteFrameAreInvisible(unittest.TestCase):
""")
assert_python_ok("-c", code)
+ @support.cpython_only
+ def test_sneaky_frame_object(self):
+
+ def trace(frame, event, arg):
+ """
+ Don't actually do anything, just force a frame object to be created.
+ """
+
+ def callback(phase, info):
+ """
+ Yo dawg, I heard you like frames, so I'm allocating a frame while
+ you're allocating a frame, so you can have a frame while you have a
+ frame!
+ """
+ nonlocal sneaky_frame_object
+ sneaky_frame_object = sys._getframe().f_back
+ # We're done here:
+ gc.callbacks.remove(callback)
+
+ def f():
+ while True:
+ yield
+
+ old_threshold = gc.get_threshold()
+ old_callbacks = gc.callbacks[:]
+ old_enabled = gc.isenabled()
+ old_trace = sys.gettrace()
+ try:
+ # Stop the GC for a second while we set things up:
+ gc.disable()
+ # Create a paused generator:
+ g = f()
+ next(g)
+ # Move all objects to the oldest generation, and tell the GC to run
+ # on the *very next* allocation:
+ gc.collect()
+ gc.set_threshold(1, 0, 0)
+ # Okay, so here's the nightmare scenario:
+ # - We're tracing the resumption of a generator, which creates a new
+ # frame object.
+ # - The allocation of this frame object triggers a collection
+ # *before* the frame object is actually created.
+ # - During the collection, we request the exact same frame object.
+ # This test does it with a GC callback, but in real code it would
+ # likely be a trace function, weakref callback, or finalizer.
+ # - The collection finishes, and the original frame object is
+ # created. We now have two frame objects fighting over ownership
+ # of the same interpreter frame!
+ sys.settrace(trace)
+ gc.callbacks.append(callback)
+ sneaky_frame_object = None
+ gc.enable()
+ next(g)
+ # g.gi_frame should be the the frame object from the callback (the
+ # one that was *requested* second, but *created* first):
+ self.assertIs(g.gi_frame, sneaky_frame_object)
+ finally:
+ gc.set_threshold(*old_threshold)
+ gc.callbacks[:] = old_callbacks
+ sys.settrace(old_trace)
+ if old_enabled:
+ gc.enable()
+
+
if __name__ == "__main__":
unittest.main()