From b943627b9ed835aac8071c8912bd1e6add068a5b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 31 Oct 2026 12:50:32 +0100 Subject: [PATCH 1/2] fix soundness when attempting to create a view of an array with unrepresentable strides --- CHANGELOG.md | 1 + src/array.rs | 21 +++++++++++++++++++-- src/borrow/mod.rs | 2 ++ src/error.rs | 2 ++ tests/array.rs | 17 +++++++++++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 858e4a238..5411851a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - Unreleased + - Fix unsoundness when attempting to create a view to a numpy array with strides that are not a multiple of the element size ([#578](https://github.com/PyO3/rust-numpy/pull/578)) - v0.30.0 - `PyReadonlyArray`/`PyReadwriteArray` extraction now returns a `PyErr` which reports the actual dtype/dimensionality mismatch instead of nonsensical `CastError` messages like “'ndarray' is not an instance of 'ndarray'” ([#562](https://github.com/PyO3/rust-numpy/pull/562)) diff --git a/src/array.rs b/src/array.rs index ef5a5ce80..ac55a423f 100644 --- a/src/array.rs +++ b/src/array.rs @@ -19,7 +19,6 @@ use pyo3::{ types::{DerefToPyAny, PyModule}, }; -use crate::borrow::{PyReadonlyArray, PyReadwriteArray}; use crate::cold; use crate::convert::{ArrayExt, IntoPyArray, NpyIndex, ToNpyDims, ToPyArray}; use crate::dtype::{Element, PyArrayDescrMethods}; @@ -30,6 +29,10 @@ use crate::error::{ use crate::npyffi::{self, _PyArray_GET_ITEM_DATA, NPY_ORDER, PY_ARRAY_API, npy_intp}; use crate::slice_container::PySliceContainer; use crate::untyped_array::{PyUntypedArray, PyUntypedArrayMethods}; +use crate::{ + borrow::{PyReadonlyArray, PyReadwriteArray}, + error::UNSUPPORTED_STRIDES_ERR, +}; /// A safe, statically-typed wrapper for NumPy's [`ndarray`][ndarray] class. /// @@ -1373,6 +1376,7 @@ where Some(unsafe { slf.data().offset(offset) }) } +#[track_caller] fn as_view(slf: &Bound<'_, PyArray>, from_shape_ptr: F) -> ArrayBase where T: Element, @@ -1380,6 +1384,7 @@ where S: RawData, F: FnOnce(StrideShape, *mut T) -> ArrayBase, { + #[track_caller] fn inner( shape: &[usize], strides: &[isize], @@ -1389,6 +1394,11 @@ where let shape = D::from_dimension(&Dim(shape)).expect(DIMENSIONALITY_MISMATCH_ERR); assert!(strides.len() <= 32, "{}", MAX_DIMENSIONALITY_ERR); + assert!( + strides.iter().all(|s| s % itemsize as isize == 0), + "{}", + UNSUPPORTED_STRIDES_ERR + ); let mut new_strides = D::zeros(strides.len()); let mut inverted_axes = 0_u32; @@ -1450,7 +1460,10 @@ where return None; } - if strides.iter().any(|strides| *strides < 0) { + if strides + .iter() + .any(|strides| *strides < 0 || *strides as usize % mem::size_of::() != 0) + { return None; } @@ -1574,6 +1587,7 @@ impl<'py, T, D> PyArrayMethods<'py, T, D> for Bound<'py, PyArray> { self.clone().try_into_readwrite() } + #[track_caller] unsafe fn as_array(&self) -> ArrayView<'_, T, D> where T: Element, @@ -1584,6 +1598,7 @@ impl<'py, T, D> PyArrayMethods<'py, T, D> for Bound<'py, PyArray> { }) } + #[track_caller] unsafe fn as_array_mut(&self) -> ArrayViewMut<'_, T, D> where T: Element, @@ -1594,6 +1609,7 @@ impl<'py, T, D> PyArrayMethods<'py, T, D> for Bound<'py, PyArray> { }) } + #[track_caller] fn as_raw_array(&self) -> RawArrayView where T: Element, @@ -1604,6 +1620,7 @@ impl<'py, T, D> PyArrayMethods<'py, T, D> for Bound<'py, PyArray> { }) } + #[track_caller] fn as_raw_array_mut(&self) -> RawArrayViewMut where T: Element, diff --git a/src/borrow/mod.rs b/src/borrow/mod.rs index 2d42fa05e..129dab132 100644 --- a/src/borrow/mod.rs +++ b/src/borrow/mod.rs @@ -262,6 +262,7 @@ where /// Provides an immutable array view of the interior of the NumPy array. #[inline(always)] + #[track_caller] pub fn as_array(&self) -> ArrayView<'_, T, D> { // SAFETY: Global borrow flags ensure aliasing discipline. unsafe { self.array.as_array() } @@ -505,6 +506,7 @@ where /// Provides a mutable array view of the interior of the NumPy array. #[inline(always)] + #[track_caller] pub fn as_array_mut(&mut self) -> ArrayViewMut<'_, T, D> { // SAFETY: Global borrow flags ensure aliasing discipline. unsafe { self.array.as_array_mut() } diff --git a/src/error.rs b/src/error.rs index 88e94a892..c265981f8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,8 @@ use crate::dtype::PyArrayDescr; /// /// [NPY_MAXDIMS]: https://github.com/numpy/numpy/blob/4c60b3263ac50e5e72f6a909e156314fc3c9cba0/numpy/core/include/numpy/ndarraytypes.h#L40 pub(crate) const MAX_DIMENSIONALITY_ERR: &str = "unexpected dimensionality: NumPy is expected to limit arrays to 32 or fewer dimensions.\nPlease report a bug against the `rust-numpy` crate."; +pub(crate) const UNSUPPORTED_STRIDES_ERR: &str = + "arrays with strides that are not a multiple of the element size are currently unsupported."; pub(crate) const DIMENSIONALITY_MISMATCH_ERR: &str = "inconsistent dimensionalities: The dimensionality expected by `PyArray` does not match that given by NumPy.\nPlease report a bug against the `rust-numpy` crate."; diff --git a/tests/array.rs b/tests/array.rs index a960c5d52..1d111cf06 100644 --- a/tests/array.rs +++ b/tests/array.rs @@ -169,6 +169,23 @@ fn as_array() { }); } +#[test] +#[should_panic] +fn test_unsupported_strides_panic() { + Python::attach(|py| { + let field = py.eval( + c"np.array([(1, 0), (2, 0), (3, 0)], dtype=[('num', 'c16'), ('other', 'f8')])['num']", + None, + Some(&get_np_locals(py)), + )? + .cast_into::>()?; + + let _view = field.readonly().as_array(); // unrepresentable -> panic + Ok::<_, pyo3::PyErr>(()) + }) + .unwrap(); +} + #[test] fn as_raw_array() { Python::attach(|py| { From c7206d7246f6b0a8a2d3de4afdd3ba7829e54add Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:31:15 +0100 Subject: [PATCH 2/2] fix naming Co-authored-by: MarkusSintonen <12939780+MarkusSintonen@users.noreply.github.com> --- src/array.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/array.rs b/src/array.rs index ac55a423f..c6388b036 100644 --- a/src/array.rs +++ b/src/array.rs @@ -1462,7 +1462,7 @@ where if strides .iter() - .any(|strides| *strides < 0 || *strides as usize % mem::size_of::() != 0) + .any(|stride| *stride < 0 || *stride as usize % mem::size_of::() != 0) { return None; }