Repository: numba/numba

Issue:
array.ctypes issue with optimization passes
Any function that uses `array.ctypes` fails (seg fault or corrupt result) it it goes through both copy propagation and remove dead code passes (minimum passes required to reproduce this). Example below can reproduce this easily.

I don't know why this is happening since the IR is equivalent either way (only the original variable is gone and replaced with another variable). @stuartarchibald @sklam @DrTodd13 any ideas?

```python
import numba
import numpy as np
numba.parfor.sequential_parfor_lowering = True
from numba import types
from numba.targets.linalg import _BLAS

kind = ord('d')
xxnrm2 = _BLAS().numba_xxnrm2(types.float64)

@numba.njit(parallel=True)
def f():
    a = np.ones(4)
    n = len(a)
    ret = np.empty((1,))
    jmp = int(a.strides[0] / a.itemsize)
    r = xxnrm2(kind, n, a.ctypes, jmp, ret.ctypes)
    return ret[0]

print(np.linalg.norm(np.ones(4)), f())
```


Discussion:
I made this a tiny bit smaller (as anything to shrink IR size is good!).
```python
import numba
import numpy as np
numba.parfor.sequential_parfor_lowering = True
from numba import types
from numba.targets.linalg import _BLAS

xxnrm2 = _BLAS().numba_xxnrm2(types.float64)

@numba.njit(parallel=True)
def f():
    ret = np.zeros(1)
    a = np.ones(4)
    xxnrm2(100, 4, a.ctypes, 1, ret.ctypes)
    return ret[0]

print(np.linalg.norm(np.ones(4)), f())
```
and to help debug, set a gdb command file like:
```
set pagination off
set breakpoint pending on
file python
break numba_xxnrm2

run issue2887.py
p n
p ((double*)x)[0]@4
p ((double*)result)[0]@1
```
put the sample into a file called `issue2887.py` and then run:
```bash
$ gdb -x cmdfile
```
output is:
```
Breakpoint 1, numba_xxnrm2 (kind=100 'd', n=4, x=0x5555563aa2e0, incx=1, result=0x5555563c9820) at numba/_lapack.c:408
408     {
$1 = 4
$2 = {-9.8683099208633679e+148, -9.8683099208633679e+148, -9.8683099208633679e+148, -9.8683099208633679e+148}
$3 = {0}
```
which suggests that it is just the `.ctypes` from variable exiting the parfors loop nest that has issues.

Resolving patch:
diff --git a/numba/datamodel/models.py b/numba/datamodel/models.py
--- a/numba/datamodel/models.py
+++ b/numba/datamodel/models.py
@@ -1151,7 +1151,8 @@ def from_data(self, builder, value):
 class ArrayCTypesModel(StructModel):
     def __init__(self, dmm, fe_type):
         # ndim = fe_type.ndim
-        members = [('data', types.CPointer(fe_type.dtype))]
+        members = [('data', types.CPointer(fe_type.dtype)),
+                   ('meminfo', types.MemInfoPointer(fe_type.dtype))]
         super(ArrayCTypesModel, self).__init__(dmm, fe_type, members)
 
 
diff --git a/numba/targets/arrayobj.py b/numba/targets/arrayobj.py
--- a/numba/targets/arrayobj.py
+++ b/numba/targets/arrayobj.py
@@ -1924,10 +1924,12 @@ def array_ctypes(context, builder, typ, value):
     arrayty = make_array(typ)
     array = arrayty(context, builder, value)
     # Create new ArrayCType structure
-    ctinfo = context.make_helper(builder, types.ArrayCTypes(typ))
+    act = types.ArrayCTypes(typ)
+    ctinfo = context.make_helper(builder, act)
     ctinfo.data = array.data
+    ctinfo.meminfo = array.meminfo
     res = ctinfo._getvalue()
-    return impl_ret_untracked(context, builder, typ, res)
+    return impl_ret_borrowed(context, builder, act, res)
 
 @lower_getattr(types.ArrayCTypes, "data")
 def array_ctypes_data(context, builder, typ, value):


Test patch:
diff --git a/numba/tests/test_array_attr.py b/numba/tests/test_array_attr.py
--- a/numba/tests/test_array_attr.py
+++ b/numba/tests/test_array_attr.py
@@ -228,13 +228,50 @@ def test_size_after_slicing(self):
             self.assertEqual(pyfunc(arr, i), cfunc(arr, i))
 
 
-class TestArrayCTypes(MemoryLeakMixin, unittest.TestCase):
+class TestArrayCTypes(MemoryLeakMixin, TestCase):
     def test_array_ctypes_data(self):
         pyfunc = array_ctypes_data
         cfunc = njit(pyfunc)
         arr = np.arange(3)
         self.assertEqual(pyfunc(arr), cfunc(arr))
 
+    def test_array_ctypes_ref_error_in_parallel(self):
+        # Issue #2887
+        from ctypes import CFUNCTYPE, c_void_p, c_int32, c_double, c_bool
+
+        @CFUNCTYPE(c_bool, c_void_p, c_int32, c_void_p)
+        def callback(inptr, size, outptr):
+            # A ctypes callback that manipulate the incoming pointers.
+            try:
+                inbuf = (c_double * size).from_address(inptr)
+                outbuf = (c_double * 1).from_address(outptr)
+                a = np.ndarray(size, buffer=inbuf, dtype=np.float64)
+                b = np.ndarray(1, buffer=outbuf, dtype=np.float64)
+                b[0] = (a + a.size)[0]
+                return True
+            except:
+                import traceback
+                traceback.print_exception()
+                return False
+
+
+        # parallel=True is required to reproduce the error.
+        @njit(parallel=True)
+        def foo(size):
+            arr = np.ones(size)
+            out = np.empty(1)
+            # Exercise array.ctypes
+            inct = arr.ctypes
+            outct = out.ctypes
+            # The reference to `arr` is dead by now
+            status = callback(inct.data, size, outct.data)
+            return status, out[0]
+
+        size = 3
+        status, got = foo(size)
+        self.assertTrue(status)
+        self.assertPreciseEqual(got, (np.ones(size) + size)[0])
+
 
 class TestRealImagAttr(MemoryLeakMixin, TestCase):
     def check_complex(self, pyfunc):