Repository: cython/cython

Issue:
[BUG] Crash due to duplicate __PYX_XCLEAR_MEMVIEW
### Describe the bug

When using a property setter that accepts a memory view, Python crashes with the following error:

```
Fatal Python error: __pyx_fatalerror: Acquisition count is -1 (line 16959)
Python runtime state: initialized

Current thread 0x00000001e83a1e00 (most recent call first):
  File "/private/tmp/cdtmp.l8Y/test.py", line 7 in <module>

Extension modules: cython.cimports.libc.math, foo (total: 2)
```

### Code to reproduce the behaviour:

```cython
# foo.pyx
cdef class Foo:
    @property
    def bar(self):
        return b'bar'

    @bar.setter
    def bar(self, const unsigned char[::1] bar):
        pass
```

```python
# test.py
#!/usr/bin/env python3

from pyximport import install; install()
from foo import Foo

c = Foo()
c.bar = b'some string'  # <-- this crashes

```

In the generated `foo.c`, I can see the following code where `__PYX_XCLEAR_MEMVIEW` is called twice on the same variable which is likely the cause of the bug:

```c
/* Python wrapper */
static int __pyx_pw_3foo_3Foo_3bar_3__set__(PyObject *__pyx_v_self, PyObject *__pyx_arg_bar); /*proto*/
static int __pyx_pw_3foo_3Foo_3bar_3__set__(PyObject *__pyx_v_self, PyObject *__pyx_arg_bar) {
  /* ... */

  /* function exit code */
  __PYX_XCLEAR_MEMVIEW(&__pyx_v_bar, 1);
  __PYX_XCLEAR_MEMVIEW(&__pyx_v_bar, 1);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}
```

### Expected behaviour

The code is executed without crashing the Python interpreter.

### OS

macOS

### Python version

Python 3.11.4

### Cython version

Cython version 3.0.0

### Additional context

I can also reproduce the error on Debian 11 with Cython 3.0.0 and Python 3.9.2


Discussion:
So the double `__PYX_XCLEAR_MEMVIEW` isn't right, but it should be safe. The crash is actually on the first one

Resolving patch:
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -2170,6 +2170,9 @@ def generate_function_definitions(self, env, code):
                                     have_gil=code.funcstate.gil_owned)
         for entry in lenv.var_entries:
             if entry.is_arg and entry.cf_is_reassigned and not entry.in_closure:
+                if entry.type.is_memoryviewslice:
+                    code.put_var_incref_memoryviewslice(entry,
+                                        have_gil=code.funcstate.gil_owned)
                 if entry.xdecref_cleanup:
                     code.put_var_xincref(entry)
                 else:
@@ -2357,10 +2360,9 @@ def align_error_path_gil_to_success_path(): pass
             if not entry.used or entry.in_closure:
                 continue
 
-            if entry.type.is_pyobject:
+            if entry.type.needs_refcounting:
                 if entry.is_arg and not entry.cf_is_reassigned:
                     continue
-            if entry.type.needs_refcounting:
                 assure_gil('success')
             # FIXME ideally use entry.xdecref_cleanup but this currently isn't reliable
             code.put_var_xdecref(entry, have_gil=gil_owned['success'])
@@ -3731,13 +3733,13 @@ def generate_function_definitions(self, env, code):
         code.put_label(code.return_label)
         for entry in lenv.var_entries:
             if entry.is_arg:
-                # mainly captures the star/starstar args
                 if entry.xdecref_cleanup:
                     code.put_var_xdecref(entry)
                 else:
                     code.put_var_decref(entry)
+        var_entries_set = set(lenv.var_entries)
         for arg in self.args:
-            if not arg.type.is_pyobject:
+            if not arg.type.is_pyobject and arg.entry not in var_entries_set:
                 # This captures anything that's been converted from a PyObject.
                 # Primarily memoryviews at the moment
                 if arg.entry.xdecref_cleanup:


Test patch:
diff --git a/tests/memoryview/memoryview.pyx b/tests/memoryview/memoryview.pyx
--- a/tests/memoryview/memoryview.pyx
+++ b/tests/memoryview/memoryview.pyx
@@ -978,6 +978,46 @@ def test_acquire_memoryview_slice():
     print b[2, 4]
     print c[2, 4]
 
+cdef class TestPassMemoryviewToSetter:
+    """
+    Setter has a fixed function signature and the
+    argument needs conversion so it ends up passing through
+    some slightly different reference counting code
+
+    >>> dmb = DoubleMockBuffer("dmb", range(2), shape=(2,))
+    >>> TestPassMemoryviewToSetter().prop = dmb
+    acquired dmb
+    In prop setter
+    released dmb
+    >>> TestPassMemoryviewToSetter().prop_with_reassignment = dmb
+    acquired dmb
+    In prop_with_reassignment setter
+    released dmb
+    >>> dmb = DoubleMockBuffer("dmb", range(1,3), shape=(2,))
+    >>> TestPassMemoryviewToSetter().prop_with_reassignment = dmb
+    acquired dmb
+    In prop_with_reassignment setter
+    released dmb
+    """
+    @property
+    def prop(self):
+        return None
+
+    @prop.setter
+    def prop(self, double[:] x):
+        print("In prop setter")
+
+    @property
+    def prop_with_reassignment(self):
+        return None
+
+    @prop_with_reassignment.setter
+    def prop_with_reassignment(self, double[:] x):
+        # reassignment again requires slightly different code
+        if x[0]:
+            x = x[1:]
+        print("In prop_with_reassignment setter")
+
 class SingleObject(object):
     def __init__(self, value):
         self.value = value