From ca1f8423abf4d224de326d37bd9c4d16a2c18842 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 25 Aug 2026 15:27:03 +0200 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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. From a0b32919edad2fad204a685793d8481043ce568b Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 31 Aug 2026 14:28:53 +0200 Subject: [PATCH 6/6] Fix insert bounds check for out-of-bounds boolean mask obj A boolean mask obj passed as a host object (Python sequence or NumPy array) was validated on its raw boolean values instead of the selected positions, so an oversized mask could pick indices beyond the axis and silently produce a wrong result. Flatnonzero the host mask before the bounds check, matching NumPy and the device path. Add regression tests covering host list, host NumPy, and device dpnp masks. --- dpnp/dpnp_iface_manipulation.py | 4 ++++ dpnp/tests/test_manipulation.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 1edfdce7982..8d3050da4e0 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -259,6 +259,10 @@ def _check_index_bounds(obj, indices, n, axis): min_idx, max_idx = dpnp.stack([indices.min(), indices.max()]).asnumpy() else: host_obj = numpy.asarray(obj) + if host_obj.dtype == dpnp.bool: + # a boolean mask selects positions, which (for an oversized mask) + # can fall out of bounds, so validate the flatnonzero result + host_obj = numpy.flatnonzero(host_obj) min_idx, max_idx = host_obj.min(), host_obj.max() min_idx, max_idx = int(min_idx), int(max_idx) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 7245c6712a3..f14c8644108 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -842,6 +842,20 @@ def test_index_out_of_bounds_ndim(self, xp, axis): with pytest.raises(IndexError, match="out of bounds"): xp.insert(a, [5, 0], 9, axis=axis) + @pytest.mark.parametrize( + "obj", + [ + [True, False, False, False, True], + numpy.array([True, False, False, False, True]), + dpnp.array([True, False, False, False, True]), + ], + ids=["list", "numpy", "dpnp"], + ) + def test_bool_mask_out_of_bounds(self, obj): + a = dpnp.array([0, 1, 2]) + with pytest.raises(IndexError, match="out of bounds"): + dpnp.insert(a, obj, 9) + # array_split has more comprehensive test of splitting. # only do simple test on hsplit, vsplit, and dsplit