From 5d329534423f66876471805b852f12ef16c931c1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 30 May 2020 23:30:54 +0300 Subject: [PATCH 1/2] bpo-25782: Prevent cycles in the __context__ chain. Setting __context__ to self has no any effect. If set __context__ to the chain contained original exception, it is moved to the start of the context chain. --- Doc/builtins/exceptions.rst | 6 ++++++ Doc/c-api/exceptions.rst | 4 ++++ Lib/test/test_exceptions.py | 20 ++++++++++++++++++++ Objects/exceptions.c | 28 +++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/Doc/builtins/exceptions.rst b/Doc/builtins/exceptions.rst index ecf62fb6391b1b..680863cebfcdaf 100644 --- a/Doc/builtins/exceptions.rst +++ b/Doc/builtins/exceptions.rst @@ -100,6 +100,12 @@ attribute, as well as due to possible memory layout incompatibilities. conflicts in the future. Therefore, it's recommended to avoid subclassing multiple exception types altogether. +.. versionchanged:: 3.5.2 + + Setting :attr:`__context__` to self has no any effect. If set + ``__context__`` to the chain contained original exception, it is moved to + the start of the context chain. + Base classes ------------ diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index 0c8d14dc134ebc..8a1de127ec2870 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -888,6 +888,10 @@ Exception Objects it. There is no type check to make sure that *ctx* is an exception instance. This ":term:`steals `" a reference to *ctx*. + .. versionchanged:: 3.5.2 + If *ctx* is *ex*, the function has no any effect. If *ex* is in the + context chain started from *ctx*, it is moved to the start of the chain. + .. c:function:: PyObject* PyException_GetCause(PyObject *ex) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 963da1b0eae27c..bcb29cb448d5f7 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -987,6 +987,26 @@ def __exit__ (self, exc_type, exc_value, exc_tb): obj = wr() self.assertIsNone(obj) + def test_context_loop(self): + exc1 = Exception(1) + exc1.__context__ = exc1 + self.assertIsNone(exc1.__context__) + + exc2 = Exception(2) + exc2.__context__ = exc1 + self.assertIs(exc2.__context__, exc1) + exc1.__context__ = exc2 + self.assertIs(exc1.__context__, exc2) + self.assertIsNone(exc2.__context__) + + exc3 = Exception(3) + exc3.__context__ = exc1 + self.assertIs(exc3.__context__, exc1) + exc1.__context__ = exc3 + self.assertIs(exc1.__context__, exc3) + self.assertIs(exc3.__context__, exc2) + self.assertIsNone(exc2.__context__) + def test_exception_target_in_nested_scope(self): # issue 4617: This used to raise a SyntaxError # "can not delete variable 'e' referenced in nested scope" diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 781b5142ec326e..2a31a6a7ad3204 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -551,8 +551,34 @@ PyException_GetContext(PyObject *self) void PyException_SetContext(PyObject *self, PyObject *context) { + assert(PyExceptionInstance_Check(self)); + assert(context == NULL || PyExceptionInstance_Check(context)); + Py_BEGIN_CRITICAL_SECTION(self); - Py_XSETREF(PyBaseExceptionObject_CAST(self)->context, context); + PyBaseExceptionObject *base_self = PyBaseExceptionObject_CAST(self); + + PyObject *exc = context; + PyObject *prev_exc = NULL; + + while (exc != NULL) { + if (exc == self) { + if (prev_exc != NULL) { + /* Move self to the start of the chain. */ + PyBaseExceptionObject_CAST(prev_exc)->context = + base_self->context; + base_self->context = context; + } + assert(Py_REFCNT(self) > 1); + Py_END_CRITICAL_SECTION(); + Py_DECREF(self); + return; + } + assert(PyExceptionInstance_Check(exc)); + prev_exc = exc; + exc = PyBaseExceptionObject_CAST(exc)->context; + } + + Py_XSETREF(base_self->context, context); Py_END_CRITICAL_SECTION(); } From 167dab3ebbe8f331ab95ee6729d1c4686578133c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 21 Oct 2026 03:28:47 +0200 Subject: [PATCH 2/2] Fix CI: repair critical-section scoping in PyException_SetContext (gh-69968) Py_END_CRITICAL_SECTION() emits a closing brace, so calling it inside the nested if/while early-return path closed the wrong block and left 'base_self' undeclared after the loop. Restructure to a single exit: record the cycle with a flag, then release the lock before Py_DECREF. Also add the missing news entry. Co-authored-by: Muse Code (Meta Muse Spark) --- ...6-10-21-00-00-00.gh-issue-69968.3kX7mQ.rst | 3 +++ Objects/exceptions.c | 20 ++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-00-00-00.gh-issue-69968.3kX7mQ.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-00-00-00.gh-issue-69968.3kX7mQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-00-00-00.gh-issue-69968.3kX7mQ.rst new file mode 100644 index 00000000000000..c6ac440110b4ac --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-00-00-00.gh-issue-69968.3kX7mQ.rst @@ -0,0 +1,3 @@ +Setting :attr:`~BaseException.__context__` to the exception itself now has no +effect, and setting it to a chain containing the exception moves the +exception to the start of the chain, preventing cycles. diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 2a31a6a7ad3204..e9e5b1fd3c804a 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -554,6 +554,8 @@ PyException_SetContext(PyObject *self, PyObject *context) assert(PyExceptionInstance_Check(self)); assert(context == NULL || PyExceptionInstance_Check(context)); + int self_in_chain = 0; + Py_BEGIN_CRITICAL_SECTION(self); PyBaseExceptionObject *base_self = PyBaseExceptionObject_CAST(self); @@ -568,18 +570,26 @@ PyException_SetContext(PyObject *self, PyObject *context) base_self->context; base_self->context = context; } - assert(Py_REFCNT(self) > 1); - Py_END_CRITICAL_SECTION(); - Py_DECREF(self); - return; + self_in_chain = 1; + break; } assert(PyExceptionInstance_Check(exc)); prev_exc = exc; exc = PyBaseExceptionObject_CAST(exc)->context; } - Py_XSETREF(base_self->context, context); + if (self_in_chain) { + /* Self was already in the chain; drop the lost link reference. */ + assert(Py_REFCNT(self) > 1); + } + else { + Py_XSETREF(base_self->context, context); + } Py_END_CRITICAL_SECTION(); + + if (self_in_chain) { + Py_DECREF(self); + } } PyObject *