Skip to content

Commit 3586f17

Browse files
committed
Add failing regression test
1 parent c206cc9 commit 3586f17

File tree

1 file changed

+40
-0
lines changed

1 file changed

+40
-0
lines changed

Lib/test/test_frame.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
import sys
44
import textwrap
5+
import threading
56
import types
67
import unittest
78
import weakref
@@ -325,6 +326,45 @@ def f():
325326
if old_enabled:
326327
gc.enable()
327328

329+
@support.cpython_only
330+
def test_sneaky_frame_object_teardown(self):
331+
332+
class SneakyDel:
333+
def __del__(self):
334+
"""
335+
Stash a reference to the entire stack for walking later.
336+
337+
It may look crazy, but you'd be surprised how common this is
338+
when using a test runner (like pytest). The typical recipe is:
339+
ResourceWarning + -Werror + a custom sys.unraisablehook.
340+
"""
341+
nonlocal sneaky_frame_object
342+
sneaky_frame_object = sys._getframe()
343+
344+
class SneakyThread(threading.Thread):
345+
"""
346+
A separate thread isn't needed to make this code crash, but it does
347+
make crashes more consistent, since it means sneaky_frame_object is
348+
backed by freed memory after the thread completes!
349+
"""
350+
351+
def run(self):
352+
"""Run SneakyDel.__del__ as this frame is popped."""
353+
ref = SneakyDel()
354+
355+
sneaky_frame_object = None
356+
t = SneakyThread()
357+
t.start()
358+
t.join()
359+
# sneaky_frame_object can be anything, really, but it's crucial that
360+
# SneakyThread.run's frame isn't anywhere on the stack while it's being
361+
# torn down:
362+
self.assertIsNotNone(sneaky_frame_object)
363+
while sneaky_frame_object is not None:
364+
self.assertIsNot(
365+
sneaky_frame_object.f_code, SneakyThread.run.__code__
366+
)
367+
sneaky_frame_object = sneaky_frame_object.f_back
328368

329369
if __name__ == "__main__":
330370
unittest.main()

0 commit comments

Comments
 (0)