All 6 types in kiwisolver 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 6 dealloc functions call Py_DECREF(Py_TYPE(self)), leaking one type reference per object destruction.
Reproducer:
import sys, gc
import kiwisolver as ks
for name, factory in [
("Variable", lambda: ks.Variable("x")),
("Solver", lambda: ks.Solver()),
("Constraint", lambda: (ks.Variable("x") + 1 >= 0)),
]:
T = type(factory())
rc_before = sys.getrefcount(T)
for _ in range(1000):
del (factory())
gc.collect()
delta = sys.getrefcount(T) - rc_before
print(f"{name}: {delta} type refs leaked over 1000 cycles")
assert delta == 1000
# Variable: 1000 type refs leaked over 1000 cycles
# Solver: 1000 type refs leaked over 1000 cycles
# Constraint: 1000 type refs leaked over 1000 cycles
Affected files (all follow the same pattern — save tp before tp_free, decref after):
variable.cpp:84-92
term.cpp:68-73
expression.cpp:74-79
constraint.cpp:71-79
solver.cpp:37-43
strength.cpp:31-34
Example fix (Variable, same pattern for all 6):
void Variable_dealloc(Variable* self)
{
PyObject_GC_UnTrack(self);
Variable_clear(self);
ACQUIRE_GLOBAL_LOCK();
self->variable.~Variable();
RELEASE_GLOBAL_LOCK();
PyTypeObject* tp = Py_TYPE(self);
tp->tp_free(pyobject_cast(self));
Py_DECREF(tp);
}
Note: the tp_traverse functions already correctly Py_VISIT(Py_TYPE(self)) on 3.9+. Only the dealloc side is missing.
Found by cext-review-toolkit.
All 6 types in kiwisolver 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 6 dealloc functions callPy_DECREF(Py_TYPE(self)), leaking one type reference per object destruction.Reproducer:
Affected files (all follow the same pattern — save
tpbeforetp_free, decref after):variable.cpp:84-92term.cpp:68-73expression.cpp:74-79constraint.cpp:71-79solver.cpp:37-43strength.cpp:31-34Example fix (Variable, same pattern for all 6):
Note: the
tp_traversefunctions already correctlyPy_VISIT(Py_TYPE(self))on 3.9+. Only the dealloc side is missing.Found by cext-review-toolkit.