From 1d028bbb1a1811e976efa8262d713485b70dc437 Mon Sep 17 00:00:00 2001
From: Scott Talbert <swt@techie.net>
Date: Thu, 17 Sep 2026 11:18:02 -0400
Subject: [PATCH 2/3] Preserve pending exception across CurlMulti dealloc
 cleanup

do_multi_dealloc() can run while an exception is already propagating,
e.g. when a CurlMulti subclass's __init__ rejects its arguments: the
freshly constructed object's refcount drops to zero and it is torn
down immediately, before the TypeError finishes unwinding. The
cleanup in util_multi_detach_easies() calls back into Python (via
PySequence_List() on easy_object_refs, and close() on any attached
easy handles), which must not run with that exception still set.

On a Python interpreter built with assertions enabled, this trips
_Py_CheckSlotResult and aborts the process (fixes #1071). Save and
restore the pending exception around the dealloc cleanup, matching
the existing pattern in easycb.c.
---
 src/multi.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/multi.c b/src/multi.c
index 170bfa098..ae4564116 100644
--- a/src/multi.c
+++ b/src/multi.c
@@ -216,6 +216,15 @@ util_multi_detach_easies(CurlMultiObject *self, int close_handles, int swallow_e
 PYCURL_INTERNAL void
 do_multi_dealloc(CurlMultiObject *self)
 {
+    /* tp_dealloc can run while an exception is propagating (e.g. when
+     * __init__ of a subclass rejects our arguments and the freshly
+     * constructed object is immediately discarded). The cleanup below
+     * calls back into Python (iterating easy_object_refs, calling the
+     * easy objects' close() method), which must not observe that
+     * exception, so stash it for the duration. */
+    PyObject *exc_type, *exc_val, *exc_tb;
+    PyErr_Fetch(&exc_type, &exc_val, &exc_tb);
+
     PyObject_GC_UnTrack(self);
     Py_TRASHCAN_BEGIN(self, do_multi_dealloc);
 
@@ -235,6 +244,8 @@ do_multi_dealloc(CurlMultiObject *self)
 
     CurlMulti_Type.tp_free(self);
     Py_TRASHCAN_END
+
+    PyErr_Restore(exc_type, exc_val, exc_tb);
 }
 
 
