Skip to content

All 13+ heap type deallocs missing Py_DECREF(Py_TYPE(self)) + DFunc missing GC_UnTrack + SubscriptionObserver traverse incomplete + freelist GC gaps #598

Description

@devdanzin

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions