Three bugs in declarative_function.cpp:
1. super_disallowed reference leak on every _Invoke call (line 113)
cppy::incref(super_disallowed) is called before PyMapping_SetItemString, which does NOT steal references. The extra incref is never released. Leaks one reference per declarative function/method invocation.
Fix: pass super_disallowed directly without the incref:
if( PyMapping_SetItemString( scope.get(), "super", super_disallowed ) == -1 )
return cppy::system_error( "Failed to set key super in dynamic scope" );
2. Cascading NULL dereferences in _Invoke (lines 106-119)
Three consecutive API calls have their return values used without NULL checks:
PyDict_New() (line 106) — OOM → NULL
PyObject_CallMethod(...) (line 107) — called with NULL empty.get() argument
PyObject_CallFunctionObjArgs(...) (lines 108-112) — called with NULL f_locals.get()
cppy::ptr wraps NULL without crashing, but passes NULL to the next API call. Eventually PyMapping_SetItemString(scope.get(), ...) dereferences the NULL scope.
Fix: add NULL checks after each failable call:
cppy::ptr empty( PyDict_New() );
if( !empty ) return 0;
cppy::ptr f_locals( PyObject_CallMethod( d_storage.get(), "get", "OO", key, empty.get() ) );
if( !f_locals ) return 0;
cppy::ptr scope( PyObject_CallFunctionObjArgs( DynamicScope, self,
f_locals.get(), f_globals.get(), f_builtins.get(), 0 ) );
if( !scope ) return 0;
3. DFunc_new leaks self on argument parse failure (lines 126-139)
PyType_GenericNew allocates self, then PyArg_ParseTupleAndKeywords is called. If parsing fails or !PyFunction_Check(im_func), the function returns without Py_DECREF(self).
Reproducer:
import gc; gc.disable()
import os, tracemalloc
tracemalloc.start()
from enaml.core.declarative_function import DeclarativeFunction
gc.collect()
before = tracemalloc.get_traced_memory()[0]
for _ in range(500):
try:
DeclarativeFunction("not_a_function", "key")
except TypeError:
pass
gc.collect()
after = tracemalloc.get_traced_memory()[0]
print(f"Leaked {after - before} bytes over 500 bad calls (~{(after - before) // 500} bytes/call)")
os._exit(0)
# Leaked 24272 bytes over 500 bad calls (~48 bytes/call)
Fix: parse arguments before allocating, or wrap self in cppy::ptr.
Found by cext-review-toolkit.
Three bugs in
declarative_function.cpp:1.
super_disallowedreference leak on every_Invokecall (line 113)cppy::incref(super_disallowed)is called beforePyMapping_SetItemString, which does NOT steal references. The extra incref is never released. Leaks one reference per declarative function/method invocation.Fix: pass
super_disalloweddirectly without the incref:2. Cascading NULL dereferences in
_Invoke(lines 106-119)Three consecutive API calls have their return values used without NULL checks:
PyDict_New()(line 106) — OOM → NULLPyObject_CallMethod(...)(line 107) — called with NULLempty.get()argumentPyObject_CallFunctionObjArgs(...)(lines 108-112) — called with NULLf_locals.get()cppy::ptrwraps NULL without crashing, but passes NULL to the next API call. EventuallyPyMapping_SetItemString(scope.get(), ...)dereferences the NULL scope.Fix: add NULL checks after each failable call:
3.
DFunc_newleaks self on argument parse failure (lines 126-139)PyType_GenericNewallocatesself, thenPyArg_ParseTupleAndKeywordsis called. If parsing fails or!PyFunction_Check(im_func), the function returns withoutPy_DECREF(self).Reproducer:
Fix: parse arguments before allocating, or wrap
selfincppy::ptr.Found by cext-review-toolkit.