Several related module init / global state hygiene issues. None cause crashes in normal single-interpreter use, but they affect correctness under reimport, subinterpreters, and error paths.
1. Exception globals leaked on module re-exec
init_exceptions() in solver.cpp:330-375 stores new references from mod.getattr() into bare PyObject* globals. These are never decref'd. If Py_mod_exec runs again (subinterpreters), the old references leak. The module has no m_clear/m_free that would release them.
2. Type object globals become borrowed after PyModule_AddObject steals
In add_objects() (kiwisolver.cpp:94-130), PyModule_AddObject steals the reference from PyType_FromSpec. After success, the static TypeObject pointers hold borrowed references from the module dict. If the module is ever unloaded, these become dangling. Using PyModule_AddObjectRef (available since 3.10) would let the globals retain their own strong reference.
3. init_exceptions() leaks on partial failure
If the 4th getattr() fails in init_exceptions(), the first 3 globals hold leaked new references. The function returns false without cleaning up previously-assigned globals. Fix: use cppy::ptr for all 6, commit to globals only after all succeed.
4. ready_types() leaks on partial failure
If the 3rd Ready() call fails, the first 2 type objects (from PyType_FromSpec) leak. Same pattern — no cleanup of previously-created types on error.
5. global_lock defined unconditionally
kiwisolver.cpp:17 defines std::recursive_mutex global_lock outside any #ifdef Py_GIL_DISABLED guard, even though the macros that use it are no-ops without Py_GIL_DISABLED. The extern declaration in util.h IS correctly guarded. The definition should match:
#ifdef Py_GIL_DISABLED
std::recursive_mutex global_lock;
#endif
Found by cext-review-toolkit.
Several related module init / global state hygiene issues. None cause crashes in normal single-interpreter use, but they affect correctness under reimport, subinterpreters, and error paths.
1. Exception globals leaked on module re-exec
init_exceptions()insolver.cpp:330-375stores new references frommod.getattr()into barePyObject*globals. These are never decref'd. IfPy_mod_execruns again (subinterpreters), the old references leak. The module has nom_clear/m_freethat would release them.2. Type object globals become borrowed after PyModule_AddObject steals
In
add_objects()(kiwisolver.cpp:94-130),PyModule_AddObjectsteals the reference fromPyType_FromSpec. After success, the staticTypeObjectpointers hold borrowed references from the module dict. If the module is ever unloaded, these become dangling. UsingPyModule_AddObjectRef(available since 3.10) would let the globals retain their own strong reference.3. init_exceptions() leaks on partial failure
If the 4th
getattr()fails ininit_exceptions(), the first 3 globals hold leaked new references. The function returnsfalsewithout cleaning up previously-assigned globals. Fix: usecppy::ptrfor all 6, commit to globals only after all succeed.4. ready_types() leaks on partial failure
If the 3rd
Ready()call fails, the first 2 type objects (fromPyType_FromSpec) leak. Same pattern — no cleanup of previously-created types on error.5. global_lock defined unconditionally
kiwisolver.cpp:17definesstd::recursive_mutex global_lockoutside any#ifdef Py_GIL_DISABLEDguard, even though the macros that use it are no-ops withoutPy_GIL_DISABLED. Theexterndeclaration inutil.hIS correctly guarded. The definition should match:Found by cext-review-toolkit.