Repository: cython/cython

Issue:
[BUG] Reference count leak with memory view arguments
**Describe the bug**
When a function has multiple memory view arguments, and the argument call fails because of a type-mismatch then it can leak references to original array being viewed.

**To Reproduce**
In `memleak.pyx`:
```cython
def foo(double[:] a, double[:] b):
    pass
```

and in `repro.py`:
```python
import sys
import numpy as np
from memleak import foo

a = np.random.rand(100)
b = np.random.randint(100)

for i in range(1000):
    try:
        foo(a, b)
    except:
        pass

    if i % 100 == 0:
        print(sys.getrefcount(a), sys.getrefcount(b))
```
`foo` is called with a valid array for `a` but an invalid `b`. The result is `a`'s refcount is constantly increasing:
```
4 12
204 12
404 12
604 12
804 12
1004 12
1204 12
1404 12
1604 12
1804 12
```

**Expected behavior**
The refcount of `a` should be constant, like it is for `b`.

**Environment (please complete the following information):**
 - OS: Linux
 - Python version 3.8.8
 - Cython version 0.29.23



Discussion:
I think the problem is that the cleanup:

```
__PYX_XCLEAR_MEMVIEW(&__pyx_v_a, 1);
__PYX_XCLEAR_MEMVIEW(&__pyx_v_b, 1);
```

is inside the implementation function foo rather than the wrapper function. Since it fails in the wrapper function and doesn't get to the implementation function then the leak occurs.

It has the signs of "might be fiddly to fix". I think it only applies to memoryviews, but in principle could affect any ref-counted, non-pyobject type added in future

Resolving patch:
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -3740,6 +3740,11 @@ def generate_argument_parsing_code(self, env, code):
                         code.put_var_xdecref_clear(self.starstar_arg.entry)
                     else:
                         code.put_var_decref_clear(self.starstar_arg.entry)
+            for arg in self.args:
+                if not arg.type.is_pyobject and arg.type.needs_refcounting:
+                    # at the moment this just catches memoryviewslices, but in future
+                    # other non-PyObject reference counted types might need cleanup
+                    code.put_var_xdecref(arg.entry)
             code.put_add_traceback(self.target.entry.qualified_name)
             code.put_finish_refcount_context()
             code.putln("return %s;" % self.error_value())


Test patch:
diff --git a/tests/memoryview/memoryview.pyx b/tests/memoryview/memoryview.pyx
--- a/tests/memoryview/memoryview.pyx
+++ b/tests/memoryview/memoryview.pyx
@@ -1172,3 +1172,36 @@ def test_assign_from_byteslike(byteslike):
         return (<unsigned char*>buf)[:5]
     finally:
         free(buf)
+
+def multiple_memoryview_def(double[:] a, double[:] b):
+    return a[0] + b[0]
+
+cpdef multiple_memoryview_cpdef(double[:] a, double[:] b):
+    return a[0] + b[0]
+
+cdef multiple_memoryview_cdef(double[:] a, double[:] b):
+    return a[0] + b[0]
+
+multiple_memoryview_cdef_wrapper = multiple_memoryview_cdef
+
+def test_conversion_failures():
+    """
+    What we're concerned with here is that we don't lose references if one
+    of several memoryview arguments fails to convert.
+
+    >>> test_conversion_failures()
+    """
+    imb = IntMockBuffer("", range(1), shape=(1,))
+    dmb = DoubleMockBuffer("", range(1), shape=(1,))
+    for first, second in [(imb, dmb), (dmb, imb)]:
+        for func in [multiple_memoryview_def, multiple_memoryview_cpdef, multiple_memoryview_cdef_wrapper]:
+            # note - using python call of "multiple_memoryview_cpdef" deliberately
+            imb_before = get_refcount(imb)
+            dmb_before = get_refcount(dmb)
+            try:
+                func(first, second)
+            except:
+                assert get_refcount(imb) == imb_before, "before %s after %s" % (imb_before, get_refcount(imb))
+                assert get_refcount(dmb) == dmb_before, "before %s after %s" % (dmb_before, get_refcount(dmb))
+            else:
+                assert False, "Conversion should fail!"