| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,11 +2,13 @@ | |||
| 2 | 2 | import re | |
| 3 | 3 | import sys | |
| 4 | 4 | import textwrap | |
| 5 | + import threading | ||
| 5 | 6 | import types | |
| 6 | 7 | import unittest | |
| 7 | 8 | import weakref | |
| 8 | 9 | ||
| 9 | 10 | from test import support | |
| 11 | + from test.support import threading_helper | ||
| 10 | 12 | from test.support.script_helper import assert_python_ok | |
| 11 | 13 | ||
| 12 | 14 | ||
@@ -325,6 +327,46 @@ def f(): | |||
| 325 | 327 | if old_enabled: | |
| 326 | 328 | gc.enable() | |
| 327 | 329 | ||
| 330 | + @support.cpython_only | ||
| 331 | + @threading_helper.requires_working_threading() | ||
| 332 | + def test_sneaky_frame_object_teardown(self): | ||
| 333 | + | ||
| 334 | + class SneakyDel: | ||
| 335 | + def __del__(self): | ||
| 336 | + """ | ||
| 337 | + Stash a reference to the entire stack for walking later. | ||
| 338 | + | ||
| 339 | + It may look crazy, but you'd be surprised how common this is | ||
| 340 | + when using a test runner (like pytest). The typical recipe is: | ||
| 341 | + ResourceWarning + -Werror + a custom sys.unraisablehook. | ||
| 342 | + """ | ||
| 343 | + nonlocal sneaky_frame_object | ||
| 344 | + sneaky_frame_object = sys._getframe() | ||
| 345 | + | ||
| 346 | + class SneakyThread(threading.Thread): | ||
| 347 | + """ | ||
| 348 | + A separate thread isn't needed to make this code crash, but it does | ||
| 349 | + make crashes more consistent, since it means sneaky_frame_object is | ||
| 350 | + backed by freed memory after the thread completes! | ||
| 351 | + """ | ||
| 352 | + | ||
| 353 | + def run(self): | ||
| 354 | + """Run SneakyDel.__del__ as this frame is popped.""" | ||
| 355 | + ref = SneakyDel() | ||
| 356 | + | ||
| 357 | + sneaky_frame_object = None | ||
| 358 | + t = SneakyThread() | ||
| 359 | + t.start() | ||
| 360 | + t.join() | ||
| 361 | + # sneaky_frame_object can be anything, really, but it's crucial that | ||
| 362 | + # SneakyThread.run's frame isn't anywhere on the stack while it's being | ||
| 363 | + # torn down: | ||
| 364 | + self.assertIsNotNone(sneaky_frame_object) | ||
| 365 | + while sneaky_frame_object is not None: | ||
| 366 | + self.assertIsNot( | ||
| 367 | + sneaky_frame_object.f_code, SneakyThread.run.__code__ | ||
| 368 | + ) | ||
| 369 | + sneaky_frame_object = sneaky_frame_object.f_back | ||
| 328 | 370 | ||
| 329 | 371 | if __name__ == "__main__": | |
| 330 | 372 | unittest.main() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + Fix an issue that could cause frames to be visible to Python code as they | ||
| 2 | + are being torn down, possibly leading to memory corruption or hard crashes | ||
| 3 | + of the interpreter. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1617,14 +1617,6 @@ trace_function_exit(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject | |||
| 1617 | 1617 | return 0; | |
| 1618 | 1618 | } | |
| 1619 | 1619 | ||
| 1620 | - static _PyInterpreterFrame * | ||
| 1621 | - pop_frame(PyThreadState *tstate, _PyInterpreterFrame *frame) | ||
| 1622 | - { | ||
| 1623 | - _PyInterpreterFrame *prev_frame = frame->previous; | ||
| 1624 | - _PyEvalFrameClearAndPop(tstate, frame); | ||
| 1625 | - return prev_frame; | ||
| 1626 | - } | ||
| 1627 | - | ||
| 1628 | 1620 | /* It is only between the PRECALL instruction and the following CALL, | |
| 1629 | 1621 | * that this has any meaning. | |
| 1630 | 1622 | */ | |
@@ -2441,7 +2433,10 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int | |||
| 2441 | 2433 | DTRACE_FUNCTION_EXIT(); | |
| 2442 | 2434 | _Py_LeaveRecursiveCallTstate(tstate); | |
| 2443 | 2435 | if (!frame->is_entry) { | |
| 2444 | - frame = cframe.current_frame = pop_frame(tstate, frame); | ||
| 2436 | + // GH-99729: We need to unlink the frame *before* clearing it: | ||
| 2437 | + _PyInterpreterFrame *dying = frame; | ||
| 2438 | + frame = cframe.current_frame = dying->previous; | ||
| 2439 | + _PyEvalFrameClearAndPop(tstate, dying); | ||
| 2445 | 2440 | _PyFrame_StackPush(frame, retval); | |
| 2446 | 2441 | goto resume_frame; | |
| 2447 | 2442 | } | |
@@ -5833,7 +5828,10 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int | |||
| 5833 | 5828 | assert(tstate->cframe->current_frame == frame->previous); | |
| 5834 | 5829 | return NULL; | |
| 5835 | 5830 | } | |
| 5836 | - frame = cframe.current_frame = pop_frame(tstate, frame); | ||
| 5831 | + // GH-99729: We need to unlink the frame *before* clearing it: | ||
| 5832 | + _PyInterpreterFrame *dying = frame; | ||
| 5833 | + frame = cframe.current_frame = dying->previous; | ||
| 5834 | + _PyEvalFrameClearAndPop(tstate, dying); | ||
| 5837 | 5835 | ||
| 5838 | 5836 | resume_with_error: | |
| 5839 | 5837 | SET_LOCALS_FROM_FRAME(); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -123,6 +123,9 @@ _PyFrame_Clear(_PyInterpreterFrame *frame) | |||
| 123 | 123 | * to have cleared the enclosing generator, if any. */ | |
| 124 | 124 | assert(frame->owner != FRAME_OWNED_BY_GENERATOR || | |
| 125 | 125 | _PyFrame_GetGenerator(frame)->gi_frame_state == FRAME_CLEARED); | |
| 126 | + // GH-99729: Clearing this frame can expose the stack (via finalizers). It's | ||
| 127 | + // crucial that this frame has been unlinked, and is no longer visible: | ||
| 128 | + assert(_PyThreadState_GET()->cframe->current_frame != frame); | ||
| 126 | 129 | if (frame->frame_obj) { | |
| 127 | 130 | PyFrameObject *f = frame->frame_obj; | |
| 128 | 131 | frame->frame_obj = NULL; | |
| Back | FazBrowse Home | New Git URL |
0 commit comments