Repository: numba/numba

Issue:
lstsq sometimes overwrites its inputs
Hi,

we've noticed that `np.linalg.lstsq` overwrites its input matrix in some edge cases. I'd expect the input matrix to never change.

To reproduce:

```python
import numpy as np
import numba as nb

X = np.array([[1., 7.54, 6.52],
              [1., 2.70, 4.00],
              [1., 2.50, 3.80],
              [1., 1.15, 5.64],
              [1., 4.22, 3.27],
              [1., 1.41, 5.70],
            ], order='F')

X_orig = X.copy()

y = np.array([1., 2., 3., 4., 5., 6.])

@nb.jit(nopython=True, nogil=True)
def f1(X, y):
    return np.linalg.lstsq(X, y)

f1(X, y)
(X == X_orig).all()
# True, all fine until here

@nb.jit(nopython=True, nogil=True)
def f2(X, y, test):
    if test:
        # never executed, but necessary to trigger the bug
        X = X[1:2, :]
    return np.linalg.lstsq(X, y)

f2(X, y, False)
(X == X_orig).all()
# False, X got corrupted
```

From the values in X it looks like it's likely being used as the work matrix of `lstsq`:
```python
array([[-2.44948974, 14.24766238,  0.53161088],
       [ 0.        ,  4.23875445,  2.46241227],
       [ 0.        ,  0.30113438,  3.52520967],
       [ 0.28989795,  0.47206594, -0.33747568],
       [ 0.28989795,  0.03894287,  0.52109546],
       [ 0.28989795,  0.43538451, -0.33387113]])
```

To trigger the issue it seems to be necessary for `X` to be in fortran order, and then be modified inside the jitted function.

I'm not entirely sure what causes this exactly, but looking at https://github.com/numba/numba/blob/master/numba/targets/linalg.py#L1619 it seems like it might be confused what the order of the X matrix is? And then skips doing the copy?

Tested with numba 0.39 as that's the latest available in anaconda, apologies if this is already fixed in 0.40.
lstsq sometimes overwrites its inputs
Hi,

we've noticed that `np.linalg.lstsq` overwrites its input matrix in some edge cases. I'd expect the input matrix to never change.

To reproduce:

```python
import numpy as np
import numba as nb

X = np.array([[1., 7.54, 6.52],
              [1., 2.70, 4.00],
              [1., 2.50, 3.80],
              [1., 1.15, 5.64],
              [1., 4.22, 3.27],
              [1., 1.41, 5.70],
            ], order='F')

X_orig = X.copy()

y = np.array([1., 2., 3., 4., 5., 6.])

@nb.jit(nopython=True, nogil=True)
def f1(X, y):
    return np.linalg.lstsq(X, y)

f1(X, y)
(X == X_orig).all()
# True, all fine until here

@nb.jit(nopython=True, nogil=True)
def f2(X, y, test):
    if test:
        # never executed, but necessary to trigger the bug
        X = X[1:2, :]
    return np.linalg.lstsq(X, y)

f2(X, y, False)
(X == X_orig).all()
# False, X got corrupted
```

From the values in X it looks like it's likely being used as the work matrix of `lstsq`:
```python
array([[-2.44948974, 14.24766238,  0.53161088],
       [ 0.        ,  4.23875445,  2.46241227],
       [ 0.        ,  0.30113438,  3.52520967],
       [ 0.28989795,  0.47206594, -0.33747568],
       [ 0.28989795,  0.03894287,  0.52109546],
       [ 0.28989795,  0.43538451, -0.33387113]])
```

To trigger the issue it seems to be necessary for `X` to be in fortran order, and then be modified inside the jitted function.

I'm not entirely sure what causes this exactly, but looking at https://github.com/numba/numba/blob/master/numba/targets/linalg.py#L1619 it seems like it might be confused what the order of the X matrix is? And then skips doing the copy?

Tested with numba 0.39 as that's the latest available in anaconda, apologies if this is already fixed in 0.40.


Discussion:
Thanks for the report, I can confirm that this is indeed a bug (am on mainline builds).

I'm pretty sure that the `X = X[1:2, :]` on the dead branch causes the typing system to consider `X` as an `A` ordered array even though it is `F` ordered. Once that happens `np.asfortranarray` is called here, https://github.com/numba/numba/blob/master/numba/targets/linalg.py#L1623, which compiles down to a runtime check of the contiguousness of the array, which turns out to be `F` ordered already so it isn't copied, net result is `dgelsd` is called on the input array, hence mutation. I'll try and fix this for the 0.41 release and ping here when there's some dev builds with a fix in to check against.

In the mean time:
```python
@nb.jit(nopython=True, nogil=True)
def f2(X, y, test):
    if test:
        ## never executed, but necessary to trigger the bug
        X = X[1:2, :]
    return np.linalg.lstsq(np.asfortranarray(X), y)
```
should work around the problem as it'll make the typing system see `X` as `F` ordered and the linalg impl will pick this up and behave as expected.
Thanks for the super fast response! Happy to help test.
Thanks for the report, I can confirm that this is indeed a bug (am on mainline builds).

I'm pretty sure that the `X = X[1:2, :]` on the dead branch causes the typing system to consider `X` as an `A` ordered array even though it is `F` ordered. Once that happens `np.asfortranarray` is called here, https://github.com/numba/numba/blob/master/numba/targets/linalg.py#L1623, which compiles down to a runtime check of the contiguousness of the array, which turns out to be `F` ordered already so it isn't copied, net result is `dgelsd` is called on the input array, hence mutation. I'll try and fix this for the 0.41 release and ping here when there's some dev builds with a fix in to check against.

In the mean time:
```python
@nb.jit(nopython=True, nogil=True)
def f2(X, y, test):
    if test:
        ## never executed, but necessary to trigger the bug
        X = X[1:2, :]
    return np.linalg.lstsq(np.asfortranarray(X), y)
```
should work around the problem as it'll make the typing system see `X` as `F` ordered and the linalg impl will pick this up and behave as expected.
Thanks for the super fast response! Happy to help test.

Resolving patch:
diff --git a/numba/np/linalg.py b/numba/np/linalg.py
--- a/numba/np/linalg.py
+++ b/numba/np/linalg.py
@@ -782,6 +782,38 @@ def _check_homogeneous_types(func_name, *types):
             raise TypingError(msg, highlighting=False)
 
 
+def _copy_to_fortran_order():
+    pass
+
+
+@overload(_copy_to_fortran_order)
+def ol_copy_to_fortran_order(a):
+    # This function copies the array 'a' into a new array with fortran order.
+    # This exists because the copy routines don't take order flags yet.
+    F_layout = a.layout == 'F'
+    A_layout = a.layout == 'A'
+    def impl(a):
+        if F_layout:
+            # it's F ordered at compile time, just copy
+            acpy = np.copy(a)
+        elif A_layout:
+            # decide based on runtime value
+            flag_f = a.flags.f_contiguous
+            if flag_f:
+                # it's already F ordered, so copy but in a round about way to
+                # ensure that the copy is also F ordered
+                acpy = np.copy(a.T).T
+            else:
+                # it's something else ordered, so let asfortranarray deal with
+                # copying and making it fortran ordered
+                acpy = np.asfortranarray(a)
+        else:
+            # it's C ordered at compile time, asfortranarray it.
+            acpy = np.asfortranarray(a)
+        return acpy
+    return impl
+
+
 @register_jitable
 def _inv_err_handler(r):
     if r != 0:
@@ -810,8 +842,6 @@ def inv_impl(a):
 
     kind = ord(get_blas_kind(a.dtype, "inv"))
 
-    F_layout = a.layout == 'F'
-
     def inv_impl(a):
         n = a.shape[-1]
         if a.shape[-2] != n:
@@ -820,10 +850,7 @@ def inv_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         if n == 0:
             return acpy
@@ -929,8 +956,6 @@ def eig_impl(a):
     JOBVL = ord('N')
     JOBVR = ord('V')
 
-    F_layout = a.layout == 'F'
-
     def real_eig_impl(a):
         """
         eig() implementation for real arrays.
@@ -942,10 +967,7 @@ def real_eig_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldvl = 1
         ldvr = n
@@ -1001,10 +1023,7 @@ def cmplx_eig_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldvl = 1
         ldvr = n
@@ -1052,8 +1071,6 @@ def eigvals_impl(a):
     JOBVL = ord('N')
     JOBVR = ord('N')
 
-    F_layout = a.layout == 'F'
-
     def real_eigvals_impl(a):
         """
         eigvals() implementation for real arrays.
@@ -1065,10 +1082,7 @@ def real_eigvals_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldvl = 1
         ldvr = 1
@@ -1127,10 +1141,7 @@ def cmplx_eigvals_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldvl = 1
         ldvr = 1
@@ -1171,8 +1182,6 @@ def eigh_impl(a):
 
     _check_linalg_matrix(a, "eigh")
 
-    F_layout = a.layout == 'F'
-
     # convert typing floats to numpy floats for use in the impl
     w_type = getattr(a.dtype, "underlying_float", a.dtype)
     w_dtype = np_support.as_dtype(w_type)
@@ -1193,10 +1202,7 @@ def eigh_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         w = np.empty(n, dtype=w_dtype)
 
@@ -1225,8 +1231,6 @@ def eigvalsh_impl(a):
 
     _check_linalg_matrix(a, "eigvalsh")
 
-    F_layout = a.layout == 'F'
-
     # convert typing floats to numpy floats for use in the impl
     w_type = getattr(a.dtype, "underlying_float", a.dtype)
     w_dtype = np_support.as_dtype(w_type)
@@ -1247,10 +1251,7 @@ def eigvalsh_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         w = np.empty(n, dtype=w_dtype)
 
@@ -1279,8 +1280,6 @@ def svd_impl(a, full_matrices=1):
 
     _check_linalg_matrix(a, "svd")
 
-    F_layout = a.layout == 'F'
-
     # convert typing floats to numpy floats for use in the impl
     s_type = getattr(a.dtype, "underlying_float", a.dtype)
     s_dtype = np_support.as_dtype(s_type)
@@ -1301,10 +1300,7 @@ def svd_impl(a, full_matrices=1):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldu = m
         minmn = min(m, n)
@@ -1361,8 +1357,6 @@ def qr_impl(a):
 
     kind = ord(get_blas_kind(a.dtype, "qr"))
 
-    F_layout = a.layout == 'F'
-
     def qr_impl(a):
         n = a.shape[-1]
         m = a.shape[-2]
@@ -1373,10 +1367,7 @@ def qr_impl(a):
         _check_finite_matrix(a)
 
         # copy A as it will be destroyed
-        if F_layout:
-            q = np.copy(a)
-        else:
-            q = np.asfortranarray(a)
+        q = _copy_to_fortran_order(a)
 
         lda = m
 
@@ -1598,9 +1589,6 @@ def lstsq_impl(a, b, rcond=-1.0):
     # B can be 1D or 2D.
     _check_linalg_1_or_2d_matrix(b, "lstsq")
 
-    a_F_layout = a.layout == 'F'
-    b_F_layout = b.layout == 'F'
-
     _check_homogeneous_types("lstsq", a, b)
 
     np_dt = np_support.as_dtype(a.dtype)
@@ -1640,10 +1628,7 @@ def lstsq_impl(a, b, rcond=-1.0):
         maxmn = max(m, n)
 
         # a is destroyed on exit, copy it
-        if a_F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         # b is overwritten on exit with the solution, copy allocate
         bcpy = np.empty((nrhs, maxmn), dtype=np_dt).T
@@ -1717,9 +1702,6 @@ def solve_impl(a, b):
     _check_linalg_matrix(a, "solve")
     _check_linalg_1_or_2d_matrix(b, "solve")
 
-    a_F_layout = a.layout == 'F'
-    b_F_layout = b.layout == 'F'
-
     _check_homogeneous_types("solve", a, b)
 
     np_dt = np_support.as_dtype(a.dtype)
@@ -1742,10 +1724,7 @@ def solve_impl(a, b):
         _system_check_dimensionally_valid(a, b)
 
         # a is destroyed on exit, copy it
-        if a_F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         # b is overwritten on exit with the solution, copy allocate
         bcpy = np.empty((nrhs, n), dtype=np_dt).T
@@ -1791,8 +1770,6 @@ def pinv_impl(a, rcond=1.e-15):
 
     numba_xxgemm = _BLAS().numba_xxgemm(a.dtype)
 
-    F_layout = a.layout == 'F'
-
     kind = ord(get_blas_kind(a.dtype, "pinv"))
     JOB = ord('S')
 
@@ -1849,10 +1826,7 @@ def pinv_impl(a, rcond=1.e-15):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         if m == 0 or n == 0:
             return acpy.T.ravel().reshape(a.shape).T
@@ -1992,8 +1966,6 @@ def slogdet_impl(a):
 
     kind = ord(get_blas_kind(a.dtype, "slogdet"))
 
-    F_layout = a.layout == 'F'
-
     diag_walker = _get_slogdet_diag_walker(a)
 
     ONE = a.dtype(1)
@@ -2010,10 +1982,7 @@ def slogdet_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ipiv = np.empty(n, dtype=F_INT_nptype)
 
@@ -2088,8 +2057,6 @@ def _compute_singular_values_impl(a):
     u = np.empty((1, 1), dtype=np_dtype)
     vt = np.empty((1, 1), dtype=np_dtype)
 
-    F_layout = a.layout == 'F'
-
     def sv_function(a):
         """
         Computes singular values.
@@ -2111,10 +2078,7 @@ def sv_function(a):
         ucol = 1
         ldvt = 1
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         # u and vt are not referenced however need to be
         # allocated (as done above) for MKL as it


Test patch:
diff --git a/numba/tests/test_linalg.py b/numba/tests/test_linalg.py
--- a/numba/tests/test_linalg.py
+++ b/numba/tests/test_linalg.py
@@ -8,7 +8,7 @@
 
 import numpy as np
 
-from numba import jit
+from numba import jit, njit, typeof
 from numba.core import errors
 from numba.tests.support import (TestCase, tag, needs_lapack, needs_blas,
                                  _is_armv7l, skip_ppc64le_issue4026)
@@ -772,6 +772,30 @@ def check(a, **kwargs):
         # Singular matrix
         self.assert_raise_on_singular(cfunc, (np.zeros((2, 2)),))
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def ainv(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.inv(X)
+
+        expected = ainv.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = ainv(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 
 @skip_ppc64le_issue4026
 class TestLinalgCholesky(TestLinalgBase):
@@ -877,17 +901,12 @@ def assert_no_domain_change(self, name, cfunc, args):
         msg = name + "() argument must not cause a domain change."
         self.assert_error(cfunc, args, msg)
 
-    def checker_for_linalg_eig(
-            self, name, func, expected_res_len, check_for_domain_change=None):
-        """
-        Test np.linalg.eig
-        """
-        n = 10
-        cfunc = jit(nopython=True)(func)
-
-        def check(a):
-            expected = func(a)
-            got = cfunc(a)
+    def _check_worker(self, cfunc, name, expected_res_len,
+                      check_for_domain_change):
+        def check(*args):
+            expected = cfunc.py_func(*args)
+            got = cfunc(*args)
+            a = args[0]
             # check that the returned tuple is same length
             self.assertEqual(len(expected), len(got))
             # and that dimension is correct
@@ -935,7 +954,7 @@ def check(a):
                     if name[-1] == 'h':
                         idxl = np.nonzero(np.eye(a.shape[0], a.shape[1], -1))
                         idxu = np.nonzero(np.eye(a.shape[0], a.shape[1], 1))
-                        cfunc(a)
+                        cfunc(*args)
                         # upper idx must match lower for default uplo="L"
                         # if complex, conjugate
                         a[idxu] = np.conj(a[idxl])
@@ -975,7 +994,19 @@ def check(a):
 
             # Ensure proper resource management
             with self.assertNoNRTLeak():
-                cfunc(a)
+                cfunc(*args)
+        return check
+
+    def checker_for_linalg_eig(
+            self, name, func, expected_res_len, check_for_domain_change=None):
+        """
+        Test np.linalg.eig
+        """
+        n = 10
+        cfunc = jit(nopython=True)(func)
+        check = self._check_worker(cfunc, name, expected_res_len,
+                                   check_for_domain_change)
+
 
         # The main test loop
         for dtype, order in product(self.dtypes, 'FC'):
@@ -1044,6 +1075,48 @@ def test_linalg_eigh(self):
     def test_linalg_eigvalsh(self):
         self.checker_for_linalg_eig("eigvalsh", eigvalsh_matrix, 1, False)
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        # checks inputs are not mutated
+
+        for c in (('eig', 2, True),
+                  ('eigvals', 1, True),
+                  ('eigh', 2, False),
+                  ('eigvalsh', 1, False)):
+
+            m, nout, domain_change = c
+
+            meth = getattr(np.linalg, m)
+
+            @jit(nopython=True)
+            def func(X, test):
+                if test:
+                    # not executed, but necessary to trigger A ordering in X
+                    X = X[1:2, :]
+                return meth(X)
+
+            check = self._check_worker(func, m, nout, domain_change)
+
+            for dtype in (np.float64, np.complex128):
+                with self.subTest(meth=meth, dtype=dtype):
+                    # trivial system, doesn't matter, just checking if it gets
+                    # mutated
+                    X = np.array([[10., 1, 0, 1],
+                                [1, 9, 0, 0],
+                                [0, 0, 8, 0],
+                                [1, 0, 0, 7],
+                                ], order='F', dtype=dtype)
+
+                    X_orig = np.copy(X)
+
+                    expected = func.py_func(X, False)
+                    np.testing.assert_allclose(X, X_orig)
+
+                    got = func(X, False)
+                    np.testing.assert_allclose(X, X_orig)
+
+                    check(X, False)
+
 
 class TestLinalgSvd(TestLinalgBase):
     """
@@ -1144,6 +1217,31 @@ def check(a, **kwargs):
             args = (np.empty(sz), True)
             self.assert_raise_on_empty(cfunc, args)
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def func(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.svd(X)
+
+        expected = func.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        for e_a, g_a in zip(expected, got):
+            np.testing.assert_allclose(e_a, g_a)
+
 
 class TestLinalgQr(TestLinalgBase):
     """
@@ -1239,6 +1337,31 @@ def check(a, **kwargs):
         for sz in [(0, 1), (1, 0), (0, 0)]:
             self.assert_raise_on_empty(cfunc, (np.empty(sz),))
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def func(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.qr(X)
+
+        expected = func.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        for e_a, g_a in zip(expected, got):
+            np.testing.assert_allclose(e_a, g_a)
+
 
 class TestLinalgSystems(TestLinalgBase):
     """
@@ -1454,8 +1577,8 @@ def inner_test_loop_fn(A, dt, **kwargs):
         empties = [
         [(0, 1), (1,)], # empty A, valid b
         [(1, 0), (1,)], # empty A, valid b
-        [(1, 1), (0,)], # valid A, empty 1D b 
-        [(1, 1), (1, 0)],  # valid A, empty 2D b 
+        [(1, 1), (0,)], # valid A, empty 1D b
+        [(1, 1), (1, 0)],  # valid A, empty 2D b
         ]
 
         for A, b in empties:
@@ -1504,6 +1627,28 @@ def inner_test_loop_fn(A, dt, **kwargs):
         self.assert_dimensionally_invalid(cfunc, (ok, bad1D))
         self.assert_dimensionally_invalid(cfunc, (ok, bad2D))
 
+    @needs_lapack
+    def test_issue3368(self):
+        X = np.array([[1., 7.54, 6.52],
+                      [1., 2.70, 4.00],
+                      [1., 2.50, 3.80],
+                      [1., 1.15, 5.64],
+                      [1., 4.22, 3.27],
+                      [1., 1.41, 5.70],], order='F')
+
+        X_orig = np.copy(X)
+        y = np.array([1., 2., 3., 4., 5., 6.])
+
+        @jit(nopython=True)
+        def f2(X, y, test):
+            if test:
+                # never executed, but necessary to trigger the bug
+                X = X[1:2, :]
+            return np.linalg.lstsq(X, y)
+
+        f2(X, y, False)
+        np.testing.assert_allclose(X, X_orig)
+
 
 class TestLinalgSolve(TestLinalgSystems):
     """
@@ -1627,6 +1772,34 @@ def check(a, b, **kwargs):
         bad2D = self.specific_sample_matrix((2, 2), np.float64, 'C', rank=1)
         self.assert_raise_on_singular(cfunc, (bad2D, ok))
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 1, 1, 1],
+                      [0., 1, 1, 1],
+                      [0., 0, 1, 1],
+                      [1., 0, 0, 1],], order='F')
+
+        X_orig = np.copy(X)
+        y = np.array([1., 2., 3., 4])
+        y_orig = np.copy(y)
+
+        @jit(nopython=True)
+        def func(X, y, test):
+            if test:
+                # not executed, triggers A order in X
+                X = X[1:2, :]
+            return np.linalg.solve(X, y)
+
+        expected = func.py_func(X, y, False)
+        np.testing.assert_allclose(X, X_orig)
+        np.testing.assert_allclose(y, y_orig)
+
+        got = func(X, y, False)
+        np.testing.assert_allclose(X, X_orig)
+        np.testing.assert_allclose(y, y_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 
 class TestLinalgPinv(TestLinalgBase):
     """
@@ -1749,6 +1922,29 @@ def check(a, **kwargs):
                                   (np.array([[1., 2., ], [np.inf, np.nan]],
                                             dtype=np.float64),))
 
+    @needs_lapack
+    def test_issue5870(self):
+        # testing for mutation of input matrix
+        @jit(nopython=True)
+        def some_fn(v):
+            return np.linalg.pinv(v[0])
+
+        v_data = np.array([[1., 3, 2, 7,],
+                           [-5, 4, 2, 3,],
+                           [9, -3, 1, 1,],
+                           [2, -2, 2, 8,]], order='F')
+
+        v_orig = np.copy(v_data)
+        reshaped_v = v_data.reshape((1, 4, 4))
+
+        expected = some_fn.py_func(reshaped_v)
+        np.testing.assert_allclose(v_data, v_orig)
+
+        got = some_fn(reshaped_v)
+        np.testing.assert_allclose(v_data, v_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 
 class TestLinalgDetAndSlogdet(TestLinalgBase):
     """
@@ -1849,6 +2045,30 @@ def test_linalg_slogdet(self):
         cfunc = jit(nopython=True)(slogdet_matrix)
         self.do_test("slogdet", self.check_slogdet, cfunc)
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def func(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.slogdet(X)
+
+        expected = func.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 # Use TestLinalgSystems as a base to get access to additional
 # testing for 1 and 2D inputs.
 
@@ -2148,6 +2368,34 @@ def check(a, **kwargs):
                                   (np.array([[1., 2., ], [np.inf, np.nan]],
                                             dtype=np.float64),))
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        # this is here to test no input mutation by
+        # numba.np.linalg._compute_singular_values
+        # which is the workhorse for norm with 2d input, rank and cond.
+
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def func(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.matrix_rank(X)
+
+        expected = func.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 
 class TestLinalgMatrixPower(TestLinalgBase):
     """
@@ -2398,5 +2646,46 @@ def check(a, b, **kwargs):
         self.assert_error(cfunc, args, msg, err=errors.TypingError)
 
 
+class TestHelpers(TestCase):
+    def test_copy_to_fortran_order(self):
+        from numba.np.linalg import _copy_to_fortran_order
+
+        def check(udt, expectfn, shapes, dtypes, orders):
+            for shape, dtype, order in product(shapes, dtypes, orders):
+                a = np.arange(np.prod(shape)).reshape(shape, order=order)
+
+                r = udt(a)
+                # check correct operation
+                self.assertPreciseEqual(expectfn(a), r)
+                # check new copy has made
+                self.assertNotEqual(a.ctypes.data, r.ctypes.data)
+
+        @njit
+        def direct_call(a):
+            return _copy_to_fortran_order(a)
+
+        shapes = [(3, 4), (3, 2, 5)]
+        dtypes = [np.intp]
+        orders = ['C', 'F']
+        check(direct_call, np.asfortranarray, shapes, dtypes, orders)
+
+
+        @njit
+        def slice_to_any(a):
+            # make a 'any' layout slice
+            sliced = a[::2][0]
+            return _copy_to_fortran_order(sliced)
+
+        shapes = [(3, 3, 4), (3, 3, 2, 5)]
+        dtypes = [np.intp]
+        orders = ['C', 'F']
+
+        def expected_slice_to_any(a):
+            # make a 'any' layout slice
+            sliced = a[::2][0]
+            return np.asfortranarray(sliced)
+
+        check(slice_to_any, expected_slice_to_any, shapes, dtypes, orders)
+
 if __name__ == '__main__':
     unittest.main()