Skip to content

Commit 364ed94

Browse files
authored
gh-89373: _Py_Dealloc() checks tp_dealloc exception (#32357)
If Python is built in debug mode, _Py_Dealloc() now ensures that the tp_dealloc function leaves the current exception unchanged.
1 parent 8a4e519 commit 364ed94

File tree

4 files changed

+50
-3
lines changed

4 files changed

+50
-3
lines changed

Doc/using/configure.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ Effects of a debug build:
288288
to detect usage of uninitialized objects.
289289
* Ensure that functions which can clear or replace the current exception are
290290
not called with an exception raised.
291+
* Check that deallocator functions don't change the current exception.
291292
* The garbage collector (:func:`gc.collect` function) runs some basic checks
292293
on objects consistency.
293294
* The :c:macro:`Py_SAFE_DOWNCAST()` macro checks for integer underflow and

Lib/test/test_capi.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,12 @@ def __del__(self):
516516
del subclass_instance
517517

518518
# Test that setting __class__ modified the reference counts of the types
519-
self.assertEqual(type_refcnt - 1, B.refcnt_in_del)
519+
if Py_DEBUG:
520+
# gh-89373: In debug mode, _Py_Dealloc() keeps a strong reference
521+
# to the type while calling tp_dealloc()
522+
self.assertEqual(type_refcnt, B.refcnt_in_del)
523+
else:
524+
self.assertEqual(type_refcnt - 1, B.refcnt_in_del)
520525
self.assertEqual(new_type_refcnt + 1, A.refcnt_in_del)
521526

522527
# Test that the original type already has decreased its refcnt
@@ -581,7 +586,12 @@ def test_c_subclass_of_heap_ctype_with_del_modifying_dunder_class_only_decrefs_o
581586
del subclass_instance
582587

583588
# Test that setting __class__ modified the reference counts of the types
584-
self.assertEqual(type_refcnt - 1, _testcapi.HeapCTypeSubclassWithFinalizer.refcnt_in_del)
589+
if Py_DEBUG:
590+
# gh-89373: In debug mode, _Py_Dealloc() keeps a strong reference
591+
# to the type while calling tp_dealloc()
592+
self.assertEqual(type_refcnt, _testcapi.HeapCTypeSubclassWithFinalizer.refcnt_in_del)
593+
else:
594+
self.assertEqual(type_refcnt - 1, _testcapi.HeapCTypeSubclassWithFinalizer.refcnt_in_del)
585595
self.assertEqual(new_type_refcnt + 1, _testcapi.HeapCTypeSubclass.refcnt_in_del)
586596

587597
# Test that the original type already has decreased its refcnt
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
If Python is built in debug mode, Python now ensures that deallocator
2+
functions leave the current exception unchanged. Patch by Victor Stinner.

Objects/object.c

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2354,11 +2354,45 @@ _PyObject_AssertFailed(PyObject *obj, const char *expr, const char *msg,
23542354
void
23552355
_Py_Dealloc(PyObject *op)
23562356
{
2357-
destructor dealloc = Py_TYPE(op)->tp_dealloc;
2357+
PyTypeObject *type = Py_TYPE(op);
2358+
destructor dealloc = type->tp_dealloc;
2359+
#ifdef Py_DEBUG
2360+
PyThreadState *tstate = _PyThreadState_GET();
2361+
PyObject *old_exc_type = tstate->curexc_type;
2362+
// Keep the old exception type alive to prevent undefined behavior
2363+
// on (tstate->curexc_type != old_exc_type) below
2364+
Py_XINCREF(old_exc_type);
2365+
// Make sure that type->tp_name remains valid
2366+
Py_INCREF(type);
2367+
#endif
2368+
23582369
#ifdef Py_TRACE_REFS
23592370
_Py_ForgetReference(op);
23602371
#endif
23612372
(*dealloc)(op);
2373+
2374+
#ifdef Py_DEBUG
2375+
// gh-89373: The tp_dealloc function must leave the current exception
2376+
// unchanged.
2377+
if (tstate->curexc_type != old_exc_type) {
2378+
const char *err;
2379+
if (old_exc_type == NULL) {
2380+
err = "Deallocator of type '%s' raised an exception";
2381+
}
2382+
else if (tstate->curexc_type == NULL) {
2383+
err = "Deallocator of type '%s' cleared the current exception";
2384+
}
2385+
else {
2386+
// It can happen if dealloc() normalized the current exception.
2387+
// A deallocator function must not change the current exception,
2388+
// not even normalize it.
2389+
err = "Deallocator of type '%s' overrode the current exception";
2390+
}
2391+
_Py_FatalErrorFormat(__func__, err, type->tp_name);
2392+
}
2393+
Py_XDECREF(old_exc_type);
2394+
Py_DECREF(type);
2395+
#endif
23622396
}
23632397

23642398

0 commit comments

Comments
 (0)