bpo-25782: avoid hang in PyErr_SetObject when current exception has a cycle in its context chain (GH-27626)


Co-authored-by: Dennis Sweeney 36520290+sweeneyde@users.noreply.github.com
(cherry picked from commit d5c217475c4957a8084ac3f92ae012ece5edc7cb)

Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
diff --git a/Python/errors.c b/Python/errors.c
index d4b9db1..9944c3a 100644
--- a/Python/errors.c
+++ b/Python/errors.c
@@ -148,12 +148,16 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
             value = fixed_value;
         }
 
-        /* Avoid reference cycles through the context chain.
+        /* Avoid creating new reference cycles through the
+           context chain, while taking care not to hang on
+           pre-existing ones.
            This is O(chain length) but context chains are
            usually very short. Sensitive readers may try
            to inline the call to PyException_GetContext. */
         if (exc_value != value) {
             PyObject *o = exc_value, *context;
+            PyObject *slow_o = o;  /* Floyd's cycle detection algo */
+            int slow_update_toggle = 0;
             while ((context = PyException_GetContext(o))) {
                 Py_DECREF(context);
                 if (context == value) {
@@ -161,6 +165,16 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
                     break;
                 }
                 o = context;
+                if (o == slow_o) {
+                    /* pre-existing cycle - all exceptions on the
+                       path were visited and checked.  */
+                    break;
+                }
+                if (slow_update_toggle) {
+                    slow_o = PyException_GetContext(slow_o);
+                    Py_DECREF(slow_o);
+                }
+                slow_update_toggle = !slow_update_toggle;
             }
             PyException_SetContext(value, exc_value);
         }