From 1148eba253efed6117f1ddd14bce8f8d0586a9aa Mon Sep 17 00:00:00 2001 From: sayed ahrar Date: Thu, 1 Oct 2026 13:15:07 +0530 Subject: [PATCH 1/2] reject negative indices in numpy array bounds checks --- include/pybind11/numpy.h | 6 +++--- tests/test_numpy_array.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index a26ac2b4cd..4b2a2de8c5 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -1158,7 +1158,7 @@ class array : public buffer { /// Dimension along a given axis ssize_t shape(ssize_t dim) const { - if (dim >= ndim()) { + if (dim < 0 || dim >= ndim()) { fail_dim_check(dim, "invalid axis"); } return shape()[dim]; @@ -1176,7 +1176,7 @@ class array : public buffer { /// Stride along a given axis ssize_t strides(ssize_t dim) const { - if (dim >= ndim()) { + if (dim < 0 || dim >= ndim()) { fail_dim_check(dim, "invalid axis"); } return strides()[dim]; @@ -1356,7 +1356,7 @@ class array : public buffer { template void check_dimensions_impl(ssize_t axis, const ssize_t *shape, ssize_t i, Ix... index) const { - if (i >= *shape) { + if (i < 0 || i >= *shape) { throw index_error(std::string("index ") + std::to_string(i) + " is out of bounds for axis " + std::to_string(axis) + " with size " + std::to_string(*shape)); diff --git a/tests/test_numpy_array.py b/tests/test_numpy_array.py index e3f29e92a6..71e79ef242 100644 --- a/tests/test_numpy_array.py +++ b/tests/test_numpy_array.py @@ -69,6 +69,12 @@ def test_array_attributes(): with pytest.raises(IndexError) as excinfo: m.strides(a, 2) assert str(excinfo.value) == "invalid axis: 2 (ndim = 2)" + with pytest.raises(IndexError) as excinfo: + m.shape(a, -1) + assert str(excinfo.value) == "invalid axis: -1 (ndim = 2)" + with pytest.raises(IndexError) as excinfo: + m.strides(a, -1) + assert str(excinfo.value) == "invalid axis: -1 (ndim = 2)" assert not m.writeable(a) assert m.size(a) == 6 assert m.itemsize(a) == 2 @@ -217,6 +223,14 @@ def test_bounds_check(arr): with pytest.raises(IndexError) as excinfo: func(arr, 0, 4) assert str(excinfo.value) == "index 4 is out of bounds for axis 1 with size 3" + # Negative indices are out of bounds too (they would address memory + # before the start of the buffer). + with pytest.raises(IndexError) as excinfo: + func(arr, -1, 0) + assert str(excinfo.value) == "index -1 is out of bounds for axis 0 with size 2" + with pytest.raises(IndexError) as excinfo: + func(arr, 0, -1) + assert str(excinfo.value) == "index -1 is out of bounds for axis 1 with size 3" def test_make_c_f_array(): From d5d782d301d416eb064b94910604920548f0dc53 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 7 Oct 2026 14:39:57 -0400 Subject: [PATCH 2/2] fix(numpy): clarify negative index errors and document no wrapping Use a single unsigned bounds compare, say in the error message that negative indices are not supported, document the behavior on the accessors, and cover offset_at/offset_at_t in test_bounds_check. Assisted-by: ClaudeCode:claude-opus-5-5 --- include/pybind11/numpy.h | 27 ++++++++++++++++----------- tests/test_numpy_array.py | 15 +++++++++++---- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 4b2a2de8c5..fd960f1dfa 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -1156,7 +1156,7 @@ class array : public buffer { } #endif - /// Dimension along a given axis + /// Dimension along a given axis. Throws if dim is negative or >= ndim() (no wrapping). ssize_t shape(ssize_t dim) const { if (dim < 0 || dim >= ndim()) { fail_dim_check(dim, "invalid axis"); @@ -1174,7 +1174,7 @@ class array : public buffer { } #endif - /// Stride along a given axis + /// Stride along a given axis. Throws if dim is negative or >= ndim() (no wrapping). ssize_t strides(ssize_t dim) const { if (dim < 0 || dim >= ndim()) { fail_dim_check(dim, "invalid axis"); @@ -1196,15 +1196,16 @@ class array : public buffer { } /// Pointer to the contained data. If index is not provided, points to the - /// beginning of the buffer. May throw if the index would lead to out of bounds access. + /// beginning of the buffer. Throws if an index is out of bounds; negative indices are not + /// wrapped. template const void *data(Ix... index) const { return static_cast(detail::array_proxy(m_ptr)->data + offset_at(index...)); } /// Mutable pointer to the contained data. If index is not provided, points to the - /// beginning of the buffer. May throw if the index would lead to out of bounds access. - /// May throw if the array is not writeable. + /// beginning of the buffer. Throws if an index is out of bounds; negative indices are not + /// wrapped. May throw if the array is not writeable. template void *mutable_data(Ix... index) { check_writeable(); @@ -1212,7 +1213,7 @@ class array : public buffer { } /// Byte offset from beginning of the array to a given index (full or partial). - /// May throw if the index would lead to out of bounds access. + /// Throws if an index is out of bounds; negative indices are not wrapped. template ssize_t offset_at(Ix... index) const { if ((ssize_t) sizeof...(index) > ndim()) { @@ -1224,7 +1225,7 @@ class array : public buffer { ssize_t offset_at() const { return 0; } /// Item count from beginning of the array to a given index (full or partial). - /// May throw if the index would lead to out of bounds access. + /// Throws if an index is out of bounds; negative indices are not wrapped. template ssize_t index_at(Ix... index) const { return offset_at(index...) / itemsize(); @@ -1356,10 +1357,12 @@ class array : public buffer { template void check_dimensions_impl(ssize_t axis, const ssize_t *shape, ssize_t i, Ix... index) const { - if (i < 0 || i >= *shape) { + // Unsigned compare also rejects negative indices + if (static_cast(i) >= static_cast(*shape)) { throw index_error(std::string("index ") + std::to_string(i) + " is out of bounds for axis " + std::to_string(axis) - + " with size " + std::to_string(*shape)); + + " with size " + std::to_string(*shape) + + (i < 0 ? " (negative indices are not supported)" : "")); } check_dimensions_impl(axis + 1, shape + 1, index...); } @@ -1449,7 +1452,8 @@ class array_t : public array { return static_cast(array::mutable_data(index...)); } - // Reference to element at a given index + // Reference to element at a given index. Throws if out of bounds; negative indices are not + // wrapped. template const T &at(Ix... index) const { if ((ssize_t) sizeof...(index) != ndim()) { @@ -1459,7 +1463,8 @@ class array_t : public array { + byte_offset(ssize_t(index)...) / itemsize()); } - // Mutable reference to element at a given index + // Mutable reference to element at a given index. Throws if out of bounds; negative indices + // are not wrapped. template T &mutable_at(Ix... index) { if ((ssize_t) sizeof...(index) != ndim()) { diff --git a/tests/test_numpy_array.py b/tests/test_numpy_array.py index 71e79ef242..2110c7c939 100644 --- a/tests/test_numpy_array.py +++ b/tests/test_numpy_array.py @@ -210,6 +210,8 @@ def test_bounds_check(arr): for func in ( m.index_at, m.index_at_t, + m.offset_at, + m.offset_at_t, m.data, m.data_t, m.mutate_data, @@ -223,14 +225,19 @@ def test_bounds_check(arr): with pytest.raises(IndexError) as excinfo: func(arr, 0, 4) assert str(excinfo.value) == "index 4 is out of bounds for axis 1 with size 3" - # Negative indices are out of bounds too (they would address memory - # before the start of the buffer). + # Negative indices are not wrapped like in NumPy with pytest.raises(IndexError) as excinfo: func(arr, -1, 0) - assert str(excinfo.value) == "index -1 is out of bounds for axis 0 with size 2" + assert ( + str(excinfo.value) + == "index -1 is out of bounds for axis 0 with size 2 (negative indices are not supported)" + ) with pytest.raises(IndexError) as excinfo: func(arr, 0, -1) - assert str(excinfo.value) == "index -1 is out of bounds for axis 1 with size 3" + assert ( + str(excinfo.value) + == "index -1 is out of bounds for axis 1 with size 3 (negative indices are not supported)" + ) def test_make_c_f_array():