From ca1f8423abf4d224de326d37bd9c4d16a2c18842 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 25 Aug 2026 15:27:03 +0200 Subject: [PATCH 1/5] Fix dpnp.insert ignoring out-of-bounds negative indices in multi-element obj Backport of numpy#31782. The multi-element index path in _insert_array_indices normalized negative indices without any bounds check, so an out-of-bounds negative index mixed with in-bounds ones silently produced wrong results instead of raising. Add the same bounds validation numpy uses so any out-of-bounds index raises IndexError, and extend the test to cover the mixed case. --- CHANGELOG.md | 1 + dpnp/dpnp_iface_manipulation.py | 9 +++++++++ dpnp/tests/test_manipulation.py | 13 ++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fd31e5a50..d109d977d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ This release is compatible with NumPy 2.5. * Fixed `dpnp.ndarray.view` ignoring the USM element offset of a sliced array, which also caused `dpnp.einsum` to silently return wrong results for a single sliced operand with no summed index [#3037](https://github.com/IntelPython/dpnp/pull/3037) * Fixed `dpnp.all` and `dpnp.any` aborting when reducing over an empty axis (e.g. an array with a zero-length dimension) [#3021](https://github.com/IntelPython/dpnp/pull/3021) * Released the GIL before the blocking OneMKL DFT calls in the FFT extension [#3040](https://github.com/IntelPython/dpnp/pull/3040) +* Fixed `dpnp.insert` silently ignoring out-of-bounds negative indices in a multi-element `obj`, so a mix of in-bounds and out-of-bounds indices now consistently raises `IndexError` [#3041](https://github.com/IntelPython/dpnp/pull/3041) ### Security diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index b2046ffc494..53cbd231a89 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -254,6 +254,15 @@ def _insert_array_indices(parameters, indices, values, obj): # Can safely cast the empty list to intp indices = indices.astype(dpnp.intp) + if indices.size > 0: + min_idx = int(indices.min()) + max_idx = int(indices.max()) + if min_idx < -n or max_idx > n: + oob = min_idx if min_idx < -n else max_idx + raise IndexError( + f"index {oob} is out of bounds for axis {axis} with size {n}" + ) + indices[indices < 0] += n numnew = len(indices) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 3dbd9691d4c..eba3e70b643 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -815,11 +815,18 @@ def test_error(self): with pytest.raises(TypeError): dpnp.insert(a, [], 2, axis="nonsense") - @pytest.mark.parametrize("idx", [4, -4]) - def test_index_out_of_bounds(self, idx): + @pytest.mark.parametrize( + "idx, values", + [ + ([4], [3, 4]), + ([-4], [3, 4]), + ([-6, 0], [9, 8]), + ], + ) + def test_index_out_of_bounds(self, idx, values): a = dpnp.array([0, 1, 2]) with pytest.raises(IndexError, match="out of bounds"): - dpnp.insert(a, [idx], [3, 4]) + dpnp.insert(a, idx, values) # array_split has more comprehensive test of splitting. From 9978b5573e2b959513762496691694f7936b208c Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 25 Aug 2026 15:48:54 +0200 Subject: [PATCH 2/5] Extend insert out-of-bounds tests to cover the array path and ND axis Parametrize the out-of-bounds test over both numpy and dpnp and add multi-element cases that exercise the newly added bounds check in _insert_array_indices, including the positive (max > n) branch, plus an ND case validating axis/size reporting for a non-zero axis. --- dpnp/tests/test_manipulation.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index eba3e70b643..e8265b36774 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -815,18 +815,32 @@ def test_error(self): with pytest.raises(TypeError): dpnp.insert(a, [], 2, axis="nonsense") + @testing.with_requires("numpy>=2.5.3") + @pytest.mark.parametrize("xp", [numpy, dpnp]) @pytest.mark.parametrize( "idx, values", [ + # single-element obj -> singleton path ([4], [3, 4]), ([-4], [3, 4]), + # multi-element obj -> array path ([-6, 0], [9, 8]), + ([0, 6], [9, 8]), + ([4, 4], [3, 4]), + ([-4, -5], [3, 4]), ], ) - def test_index_out_of_bounds(self, idx, values): - a = dpnp.array([0, 1, 2]) + def test_index_out_of_bounds(self, xp, idx, values): + a = xp.array([0, 1, 2]) + with pytest.raises(IndexError, match="out of bounds"): + xp.insert(a, idx, values) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) + @pytest.mark.parametrize("axis", [0, 1]) + def test_index_out_of_bounds_ndim(self, xp, axis): + a = xp.ones((3, 3)) with pytest.raises(IndexError, match="out of bounds"): - dpnp.insert(a, idx, values) + xp.insert(a, [5, 0], 9, axis=axis) # array_split has more comprehensive test of splitting. From 97b4495405f4633928824431d0606f16937c4dbe Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 26 Aug 2026 19:57:12 +0200 Subject: [PATCH 3/5] Update fixture with proper expecting numpy release version --- dpnp/tests/test_manipulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index e8265b36774..7245c6712a3 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -815,7 +815,7 @@ def test_error(self): with pytest.raises(TypeError): dpnp.insert(a, [], 2, axis="nonsense") - @testing.with_requires("numpy>=2.5.3") + @testing.with_requires("numpy>=2.6") @pytest.mark.parametrize("xp", [numpy, dpnp]) @pytest.mark.parametrize( "idx, values", From af3cb8af4cac07d21a34b55b78649ed828c644e4 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 27 Aug 2026 16:24:09 +0200 Subject: [PATCH 4/5] Hoist insert bounds check and avoid host sync for host obj Move the out-of-bounds validation out of _insert_array_indices into a shared _check_index_bounds helper called from insert(). When obj lives on the host (Python sequence/scalar or NumPy array) the bounds are checked with NumPy, avoiding any device sync; a slice is skipped since it is always in bounds; only a device-array obj needs a single host transfer, reading both extremes at once instead of two. --- dpnp/dpnp_iface_manipulation.py | 40 +++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 53cbd231a89..3b45cf74442 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -240,6 +240,35 @@ def _calc_parameters(a, axis, obj, values=None): ) +def _check_index_bounds(obj, indices, n, axis): + """ + Raise ``IndexError`` if any index in `obj` is out of bounds for `axis`. + + Mirrors the size-1 path in ``_insert_singleton_index``: when `obj` lives on + the host (a Python sequence/scalar or NumPy array) the bounds are validated + with NumPy, avoiding a device sync entirely. A slice cannot be out of bounds + (``obj.indices(n)`` is clamped to ``[0, n]``), so it is skipped. Only a + device array `obj` needs a single host transfer to read its extremes. + + """ + + if isinstance(obj, slice) or indices.size == 0: + return + + if dpnp.is_supported_array_type(obj): + min_idx, max_idx = dpnp.stack([indices.min(), indices.max()]).asnumpy() + else: + host_obj = numpy.asarray(obj) + min_idx, max_idx = host_obj.min(), host_obj.max() + + min_idx, max_idx = int(min_idx), int(max_idx) + if min_idx < -n or max_idx > n: + oob = min_idx if min_idx < -n else max_idx + raise IndexError( + f"index {oob} is out of bounds for axis {axis} with size {n}" + ) + + def _insert_array_indices(parameters, indices, values, obj): """ Utility function for ``dpnp.insert`` when indices is an array with @@ -254,15 +283,6 @@ def _insert_array_indices(parameters, indices, values, obj): # Can safely cast the empty list to intp indices = indices.astype(dpnp.intp) - if indices.size > 0: - min_idx = int(indices.min()) - max_idx = int(indices.max()) - if min_idx < -n or max_idx > n: - oob = min_idx if min_idx < -n else max_idx - raise IndexError( - f"index {oob} is out of bounds for axis {axis} with size {n}" - ) - indices[indices < 0] += n numnew = len(indices) @@ -2531,8 +2551,10 @@ def insert(arr, obj, values, axis=None): ) if indices.size == 1: + # the size-1 path validates the bounds itself while reading the index return _insert_singleton_index(params, indices, values, obj) + _check_index_bounds(obj, indices, params.n, params.axis) return _insert_array_indices(params, indices, values, obj) From 1792d77054fc2d72f63c2fa18d11c68bfccb9a41 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 27 Aug 2026 16:33:58 +0200 Subject: [PATCH 5/5] Document possible synchronization in dpnp.insert docstring --- dpnp/dpnp_iface_manipulation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 3b45cf74442..1edfdce7982 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -2464,6 +2464,13 @@ def insert(arr, obj, values, axis=None): does not occur in-place: a new array is returned. If `axis` is ``None``, `out` is a flattened array. + Warnings + -------- + This function might synchronize in order to validate that the indices are + within bounds. This may harm performance in some applications. To avoid + synchronization, pass `obj` as a Python scalar or sequence, or as a NumPy + array. + See Also -------- :obj:`dpnp.append` : Append elements at the end of an array.