Skip to content

All 13 heap type deallocs missing Py_DECREF(Py_TYPE(self)) + DefaultAtomDict::Ready missing Py_INCREF #254

@devdanzin

Description

@devdanzin

All 13 types in atom are created via PyType_FromSpec (heap types). Heap type instances hold a strong reference to their type object, which must be released in tp_dealloc. None of the 13 dealloc functions call Py_DECREF(Py_TYPE(self)), leaking one type reference per object destruction. The tp_traverse functions correctly Py_VISIT(Py_TYPE(self)) (added in commit 8df1418), but the corresponding dealloc decref was never added (since the heap type conversion in commit 8b5fe85, 2019).

Reproducer (release build — silent leak):

import sys, gc
gc.disable()
from atom.datastructures.sortedmap import sortedmap

T = type(sortedmap())
rc_before = sys.getrefcount(T)
for _ in range(500):
    x = sortedmap()
    del x
delta = sys.getrefcount(T) - rc_before
print(f"SortedMap: {delta} type refs leaked over 500 cycles")
# SortedMap: 500 type refs leaked over 500 cycles

Additionally, DefaultAtomDict::Ready() at atomdict.cpp:458-459 creates a bases tuple with PyTuple_SET_ITEM(bases, 0, AtomDict::TypeObject) without Py_INCREF first. The bases tuple is never freed (leaked). PyType_FromSpecWithBases creates its own references to the base class correctly, but the leaked tuple remains GC-tracked and visits atomdict during GC traversal. This creates a refcount/gc_refs mismatch: atomdict has refcount=10 but 11 GC-tracked referrers. During subtract_refs, gc_refs goes to -1, triggering a fatal gc_decref assertion on debug builds — even just importing atom and exiting crashes.

Debug build crash (just import + exit):

Python/gc.c:99: gc_decref: Assertion "gc_get_refs(g) > 0" failed: refcount is too small
object refcount : 10
object type name: type
object repr     : <class 'atom.catom.atomdict'>
Fatal Python error: _PyObject_AssertFailed
Aborted (core dumped)

Verification (release build — confirms the mismatch):

import sys, gc
gc.collect()
import atom.catom as catom
atomdict = catom.atomdict
rc = sys.getrefcount(atomdict) - 1  # 10
gc_referrers = len([r for r in gc.get_referrers(atomdict) if gc.is_tracked(r)])  # 11
print(f"refcount={rc}, GC referrers={gc_referrers}")
# refcount=10, GC referrers=11

Affected dealloc functions (all need PyTypeObject* tp = Py_TYPE(self); before free, then Py_DECREF(tp); after):

Type File Line
CAtom catom.cpp 134
Member member.cpp 98
AtomList atomlist.cpp 300
AtomCList atomlist.cpp 1037
AtomDict atomdict.cpp 115
DefaultAtomDict atomdict.cpp 250
AtomSet atomset.cpp 123
AtomRef atomref.cpp 90
SortedMap sortedmap.cpp 365
EventBinder eventbinder.cpp 48
SignalConnector signalconnector.cpp 47
MethodWrapper methodwrapper.cpp 28
AtomMethodWrapper methodwrapper.cpp 134

For freelist types (EventBinder, SignalConnector), the decref goes only in the actual-free branch.

DefaultAtomDict::Ready fix (atomdict.cpp:458-459):

cppy::ptr bases( PyTuple_New( 1 ) );
Py_INCREF( pyobject_cast( AtomDict::TypeObject ) );
PyTuple_SET_ITEM( bases.get(), 0, pyobject_cast( AtomDict::TypeObject ) );
TypeObject = pytype_cast( PyType_FromSpecWithBases( &TypeObject_Spec, bases.get() ) );

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