From 87e97ae4ad8942bc0cc09174d8964c3e02b6b03b Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Mon, 27 Jun 2022 16:56:37 +0100 Subject: [PATCH 01/11] Add test for creating frame object when making a generator. --- Lib/test/test_generators.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 87a7dd69d106c4..2e5bff9cc787fe 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -170,6 +170,21 @@ def f(): g.send(0) self.assertEqual(next(g), 1) + def test_handle_frame_object_in_creation(self): + + def cb(*args): + try: + sys._getframe(1) + except ValueError: + pass + + gc.set_threshold(1, 0, 0) + gc.callbacks.append(cb) + + def gen(): + yield 1 + + gen() class ExceptionTest(unittest.TestCase): # Tests for the issue #23353: check that the currently handled exception From 583d16470101800ceac6fc73bb46f37dc853404b Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Mon, 27 Jun 2022 19:39:36 +0100 Subject: [PATCH 02/11] Don't expose incomplete frames as frame objects. --- Include/internal/pycore_frame.h | 7 +++++++ Python/frame.c | 8 ++++++-- Python/sysmodule.c | 14 +++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Include/internal/pycore_frame.h b/Include/internal/pycore_frame.h index eed26fbb06218a..fc29063cbf24e2 100644 --- a/Include/internal/pycore_frame.h +++ b/Include/internal/pycore_frame.h @@ -210,6 +210,13 @@ PyGenObject *_PyFrame_GetGenerator(_PyInterpreterFrame *frame) return (PyGenObject *)(((char *)frame) - offset_in_gen); } + +static inline bool +_PyFrame_IsIncomplete(_PyInterpreterFrame *frame) +{ + return frame->prev_instr < _PyCode_CODE(frame->f_code) + frame->f_code->_co_firsttraceable; +} + #ifdef __cplusplus } #endif diff --git a/Python/frame.c b/Python/frame.c index 206ecab44c1b87..7c6705e208f3d3 100644 --- a/Python/frame.c +++ b/Python/frame.c @@ -68,9 +68,13 @@ take_ownership(PyFrameObject *f, _PyInterpreterFrame *frame) f->f_frame = frame; frame->owner = FRAME_OWNED_BY_FRAME_OBJECT; assert(f->f_back == NULL); - if (frame->previous != NULL) { + _PyInterpreterFrame *prev = frame->previous; + while (prev && _PyFrame_IsIncomplete(prev)) { + prev = prev->previous; + } + if (prev) { /* Link PyFrameObjects.f_back and remove link through _PyInterpreterFrame.previous */ - PyFrameObject *back = _PyFrame_GetFrameObject(frame->previous); + PyFrameObject *back = _PyFrame_GetFrameObject(prev); if (back == NULL) { /* Memory error here. */ assert(PyErr_ExceptionMatches(PyExc_MemoryError)); diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 444042f82d9473..93120071b400fd 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1776,9 +1776,17 @@ sys__getframe_impl(PyObject *module, int depth) return NULL; } - while (depth > 0 && frame != NULL) { - frame = frame->previous; - --depth; + if (frame != NULL) { + while (depth > 0) { + frame = frame->previous; + if (frame == NULL) { + break; + } + if (_PyFrame_IsIncomplete(frame)) { + continue; + } + --depth; + } } if (frame == NULL) { _PyErr_SetString(tstate, PyExc_ValueError, From 5043a8bd8bf501cf867fa1ad165058e4b23f0774 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 28 Jun 2022 10:08:17 +0100 Subject: [PATCH 03/11] Add news --- .../2022-06-28-10-08-06.gh-issue-94262.m-HWUZ.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-06-28-10-08-06.gh-issue-94262.m-HWUZ.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-06-28-10-08-06.gh-issue-94262.m-HWUZ.rst b/Misc/NEWS.d/next/Core and Builtins/2022-06-28-10-08-06.gh-issue-94262.m-HWUZ.rst new file mode 100644 index 00000000000000..7ba39bbcfe21da --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-06-28-10-08-06.gh-issue-94262.m-HWUZ.rst @@ -0,0 +1,3 @@ +Don't create frame objects for incomplete frames. Prevents the creation of +generators and closures from being observable to Python and C extensions, +restoring the behavior of 3.10 and earlier. From e81c1f35f32b8fe0c0a1a3b254241c5bb9d71371 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 28 Jun 2022 12:57:38 +0100 Subject: [PATCH 04/11] Cleanup after test. --- Lib/test/test_generators.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 2e5bff9cc787fe..068a92d9550a14 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -178,13 +178,18 @@ def cb(*args): except ValueError: pass - gc.set_threshold(1, 0, 0) - gc.callbacks.append(cb) - def gen(): yield 1 - gen() + thresholds = gc.get_threshold() + gc.callbacks.append(cb) + gc.set_threshold(1, 0, 0) + + try: + gen() + finally: + gc.set_threshold(*thresholds) + gc.callbacks.pop() class ExceptionTest(unittest.TestCase): # Tests for the issue #23353: check that the currently handled exception From 6d071657e154f627126b30b64696cd0692c15467 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 28 Jun 2022 13:02:06 +0100 Subject: [PATCH 05/11] Explain _PyFrame_IsIncomplete() --- Include/internal/pycore_frame.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Include/internal/pycore_frame.h b/Include/internal/pycore_frame.h index fc29063cbf24e2..2accf903f369bb 100644 --- a/Include/internal/pycore_frame.h +++ b/Include/internal/pycore_frame.h @@ -210,7 +210,10 @@ PyGenObject *_PyFrame_GetGenerator(_PyInterpreterFrame *frame) return (PyGenObject *)(((char *)frame) - offset_in_gen); } - +/* Determine whether a frame is incomplete. + * A frame is incomplete until the first RESUME instruction + * as it may be part way through creating cell objects or a + * generator or coroutine. */ static inline bool _PyFrame_IsIncomplete(_PyInterpreterFrame *frame) { From ce0d4b5b8d2255ab3e1ae2aaa2ec39e672f84d2a Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 28 Jun 2022 15:21:39 +0100 Subject: [PATCH 06/11] Try harder to break things --- Lib/test/test_generators.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 068a92d9550a14..d9b2f86882fa15 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -172,11 +172,11 @@ def f(): def test_handle_frame_object_in_creation(self): + #Attempt to expose partially constructed frames + #See https://github.com/python/cpython/issues/94262 + def cb(*args): - try: - sys._getframe(1) - except ValueError: - pass + inspect.stack() def gen(): yield 1 @@ -184,13 +184,29 @@ def gen(): thresholds = gc.get_threshold() gc.callbacks.append(cb) gc.set_threshold(1, 0, 0) - try: gen() finally: gc.set_threshold(*thresholds) gc.callbacks.pop() + class Sneaky: + def __del__(self): + raise KeyboardInterrupt + + sneaky = Sneaky() + sneaky._s = Sneaky() + sneaky._s._s = sneaky + del sneaky + + gc.set_threshold(1, 0, 0) + try: + gen() + finally: + gc.set_threshold(*thresholds) + + + class ExceptionTest(unittest.TestCase): # Tests for the issue #23353: check that the currently handled exception # is correctly saved/restored in PyEval_EvalFrameEx(). From f56451bc4aea748369d7ba28bff9a28b6e8e4e66 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 28 Jun 2022 15:22:13 +0100 Subject: [PATCH 07/11] Avoid creating frame objects for incomplete frames vai f_back. --- Objects/frameobject.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 9ff0443e4558f4..44cc06205a6237 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -1169,8 +1169,14 @@ PyFrame_GetBack(PyFrameObject *frame) { assert(frame != NULL); PyFrameObject *back = frame->f_back; - if (back == NULL && frame->f_frame->previous != NULL) { - back = _PyFrame_GetFrameObject(frame->f_frame->previous); + if (back == NULL) { + _PyInterpreterFrame *prev = frame->f_frame->previous; + while (prev && _PyFrame_IsIncomplete(prev)) { + prev = prev->previous; + } + if (prev) { + back = _PyFrame_GetFrameObject(prev); + } } Py_XINCREF(back); return back; From b69e8034d9ee549f5b7816b38056400b64dc6e92 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 28 Jun 2022 16:09:41 +0100 Subject: [PATCH 08/11] Fix test --- Lib/test/test_generators.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index d9b2f86882fa15..92f71397486c81 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -182,6 +182,7 @@ def gen(): yield 1 thresholds = gc.get_threshold() + gc.callbacks.append(cb) gc.set_threshold(1, 0, 0) try: @@ -197,11 +198,14 @@ def __del__(self): sneaky = Sneaky() sneaky._s = Sneaky() sneaky._s._s = sneaky - del sneaky gc.set_threshold(1, 0, 0) try: - gen() + del sneaky + try: + gen() + except KeyboardInterrupt: + pass finally: gc.set_threshold(*thresholds) From c648d46ec789782cade7541c5182fc476ce1fe52 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 28 Jun 2022 16:10:09 +0100 Subject: [PATCH 09/11] Assert frame is valid when creating frame object. --- Include/internal/pycore_frame.h | 27 +++++++++++++++++---------- Objects/codeobject.c | 11 +++++------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Include/internal/pycore_frame.h b/Include/internal/pycore_frame.h index 2accf903f369bb..994c205c3d1644 100644 --- a/Include/internal/pycore_frame.h +++ b/Include/internal/pycore_frame.h @@ -134,6 +134,21 @@ _PyFrame_SetStackPointer(_PyInterpreterFrame *frame, PyObject **stack_pointer) frame->stacktop = (int)(stack_pointer - frame->localsplus); } +/* Determine whether a frame is incomplete. + * A frame is incomplete if it is part way through + * creating cell objects or a generator or coroutine. + * + * Frames on the frame stack are incomplete until the + * first RESUME instruction. + * Frames owned by a generator are always complete. + */ +static inline bool +_PyFrame_IsIncomplete(_PyInterpreterFrame *frame) +{ + return frame->owner != FRAME_OWNED_BY_GENERATOR && + frame->prev_instr < _PyCode_CODE(frame->f_code) + frame->f_code->_co_firsttraceable; +} + /* For use by _PyFrame_GetFrameObject Do not call directly. */ PyFrameObject * @@ -145,6 +160,8 @@ _PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame); static inline PyFrameObject * _PyFrame_GetFrameObject(_PyInterpreterFrame *frame) { + + assert(!_PyFrame_IsIncomplete(frame)); PyFrameObject *res = frame->frame_obj; if (res != NULL) { return res; @@ -210,16 +227,6 @@ PyGenObject *_PyFrame_GetGenerator(_PyInterpreterFrame *frame) return (PyGenObject *)(((char *)frame) - offset_in_gen); } -/* Determine whether a frame is incomplete. - * A frame is incomplete until the first RESUME instruction - * as it may be part way through creating cell objects or a - * generator or coroutine. */ -static inline bool -_PyFrame_IsIncomplete(_PyInterpreterFrame *frame) -{ - return frame->prev_instr < _PyCode_CODE(frame->f_code) + frame->f_code->_co_firsttraceable; -} - #ifdef __cplusplus } #endif diff --git a/Objects/codeobject.c b/Objects/codeobject.c index c38c51b45321c1..956b2c7fb9c36f 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -638,11 +638,10 @@ PyCode_New(int argcount, int kwonlyargcount, exceptiontable); } -static const char assert0[4] = { - LOAD_ASSERTION_ERROR, - 0, - RAISE_VARARGS, - 1 +static const char assert0[6] = { + RESUME, 0, + LOAD_ASSERTION_ERROR, 0, + RAISE_VARARGS, 1 }; PyCodeObject * @@ -666,7 +665,7 @@ PyCode_NewEmpty(const char *filename, const char *funcname, int firstlineno) if (filename_ob == NULL) { goto failed; } - code_ob = PyBytes_FromStringAndSize(assert0, 4); + code_ob = PyBytes_FromStringAndSize(assert0, 6); if (code_ob == NULL) { goto failed; } From cf0b32c0216357a893e22d0fe93f744c55c49ec7 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Tue, 28 Jun 2022 16:15:06 +0100 Subject: [PATCH 10/11] Prevent test polluting environment --- Lib/test/test_generators.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 92f71397486c81..12ae43e17a54e4 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -193,7 +193,10 @@ def gen(): class Sneaky: def __del__(self): - raise KeyboardInterrupt + try: + raise KeyboardInterrupt + except: + pass sneaky = Sneaky() sneaky._s = Sneaky() @@ -202,10 +205,7 @@ def __del__(self): gc.set_threshold(1, 0, 0) try: del sneaky - try: - gen() - except KeyboardInterrupt: - pass + gen() finally: gc.set_threshold(*thresholds) From 939769cab192429916417b8176b8eea069efecee Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Wed, 29 Jun 2022 11:19:53 +0100 Subject: [PATCH 11/11] Create all frames on stack from finalizer. --- Lib/test/test_generators.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 12ae43e17a54e4..e5aa7da1e0df13 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -193,10 +193,7 @@ def gen(): class Sneaky: def __del__(self): - try: - raise KeyboardInterrupt - except: - pass + inspect.stack() sneaky = Sneaky() sneaky._s = Sneaky()