Skip to content

Commit 28dfb24

Browse files
authored
Handle misaligned arrays in as_slice (#525)
Handle misaligned arrays in `as_slice` Previously, `as_slice` would only check that arrays are contiguous. While misaligned non-empty arrays remain problematic across the library (`get`, for example, would still be invalid), this change begins handling this situation in a function that already returns `Result`. As a convenience, the empty slice is returned for zero-length arrays, regardless of the alignment of the underlying pointer. Misaligned zero-length pointers can fairly easily arise from allocator optimisations. For example, CPython reliably returned an odd-address pointer in `bytearray()` in (at least) CPython 3.14.0 on Linux x86-64, which caused empty Numpy arrays passing through Pickle protocol 5 (with the default handling of its `PickleBuffer`s) to be backed by a misaligned pointer for multi-byte aligned dtypes.
1 parent 69ca88e commit 28dfb24

File tree

7 files changed

+128
-23
lines changed

7 files changed

+128
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- v0.28.0
33
- Fix mismatched behavior between `PyArrayLike1` and `PyArrayLike2` when used with floats ([#520](https://github.com/PyO3/rust-numpy/pull/520))
44
- Add ownership-moving conversions into `PyReadonlyArray` and `PyReadwriteArray` ([#524](https://github.com/PyO3/rust-numpy/pull/524))
5+
- Fix UB when calling `PyArray::as_slice` and `as_slice_mut` on misaligned arrays ([#525](https://github.com/PyO3/rust-numpy/pull/525))
56

67
- v0.27.1
78
- Bump ndarray dependency to v0.17. ([#516](https://github.com/PyO3/rust-numpy/pull/516))

src/array.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::cold;
2626
use crate::convert::{ArrayExt, IntoPyArray, NpyIndex, ToNpyDims, ToPyArray};
2727
use crate::dtype::{Element, PyArrayDescrMethods};
2828
use crate::error::{
29-
BorrowError, DimensionalityError, FromVecError, IgnoreError, NotContiguousError, TypeError,
29+
AsSliceError, BorrowError, DimensionalityError, FromVecError, IgnoreError, TypeError,
3030
DIMENSIONALITY_MISMATCH_ERR, MAX_DIMENSIONALITY_ERR,
3131
};
3232
use crate::npyffi::{self, npy_intp, NPY_ORDER, PY_ARRAY_API};
@@ -739,15 +739,20 @@ pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> + Sized {
739739
/// or concurrently modified by Python or other native code.
740740
///
741741
/// Please consider the safe alternative [`PyReadonlyArray::as_slice`].
742-
unsafe fn as_slice(&self) -> Result<&[T], NotContiguousError>
742+
unsafe fn as_slice(&self) -> Result<&[T], AsSliceError>
743743
where
744744
T: Element,
745745
D: Dimension,
746746
{
747-
if self.is_contiguous() {
748-
Ok(slice::from_raw_parts(self.data(), self.len()))
747+
let len = self.len();
748+
if len == 0 {
749+
// We can still produce a slice over zero objects regardless of whether
750+
// the underlying pointer is aligned or not.
751+
Ok(&[])
752+
} else if self.is_aligned() && self.is_contiguous() {
753+
Ok(slice::from_raw_parts(self.data(), len))
749754
} else {
750-
Err(NotContiguousError)
755+
Err(AsSliceError)
751756
}
752757
}
753758

@@ -761,15 +766,20 @@ pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> + Sized {
761766
///
762767
/// Please consider the safe alternative [`PyReadwriteArray::as_slice_mut`].
763768
#[allow(clippy::mut_from_ref)]
764-
unsafe fn as_slice_mut(&self) -> Result<&mut [T], NotContiguousError>
769+
unsafe fn as_slice_mut(&self) -> Result<&mut [T], AsSliceError>
765770
where
766771
T: Element,
767772
D: Dimension,
768773
{
769-
if self.is_contiguous() {
770-
Ok(slice::from_raw_parts_mut(self.data(), self.len()))
774+
let len = self.len();
775+
if len == 0 {
776+
// We can still produce a slice over zero objects regardless of whether
777+
// the underlying pointer is aligned or not.
778+
Ok(&mut [])
779+
} else if self.is_aligned() && self.is_contiguous() {
780+
Ok(slice::from_raw_parts_mut(self.data(), len))
771781
} else {
772-
Err(NotContiguousError)
782+
Err(AsSliceError)
773783
}
774784
}
775785

@@ -951,7 +961,7 @@ pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> + Sized {
951961
/// })
952962
/// # }
953963
/// ```
954-
fn to_vec(&self) -> Result<Vec<T>, NotContiguousError>
964+
fn to_vec(&self) -> Result<Vec<T>, AsSliceError>
955965
where
956966
T: Element,
957967
D: Dimension;
@@ -1503,7 +1513,7 @@ impl<'py, T, D> PyArrayMethods<'py, T, D> for Bound<'py, PyArray<T, D>> {
15031513
unsafe { self.cast_unchecked() }
15041514
}
15051515

1506-
fn to_vec(&self) -> Result<Vec<T>, NotContiguousError>
1516+
fn to_vec(&self) -> Result<Vec<T>, AsSliceError>
15071517
where
15081518
T: Element,
15091519
D: Dimension,

src/borrow/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ use pyo3::{Borrowed, Bound, CastError, FromPyObject, PyAny, PyResult};
180180
use crate::array::{PyArray, PyArrayMethods};
181181
use crate::convert::NpyIndex;
182182
use crate::dtype::Element;
183-
use crate::error::{BorrowError, NotContiguousError};
183+
use crate::error::{AsSliceError, BorrowError};
184184
use crate::npyffi::flags;
185185
use crate::untyped_array::PyUntypedArrayMethods;
186186

@@ -268,7 +268,7 @@ where
268268

269269
/// Provide an immutable slice view of the interior of the NumPy array if it is contiguous.
270270
#[inline(always)]
271-
pub fn as_slice(&self) -> Result<&[T], NotContiguousError> {
271+
pub fn as_slice(&self) -> Result<&[T], AsSliceError> {
272272
// SAFETY: Global borrow flags ensure aliasing discipline.
273273
unsafe { self.array.as_slice() }
274274
}
@@ -511,7 +511,7 @@ where
511511

512512
/// Provide a mutable slice view of the interior of the NumPy array if it is contiguous.
513513
#[inline(always)]
514-
pub fn as_slice_mut(&mut self) -> Result<&mut [T], NotContiguousError> {
514+
pub fn as_slice_mut(&mut self) -> Result<&mut [T], AsSliceError> {
515515
// SAFETY: Global borrow flags ensure aliasing discipline.
516516
unsafe { self.array.as_slice_mut() }
517517
}

src/error.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,15 @@ impl fmt::Display for FromVecError {
141141

142142
impl_pyerr!(FromVecError);
143143

144-
/// Represents that the given array is not contiguous.
144+
/// Represents that the given array is not compatible with viewing as a Rust slice.
145145
#[derive(Debug)]
146-
pub struct NotContiguousError;
147-
148-
impl fmt::Display for NotContiguousError {
146+
pub struct AsSliceError;
147+
impl fmt::Display for AsSliceError {
149148
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150-
write!(f, "The given array is not contiguous")
149+
write!(f, "The given array is not contiguous or is misaligned.")
151150
}
152151
}
153-
154-
impl_pyerr!(NotContiguousError);
152+
impl_pyerr!(AsSliceError);
155153

156154
/// Inidcates why borrowing an array failed.
157155
#[derive(Debug)]

src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub use crate::borrow::{
106106
};
107107
pub use crate::convert::{IntoPyArray, NpyIndex, ToNpyDims, ToPyArray};
108108
pub use crate::dtype::{dtype, Complex32, Complex64, Element, PyArrayDescr, PyArrayDescrMethods};
109-
pub use crate::error::{BorrowError, FromVecError, NotContiguousError};
109+
pub use crate::error::{AsSliceError, BorrowError, FromVecError};
110110
pub use crate::npyffi::{PY_ARRAY_API, PY_UFUNC_API};
111111
pub use crate::strings::{PyFixedString, PyFixedUnicode};
112112
pub use crate::sum_products::{dot, einsum, inner};
@@ -130,6 +130,11 @@ pub mod prelude {
130130
pub use crate::untyped_array::PyUntypedArrayMethods;
131131
}
132132

133+
/// Deprecated type alias to [`AsSliceError`]. The new name is preferred because arrays might also
134+
/// fail to view as a slice due to misalignment.
135+
#[deprecated(note = "use AsSliceError instead", since = "0.28.0")]
136+
pub type NonContiguousError = AsSliceError;
137+
133138
#[cfg(doctest)]
134139
mod doctest {
135140
macro_rules! doc_comment {

src/untyped_array.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,39 @@ pub trait PyUntypedArrayMethods<'py>: Sealed {
102102
/// [PyArray_DTYPE]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_DTYPE
103103
fn dtype(&self) -> Bound<'py, PyArrayDescr>;
104104

105+
/// Returns `true` if the internal data of the array is aligned for the dtype.
106+
///
107+
/// Note that NumPy considers zero-length data to be aligned regardless of the base pointer,
108+
/// which is a weaker condition than Rust's slice guarantees. [PyArrayMethods::as_slice] will
109+
/// safely handle the case of a misaligned zero-length array.
110+
///
111+
/// # Example
112+
///
113+
/// ```
114+
/// use numpy::{PyArray1, PyUntypedArrayMethods};
115+
/// use pyo3::{types::{IntoPyDict, PyAnyMethods}, Python, ffi::c_str};
116+
///
117+
/// # fn main() -> pyo3::PyResult<()> {
118+
/// Python::attach(|py| {
119+
/// let array = PyArray1::<u16>::zeros(py, 8, false);
120+
/// assert!(array.is_aligned());
121+
///
122+
/// let view = py
123+
/// .eval(
124+
/// c_str!("array.view('u1')[1:-1].view('u2')"),
125+
/// None,
126+
/// Some(&[("array", array)].into_py_dict(py)?),
127+
/// )?
128+
/// .cast_into::<PyArray1<u16>>()?;
129+
/// assert!(!view.is_aligned());
130+
/// # Ok(())
131+
/// })
132+
/// # }
133+
/// ```
134+
fn is_aligned(&self) -> bool {
135+
unsafe { check_flags(&*self.as_array_ptr(), npyffi::NPY_ARRAY_ALIGNED) }
136+
}
137+
105138
/// Returns `true` if the internal data of the array is contiguous,
106139
/// indepedently of whether C-style/row-major or Fortran-style/column-major.
107140
///

tests/array.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ fn not_contiguous_array(py: Python<'_>) -> Bound<'_, PyArray1<i32>> {
3232
.unwrap()
3333
}
3434

35+
fn not_aligned_array(py: Python<'_>) -> Bound<'_, PyArray1<u16>> {
36+
py.eval(
37+
c_str!("np.zeros(8, dtype='u2').view('u1')[1:-1].view('u2')"),
38+
None,
39+
Some(&get_np_locals(py)),
40+
)
41+
.unwrap()
42+
.cast_into()
43+
.unwrap()
44+
}
45+
3546
#[test]
3647
fn new_c_order() {
3748
Python::attach(|py| {
@@ -51,6 +62,7 @@ fn new_c_order() {
5162
assert!(arr.is_contiguous());
5263
assert!(arr.is_c_contiguous());
5364
assert!(!arr.is_fortran_contiguous());
65+
assert!(arr.is_aligned());
5466
});
5567
}
5668

@@ -73,6 +85,7 @@ fn new_fortran_order() {
7385
assert!(arr.is_contiguous());
7486
assert!(!arr.is_c_contiguous());
7587
assert!(arr.is_fortran_contiguous());
88+
assert!(arr.is_aligned());
7689
});
7790
}
7891

@@ -179,7 +192,52 @@ fn as_slice() {
179192

180193
let not_contiguous = not_contiguous_array(py);
181194
let err = not_contiguous.readonly().as_slice().unwrap_err();
182-
assert_eq!(err.to_string(), "The given array is not contiguous");
195+
assert_eq!(
196+
err.to_string(),
197+
"The given array is not contiguous or is misaligned."
198+
);
199+
let err = not_contiguous.readwrite().as_slice_mut().unwrap_err();
200+
assert_eq!(
201+
err.to_string(),
202+
"The given array is not contiguous or is misaligned."
203+
);
204+
205+
let not_aligned = not_aligned_array(py);
206+
assert!(!not_aligned.is_aligned());
207+
let err = not_aligned.readonly().as_slice().unwrap_err();
208+
assert_eq!(
209+
err.to_string(),
210+
"The given array is not contiguous or is misaligned."
211+
);
212+
let err = not_aligned.readwrite().as_slice_mut().unwrap_err();
213+
assert_eq!(
214+
err.to_string(),
215+
"The given array is not contiguous or is misaligned."
216+
);
217+
218+
let misaligned_empty: Bound<'_, PyArray1<u16>> = {
219+
let arr = not_aligned_array(py);
220+
py.eval(
221+
c_str!("arr[0:0]"),
222+
None,
223+
Some(&[("arr", arr)].into_py_dict(py).unwrap()),
224+
)
225+
.unwrap()
226+
.cast_into()
227+
.unwrap()
228+
};
229+
assert_eq!(
230+
misaligned_empty.readonly().as_slice().unwrap(),
231+
&[] as &[u16]
232+
);
233+
assert_eq!(
234+
misaligned_empty.readwrite().as_slice_mut().unwrap(),
235+
&mut [] as &mut [u16]
236+
);
237+
assert_eq!(
238+
misaligned_empty.readonly().to_vec().unwrap(),
239+
Vec::<u16>::new()
240+
);
183241
});
184242
}
185243

0 commit comments

Comments
 (0)