Skip to content

declarative_function: super_disallowed ref leak per call + _Invoke cascading NULL + DFunc_new leaks self on bad args #602

Description

@devdanzin

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.

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