Several type lifecycle bugs, all interrelated:
1. All 13 heap types missing Py_DECREF(Py_TYPE(self)) in dealloc
All types are created via PyType_FromSpec (heap types). None of the dealloc functions call Py_DECREF(Py_TYPE(self)), leaking one type reference per object destruction. Same systematic bug as kiwisolver (#223, 6/6) and atom (#254, 13/13). Missing since the heap type conversion in commit e17a4a03 (2019).
Reproducer:
import sys, gc
gc.disable()
from enaml.signaling import Signal
T = Signal
rc_before = sys.getrefcount(T)
for _ in range(500):
x = Signal()
del x
delta = sys.getrefcount(T) - rc_before
print(f"Signal: {delta} type refs leaked over 500 cycles")
# Signal: 500 type refs leaked over 500 cycles
Affected deallocs (all need PyTypeObject* tp = Py_TYPE(self); before free, Py_DECREF(tp); after):
| Type |
File |
Line |
| WeakMethod |
weakmethod.cpp |
211 |
| CallableRef |
callableref.cpp |
90 |
| Signal |
signaling.cpp |
157 |
| _Disconnector |
signaling.cpp |
371 |
| BoundSignal |
signaling.cpp |
595 |
| Alias |
alias.cpp |
71 |
| Color |
colorext.cpp |
74 |
| Font |
fontext.cpp |
112 |
| DFunc |
declarative_function.cpp |
165 |
| BoundDMethod |
declarative_function.cpp |
324 |
| Nonlocals |
dynamicscope.cpp |
337 |
| DynamicScope |
dynamicscope.cpp |
588 |
| SubscriptionObserver |
subscription_observer.cpp |
95 |
2. WinEnum dealloc uses PyObject_Del directly (winutil.cpp:51)
WinEnum's dealloc slot is set to PyObject_Del — bypasses Py_TYPE(self)->tp_free and never calls Py_DECREF(Py_TYPE(self)). Also, the MAKE_ENUM macro uses return NULL from an int-returning function (winutil_modexec), silently swallowing errors.
3. DFunc_dealloc missing PyObject_GC_UnTrack (declarative_function.cpp:165-169)
DFunc has Py_TPFLAGS_HAVE_GC but its dealloc does not call PyObject_GC_UnTrack before clearing members. Every other GC-tracked type in the codebase does this correctly. The GC may visit the object while it's being torn down.
4. SubscriptionObserver_traverse missing Py_VISIT(Py_TYPE(self)) and Py_VISIT(self->name) (subscription_observer.cpp:87-91)
Added in 2025, 5 years after commit afdbcc6e which added Py_VISIT(Py_TYPE(self)) to all other traverse functions. This module was written by a different author and missed the pattern. Also missing Py_VISIT(self->name) — the clear function does Py_CLEAR(self->name) but traverse does not visit it.
5. Freelist-reused BoundSignal and BoundDMethod objects not GC-tracked
After PyObject_GC_UnTrack in dealloc and freelist stash, reuse via _Py_NewReference does NOT call PyObject_GC_Track. Reused objects are invisible to GC — cycles involving them cannot be collected. The freelists also don't manage type references correctly (no Py_DECREF on dealloc, no Py_INCREF on reuse).
Recommended fix for freelists: remove them entirely — modern Python allocators (pymalloc, mimalloc in 3.12+) make the optimization negligible, and the interaction with heap type lifecycle is unsound.
Found by cext-review-toolkit.
Several type lifecycle bugs, all interrelated:
1. All 13 heap types missing
Py_DECREF(Py_TYPE(self))in deallocAll types are created via
PyType_FromSpec(heap types). None of the dealloc functions callPy_DECREF(Py_TYPE(self)), leaking one type reference per object destruction. Same systematic bug as kiwisolver (#223, 6/6) and atom (#254, 13/13). Missing since the heap type conversion in commite17a4a03(2019).Reproducer:
Affected deallocs (all need
PyTypeObject* tp = Py_TYPE(self);before free,Py_DECREF(tp);after):2.
WinEnumdealloc usesPyObject_Deldirectly (winutil.cpp:51)WinEnum's dealloc slot is set toPyObject_Del— bypassesPy_TYPE(self)->tp_freeand never callsPy_DECREF(Py_TYPE(self)). Also, theMAKE_ENUMmacro usesreturn NULLfrom anint-returning function (winutil_modexec), silently swallowing errors.3.
DFunc_deallocmissingPyObject_GC_UnTrack(declarative_function.cpp:165-169)DFunchasPy_TPFLAGS_HAVE_GCbut its dealloc does not callPyObject_GC_UnTrackbefore clearing members. Every other GC-tracked type in the codebase does this correctly. The GC may visit the object while it's being torn down.4.
SubscriptionObserver_traversemissingPy_VISIT(Py_TYPE(self))andPy_VISIT(self->name)(subscription_observer.cpp:87-91)Added in 2025, 5 years after commit
afdbcc6ewhich addedPy_VISIT(Py_TYPE(self))to all other traverse functions. This module was written by a different author and missed the pattern. Also missingPy_VISIT(self->name)— the clear function doesPy_CLEAR(self->name)but traverse does not visit it.5. Freelist-reused
BoundSignalandBoundDMethodobjects not GC-trackedAfter
PyObject_GC_UnTrackin dealloc and freelist stash, reuse via_Py_NewReferencedoes NOT callPyObject_GC_Track. Reused objects are invisible to GC — cycles involving them cannot be collected. The freelists also don't manage type references correctly (noPy_DECREFon dealloc, noPy_INCREFon reuse).Recommended fix for freelists: remove them entirely — modern Python allocators (pymalloc, mimalloc in 3.12+) make the optimization negligible, and the interaction with heap type lifecycle is unsound.
Found by cext-review-toolkit.