From 23d96a68599ef2d4650dc21cc9283a9b36468b7a Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Fri, 8 Jul 2022 18:52:26 -0700 Subject: [PATCH] GH-93252: Fix error handling for failed Python calls (GH-94693) (cherry picked from commit 8a285df806816805484fed36dce5fd5b77a215a6) Co-authored-by: Brandt Bucher --- Lib/test/test_call.py | 12 ++++++++++++ .../2022-07-08-11-44-45.gh-issue-93252.i2358c.rst | 2 ++ Python/ceval.c | 6 +++++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-07-08-11-44-45.gh-issue-93252.i2358c.rst diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 6936f093e3db19..07355e8fa0616c 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -26,6 +26,18 @@ def fn(**kw): self.assertIsInstance(res, dict) self.assertEqual(list(res.items()), expected) + def test_frames_are_popped_after_failed_calls(self): + # GH-93252: stuff blows up if we don't pop the new frame after + # recovering from failed calls: + def f(): + pass + for _ in range(1000): + try: + f(None) + except TypeError: + pass + # BOOM! + @cpython_only class CFunctionCallsErrorMessages(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-07-08-11-44-45.gh-issue-93252.i2358c.rst b/Misc/NEWS.d/next/Core and Builtins/2022-07-08-11-44-45.gh-issue-93252.i2358c.rst new file mode 100644 index 00000000000000..1cc2d8560e53e5 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-07-08-11-44-45.gh-issue-93252.i2358c.rst @@ -0,0 +1,2 @@ +Fix an issue that caused internal frames to outlive failed Python function +calls, possibly resulting in memory leaks or hard interpreter crashes. diff --git a/Python/ceval.c b/Python/ceval.c index 1a5454570518ff..a24ca6fedf0380 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -6370,7 +6370,7 @@ _PyEvalFramePushAndInit(PyThreadState *tstate, PyFunctionObject *func, } if (initialize_locals(tstate, func, localsarray, args, argcount, kwnames)) { assert(frame->owner != FRAME_OWNED_BY_GENERATOR); - _PyFrame_Clear(frame); + _PyEvalFrameClearAndPop(tstate, frame); return NULL; } return frame; @@ -6392,6 +6392,10 @@ _PyEvalFramePushAndInit(PyThreadState *tstate, PyFunctionObject *func, static void _PyEvalFrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame * frame) { + // Make sure that this is, indeed, the top frame. We can't check this in + // _PyThreadState_PopFrame, since f_code is already cleared at that point: + assert((PyObject **)frame + frame->f_code->co_framesize == + tstate->datastack_top); tstate->recursion_remaining--; assert(frame->frame_obj == NULL || frame->frame_obj->f_frame == frame); assert(frame->owner == FRAME_OWNED_BY_THREAD);