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.
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 intp_dealloc. None of the 13 dealloc functions callPy_DECREF(Py_TYPE(self)), leaking one type reference per object destruction. Thetp_traversefunctions correctlyPy_VISIT(Py_TYPE(self))(added in commit8df1418), but the corresponding dealloc decref was never added (since the heap type conversion in commit8b5fe85, 2019).Reproducer (release build — silent leak):
Additionally,
DefaultAtomDict::Ready()atatomdict.cpp:458-459creates a bases tuple withPyTuple_SET_ITEM(bases, 0, AtomDict::TypeObject)withoutPy_INCREFfirst. The bases tuple is never freed (leaked).PyType_FromSpecWithBasescreates its own references to the base class correctly, but the leaked tuple remains GC-tracked and visitsatomdictduring GC traversal. This creates a refcount/gc_refs mismatch:atomdicthas refcount=10 but 11 GC-tracked referrers. Duringsubtract_refs,gc_refsgoes to -1, triggering a fatalgc_decrefassertion on debug builds — even just importing atom and exiting crashes.Debug build crash (just import + exit):
Verification (release build — confirms the mismatch):
Affected dealloc functions (all need
PyTypeObject* tp = Py_TYPE(self);before free, thenPy_DECREF(tp);after):For freelist types (EventBinder, SignalConnector), the decref goes only in the actual-free branch.
DefaultAtomDict::Ready fix (
atomdict.cpp:458-459):Found by cext-review-toolkit.