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