Repository: numba/numba

Issue:
Arrays in tuples referred to in globals are erroneously writable.
here is a not-so-minimal reproducer (tested with Python 3.8):

```python
import numpy as np, numba
grid = (100, 10)
def fa(data):
    return (np.ones(5, dtype=int), np.zeros([data.shape[i] + 2 * 1 for i in range(2)]))
def fb(data):
    halos = [[(1 - (d == c)) for c in range(2)] for d in range(2)]
    dd = [np.zeros([[data[d].shape[c] + 2 * halos[d][c] for c in range(2)] for d in range(2)][d]) for d in range(2)]
    return (np.ones(5, dtype=int), dd[0], dd[1])
@numba.njit()
def atv_2d_axis0(focus, arrs, i, k=0):
    return arrs[0][focus[0] + int(i - .5), focus[-1] + int(k)]
@numba.njit()
def atv_2d_axis1(focus, arrs, k, i=0):
    return arrs[-1][focus[0] + int(i), focus[-1] + int(k - .5)]
@numba.njit()
def set(arr, i, k, value):
    arr[i, k] = value
@numba.njit()
def apply_scalar_impl(fun_outer, fun_inner, out, vec_arg1_outer, vec_arg1_inner):
    for i in range(1, grid[0]+1):
        for k in range(1, grid[1]+1):
            set(out, i, k, fun_inner(out[i, k], ((i, k), (vec_arg1_outer, vec_arg1_inner))))
            set(out, i, k, fun_outer(out[i, k], ((i, k), (vec_arg1_outer, vec_arg1_inner))))
@numba.njit()
def apply_scalar(fun_outer, fun_inner, out, vec_arg1_meta, vec_arg1_outer, vec_arg1_inner, scal_arg2_meta):
    scal_arg2_meta[0] = True
    apply_scalar_impl(fun_outer, fun_inner, out, vec_arg1_outer, vec_arg1_inner)
def mu(atv):
    @numba.njit()
    def upwind(init, flux):
        return init + atv(*flux, -.5) - atv(*flux, .5)
    return upwind
form = tuple([mu(atv) for atv in (atv_2d_axis0, atv_2d_axis1)])
ns = fa(np.empty([0]*2))
@numba.njit()
def step(a, b):
    return apply_scalar(*form, a[1], *b, ns[0])
step(fa(data=np.zeros(grid)), fb(data=(np.zeros((grid[0] + 1, grid[1])), np.zeros((grid[0], grid[1] + 1)))))
```

gives:

```
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
```

works OK with `NUMBA_DISABLE_JIT` set to "1"


Discussion:
I cannot replicate this using mainline or 0.53.1. Can you try it on the latest mainline?
I can reproduce on mainline:
```
$ git rev-parse HEAD
8dd28abf57bd554fe25921ad2abee5d2faa81816

$ gdb --args python issue7120.py 
<snip>
(gdb) r
<snip>
Program received signal SIGSEGV, Segmentation fault.
0x00007fffcbe75076 in __main__::apply_scalar$242(type$28CPUDispatcher$28$3cfunction$20mu::$3clocals$3e::upwind$20at$200x7fffcf849af0$3e$29$29, type$28CPUDispatcher$28$3cfunction$20mu::$3clocals$3e::upwind$20at$200x7fffcf849ca0$3e$29$29, Array<double, 2, C, mutable, aligned>, Array<long long, 1, C, mutable, aligned>, Array<double, 2, C, mutable, aligned>, Array<double, 2, C, mutable, aligned>, Array<long long, 1, C, mutable, aligned>) () at issue7120.py:36
36          scal_arg2_meta[0] = True
(gdb) bt
#0  0x00007fffcbe75076 in __main__::apply_scalar$242(type$28CPUDispatcher$28$3cfunction$20mu::$3clocals$3e::upwind$20at$200x7fffcf849af0$3e$29$29, type$28CPUDispatcher$28$3cfunction$20mu::$3clocals$3e::upwind$20at$200x7fffcf849ca0$3e$29$29, Array<double, 2, C, mutable, aligned>, Array<long long, 1, C, mutable, aligned>, Array<double, 2, C, mutable, aligned>, Array<double, 2, C, mutable, aligned>, Array<long long, 1, C, mutable, aligned>) () at issue7120.py:36
#1  0x00007fffcbdf1178 in __main__::step$241(Tuple<Array<long long, 1, C, mutable, aligned>, Array<double, 2, C, mutable, aligned> >, Tuple<Array<long long, 1, C, mutable, aligned>, Array<double, 2, C, mutable, aligned>, Array<double, 2, C, mutable, aligned> >) () at issue7120.py:50
#2  0x00007fffcbdf18e7 in cpython::__main__::step$241(Tuple<Array<long long, 1, C, mutable, aligned>, Array<double, 2, C, mutable, aligned> >, Tuple<Array<long long, 1, C, mutable, aligned>, Array<double, 2, C, mutable, aligned>, Array<double, 2, C, mutable, aligned> >) ()
#3  0x00007fffcfcbba54 in call_cfunc(Dispatcher*, _object*, _object*, _object*, _object*) ()
```
I think I might have aliased a segfault by switching on debug, this also segfaults but I suspect for a different reason:
```python
import numpy as np, numba

jit = numba.njit(debug=True)

@jit
def bar(x):
    x[0] = True

g = (np.ones(1),)

@jit
def foo():
    return bar(g[0])

foo()
```
I'm suspicious this is coming from trying to write to an array in globals.
```python
import numpy as np, numba

@numba.njit()
def bar(x):
    x[:] = 72

@numba.njit
def foo():
    bar(g[0])

g = (np.ones(10, np.int64),)

foo()
```
reproduces.
I think this is a bug in the typing of tuples in globals, the arrays inside the tuple should be marked read only, whereas in the above it appears as:
```
 $4load_global.1 := UniTuple(array(int64, 1d, C) x 1),
```
this permits mutation, but I don't think it's valid.
thank you!
ping :)
@slayoo this is marked as a bug and needs a fix. The fix will be to catch the issue in code such as that in your reproducer or in the MWR [here](https://github.com/numba/numba/issues/7120#issuecomment-863962276) and abort compilation, i.e. with a fix in place, your code would not compile as it violates this assumption made by Numba: https://numba.readthedocs.io/en/stable/reference/pysemantics.html#global-and-closure-variables.

To make your code work, you'd need to pass the array in as an argument such that writing to it is a valid operation.

Hope this clarification helps?
thank you! this clarification was super super helpful. Workaround in place!
@slayoo No problem. I'm also going to edit the title of this issue to reflect the actual problem.

Resolving patch:
diff --git a/numba/core/typeinfer.py b/numba/core/typeinfer.py
--- a/numba/core/typeinfer.py
+++ b/numba/core/typeinfer.py
@@ -1612,6 +1612,17 @@ def typeof_global(self, inst, target, gvar):
             if all(literaled):
                 typ = types.Tuple(literaled)
 
+            # if any of the items in the tuple are arrays, they need to be
+            # typed as readonly, mutating an array in a global container
+            # is not supported (should be compile time constant etc).
+            def mark_array_ro(tup):
+                for item in tup.types:
+                    if isinstance(item, types.Array):
+                        item.mutable = False
+                    elif isinstance(item, types.BaseAnonymousTuple):
+                        mark_array_ro(item)
+            mark_array_ro(typ)
+
         self.sentry_modified_builtin(inst, gvar)
         # Setting literal_value for globals because they are handled
         # like const value in numba
diff --git a/numba/core/typing/arraydecl.py b/numba/core/typing/arraydecl.py
--- a/numba/core/typing/arraydecl.py
+++ b/numba/core/typing/arraydecl.py
@@ -178,7 +178,8 @@ def generic(self, args, kws):
         if not isinstance(ary, types.Buffer):
             return
         if not ary.mutable:
-            raise NumbaTypeError(f"Cannot modify value of type {ary}")
+            msg = f"Cannot modify readonly array of type: {ary}"
+            raise NumbaTypeError(msg)
         out = get_array_index_type(ary, idx)
         if out is None:
             return


Test patch:
diff --git a/numba/tests/test_globals.py b/numba/tests/test_globals.py
--- a/numba/tests/test_globals.py
+++ b/numba/tests/test_globals.py
@@ -1,5 +1,5 @@
 import numpy as np
-from numba import jit, njit
+from numba import jit, njit, errors
 from numba.extending import register_jitable
 from numba.tests import usecases
 import unittest
@@ -78,6 +78,8 @@ def global_module_func(x, y):
 tup_mixed = (1, 'a')
 tup_float = (1.2, 3.5)
 tup_npy_ints = (np.uint64(12), np.int8(3))
+tup_tup_array = ((np.ones(5),),)
+mixed_tup_tup_array = (('Z', np.ones(5),), 2j, 'A')
 
 def global_int_tuple():
     return tup_int[0] + tup_int[1]
@@ -101,6 +103,14 @@ def global_npy_int_tuple():
     return tup_npy_ints[0] + tup_npy_ints[1]
 
 
+def global_write_to_arr_in_tuple():
+    tup_tup_array[0][0][0] = 10.
+
+
+def global_write_to_arr_in_mixed_tuple():
+    mixed_tup_tup_array[0][1][0] = 10.
+
+
 _glbl_np_bool_T = np.bool_(True)
 _glbl_np_bool_F = np.bool_(False)
 
@@ -233,6 +243,17 @@ def test_global_npy_int_tuple(self):
         jitfunc = njit(pyfunc)
         self.assertEqual(pyfunc(), jitfunc())
 
+    def test_global_write_to_arr_in_tuple(self):
+        # Test writing to an array in a global tuple
+        # See issue https://github.com/numba/numba/issues/7120
+        for func in (global_write_to_arr_in_tuple,
+                     global_write_to_arr_in_mixed_tuple):
+            jitfunc = njit(func)
+            with self.assertRaises(errors.TypingError) as e:
+                jitfunc()
+            msg = "Cannot modify readonly array of type:"
+            self.assertIn(msg, str(e.exception))
+
     def test_global_npy_bool(self):
         # Test global NumPy bool
         # See issue https://github.com/numba/numba/issues/6979
diff --git a/numba/tests/test_indexing.py b/numba/tests/test_indexing.py
--- a/numba/tests/test_indexing.py
+++ b/numba/tests/test_indexing.py
@@ -1053,7 +1053,7 @@ def test_setitem_readonly(self):
         arr.flags.writeable = False
         with self.assertRaises((TypeError, errors.TypingError)) as raises:
             setitem_usecase(arr, 1, 42)
-        self.assertIn("Cannot modify value of type readonly array",
+        self.assertIn("Cannot modify readonly array of type:",
                       str(raises.exception))