Null pointer dereference on C API allocation failure in PyArray::new_with_data
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 56/100
Research direction
Start in src/array.rs at PyArray::new_with_data and new_uninit, then trace the IntoPyArray::into_pyarray and PyArray::from_owned_array entry points. Check the result of PyArray_NewFromDescr and the ownership path around PyArray_SetBaseObject. Done means allocation failure avoids the null dereference and owned resources are handled without leaking.
Written by the indexing model from the issue text.
Description
[!NOTE]
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
Generally I'm not too worried about UB around OOM, but sometimes it matters when you're using a custom allocator / embedded systems.
In PyArray::new_with_data (and similarly in new_uninit), the NumPy C API function PyArray_NewFromDescr is invoked to allocate a new underlying PyArrayObject on the Python heap.
If Python memory allocation fails under low-memory conditions (OOM) or if invalid dimension parameters exceeding C API limits (such as NPY_MAXDIMS) are supplied, PyArray_NewFromDescr sets a Python exception and returns a null pointer (NULL). Immediately following this invocation, without verifying whether ptr is non-null, PY_ARRAY_API.PyArray_SetBaseObject is invoked with ptr as the array argument. In the underlying NumPy C API (arrayobject.c), PyArray_SetBaseObject directly dereferences the target array pointer (arr->base = obj). Passing a null pointer triggers an immediate null pointer dereference and process crash (segmentation fault / undefined behavior).
This vulnerability is triggerable from safe public Rust entry points such as IntoPyArray::into_pyarray and PyArray::from_owned_array. When converting an owned array or collection whose shape exceeds NumPy C API limits or under exhausted heap memory conditions, safe callers experience immediate undefined behavior and process termination rather than returning a Python memory error or unwinding cleanly.
Suggested Fix
Verify that ptr is non-null immediately after calling PyArray_NewFromDescr (and similarly in new_uninit). If ptr.is_null(), ensure that container (or any owned resources) is safely disposed of or converted back to an owned wrapper to prevent memory leaks, and propagate an error or trigger a clean Rust panic before calling PyArray_SetBaseObject or Bound::from_owned_ptr.
[!NOTE]
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue. The audit report has not been human-reviewed, it may contain misleading claims.
Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review: numpy (v0_28)
Overall Safety Assessment
numpy (v0_28) provides safe Rust bindings and N-dimensional array integration for NumPy via PyO3. The crate architecture relies heavily on dynamically loaded C API function pointers (PY_ARRAY_API and PY_UFUNC_API), type-erased heap containers (PySliceContainer), and global dynamic borrow tracking (BorrowFlags). While the high-level API design aims to encapsulate raw pointers inside transparent PyO3 Bound wrappers and runtime aliasing checks, the codebase exhibits a high density of unsafe operations. The audit identified two critical soundness findings: an unconditional segfault upon Python memory allocation failure (OOM) during array creation, and potential undefined behavior from pointer::offset_from across distinct heap allocations during aliasing conflict checks. Additionally, numerous internal low-level FFI wrappers and pointer helpers lack rigorous safety documentation and proof-obligation annotations.
Critical Findings
1. Null Pointer Dereference / Segfault on Python Memory Allocation Failure in PyArray::new_with_data (src/array.rs:248) ๐ด ๐จ
- Priority: ๐ด High
- Threat Vector: ๐จ Untrusted Input
- Bug Type: Null Pointer Dereference
- Vulnerability: In
PyArray::new_with_data(and similarly innew_uninit),PY_ARRAY_API.PyArray_NewFromDescr(...)is invoked to allocate a new underlyingPyArrayObjecton the Python heap. If Python runs out of memory (MemoryError) or invalid dimensions are supplied,PyArray_NewFromDescrreturnsNULL(ptr == 0x0). Immediately following this invocation, without checking whetherptris non-null,PY_ARRAY_API.PyArray_SetBaseObject(py, ptr as *mut npyffi::PyArrayObject, container as *mut ffi::PyObject)is invoked. In the NumPy C API (arrayobject.c),PyArray_SetBaseObject(arr, obj)directly dereferencesarr(arr->base = obj). PassingNULLasarrtriggers an immediate null pointer dereference and process crash (segfault / Undefined Behavior). - Impact: In safe Rust entry points such as
IntoPyArray::into_pyarray(which callsfrom_raw_parts->new_with_data), allocation failures under low-memory conditions result in immediate undefined behavior and process termination rather than unwinding cleanly or returning a Python memory error. - Remediation: Verify
if ptr.is_null()immediately after callingPyArray_NewFromDescr. If null, ensurecontaineris safely disposed of (or converted back to an owned reference/bound object to prevent leaks) and return an error or trigger a clean panic before callingPyArray_SetBaseObject.
2. Undefined Behavior from pointer::offset_from Across Disjoint Allocations in BorrowKey::conflicts (src/borrow/shared.rs:245) ๐ ๐งช
-
Priority: ๐ Medium
-
Threat Vector: ๐งช Contrived Setup
-
Bug Type: Out-of-Bounds Pointer Arithmetic, Pointer Provenance Violation
-
Vulnerability: In
BorrowKey::conflicts, to determine whether two aliased NumPy arrays conflict in memory, the byte distance between their data buffers is computed viaunsafe { self.data_ptr.offset_from(other.data_ptr).abs() }. According to standard library documentation (std::ptr::offset_from), invokingoffset_fromis undefined behavior unless both pointers are derived from the exact same allocated object (provenance domain) and their difference is withinisize::MAXbytes. While two arrays sharing the same root NumPy base object typically share the same underlying memory buffer, custom C extensions, ctypes/cffi code, orPyArray_SetBaseObjectusages can attach the same Python base object to array descriptors pointing to completely disjoint heap allocations. Evaluatingoffset_fromacross disjoint allocations triggers undefined behavior. -
Impact: Inspecting or borrowing malformed or custom-backed NumPy arrays from safe Rust code can trigger miscompilations or invalid pointer distance calculations.
-
Remediation: Remove the
unsafeblock entirely and replaceoffset_fromwith(self.data_ptr as usize).abs_diff(other.data_ptr as usize). Casting raw pointers tousizeand computing their absolute difference calculates the exact byte distance safely without anyunsafeblock or provenance restrictions.
Fishy Findings
1. Unchecked Return Value and Reference Leak on Failure in PyArray::new_with_data (src/array.rs:271) ๐ก โ ๏ธ
-
Priority: ๐ก Low
-
Threat Vector: โ ๏ธ Accidental Misuse
-
Bug Type: Resource Leak
-
PyArray_SetBaseObjectreturnsc_int(0 on success, -1 on failure), but its return value is discarded. IfPyArray_SetBaseObjectfails (-1), NumPy C API semantics state it may or may not have stolen the reference tocontainer. Furthermore,containeris passed as a raw pointer*mut PyAnyderived fromcontainer.into_ptr(). IfBound::from_owned_ptr(py, ptr)on line 277 panics, or ifPyArray_SetBaseObjectfails without stealing the reference, the owned reference count oncontaineris leaked.
2. Fragile FFI Assumption in PyArrayDescr::new (src/dtype.rs:86) Regarding PyArray_DescrConverter2 ๐ก โ ๏ธ
-
Priority: ๐ก Low
-
Threat Vector: โ ๏ธ Accidental Misuse
-
Bug Type: Fragile FFI Assumption
-
PyArray_DescrConverter2is called with&mut descrinitialized tonull_mut(). Ifobjcannot be converted,PyArray_DescrConverter2returnsNPY_FAIL(0) and leavesdescrnull. The code subsequently callsBound::from_owned_ptr_or_err(py, descr.cast()), which checksif ptr.is_null(). While functional, relying on out-parameter nullness rather than checking the FFI function's explicit return code is error-prone.
3. Broad Scope of unsafe fn Bodies Masking Unannotated Unsafe Operations ๐ก โ ๏ธ
-
Priority: ๐ก Low
-
Threat Vector: โ ๏ธ Accidental Misuse
-
Bug Type: Missing Unsafe Demarcation
-
Across
src/array.rs,src/convert.rs, andsrc/npyffi/, many functions are declaredpub unsafe fnorunsafe fnand execute multiple distinct raw FFI calls, pointer offsets, and type casts directly within the top-level function scope. Under Rust 2024 semantics (#[deny(unsafe_op_in_unsafe_fn)]), unsafe operations insideunsafe fnrequire explicitunsafe {}blocks to clearly demarcate proof obligations from standard logic.
Missing Safety Comments
src/dtype.rs:55: Missing# Safetydoc comment onunsafe impl PyTypeInfo for PyArrayDescrjustifying why type descriptor representation satisfies PyO3 type info contracts. ๐ดsrc/dtype.rs:61: Missing// SAFETY:comment onPY_ARRAY_API.get_type_objectcall. ๐ดsrc/dtype.rs:87: Missing// SAFETY:comment onPyArray_DescrConverter2FFI invocation. ๐ดsrc/dtype.rs:116: Missing// SAFETY:comment onPyArray_DescrFromTypeFFI invocation. ๐ดsrc/dtype.rs:123: Missing// SAFETY:comment onPyArray_DescrNewFromTypeFFI invocation. ๐ดsrc/dtype.rs:162: Missing// SAFETY:comment on raw pointer dereference&*self.as_dtype_ptr(). ๐ดsrc/dtype.rs:187: Missing// SAFETY:comment on raw pointer dereference&*self.as_dtype_ptr(). ๐ดsrc/dtype.rs:198: Missing// SAFETY:comment on raw pointer dereference&*self.as_dtype_ptr(). ๐ดsrc/dtype.rs:209: Missing// SAFETY:comment on raw pointer dereference&*self.as_dtype_ptr(). ๐ดsrc/dtype.rs:326: Missing// SAFETY:comment onPyArray_EquivTypesFFI invocation. ๐ดsrc/dtype.rs:333: Missing// SAFETY:comment on&*self.as_dtype_ptr(). ๐ดsrc/dtype.rs:334: Missing// SAFETY:comment onPyType::from_borrowed_type_ptr. ๐ดsrc/dtype.rs:338: Missing// SAFETY:comment onPyDataType_ELSIZEcall. ๐ดsrc/dtype.rs:342: Missing// SAFETY:comment onPyDataType_ALIGNMENTcall. ๐ดsrc/dtype.rs:346: Missing// SAFETY:comment onPyDataType_FLAGScall. ๐ดsrc/dtype.rs:350,353,358,361,368,373,380,384,391,403,406: Missing// SAFETY:comments on internal FFI descriptor accesses and unchecked PyO3 casts. ๐ดsrc/dtype.rs:581(insideimpl_element_scalar!macro): Missing// SAFETY:proof comment justifying why primitive scalar types are trivially copyable (IS_COPY = true) and safely managed by NumPy buffers. ๐ดsrc/dtype.rs:611: Missing// SAFETY:comment onunsafe impl Element for bf16. ๐ดsrc/dtype.rs:636: Missing// SAFETY:comment onunsafe impl Element for Py<PyAny>justifying whyPyObject*pointers (IS_COPY = false) are soundly managed in NumPy object arrays. ๐ดsrc/array.rs:126: Missing# Safetydoc comment onunsafe impl PyTypeInfo for PyArray<T, D>. ๐ดsrc/array.rs:131: Missing// SAFETY:comment onget_type_objectinvocation. ๐ดsrc/array.rs:145: Missing// SAFETY:comment onPyArray_Checkandcast_unchecked. ๐ดsrc/array.rs:223: Missing# Safetydoc comment onunsafe fn new_uninitexplaining invariants onstridesandflag. ๐ดsrc/array.rs:248: Missing# Safetydoc comment onunsafe fn new_with_dataexplaining invariants onstrides,data_ptr, andcontainer. ๐ดsrc/array.rs:280: Missing# Safetydoc comment onunsafe fn from_raw_parts. ๐ดsrc/array.rs:378: Missing// SAFETY:comment onPyArray_Zerosinvocation. ๐ดsrc/array.rs:410: Missing// SAFETY:comment onSelf::from_raw_partscall. ๐ดsrc/array.rs:486: Missing// SAFETY:comment onSelf::from_raw_partscall. ๐ดsrc/array.rs:514: Missing// SAFETY:comment on uninitialized buffer writing viaclone_elements. ๐ดsrc/array.rs:690: Missing// SAFETY:comment onPyArray_Arangeinvocation. ๐ดsrc/array.rs:703: Missing# Safetydoc comment onunsafe fn clone_elementsexplaining capacity and alignment invariants ondata_ptr. ๐ดsrc/array.rs:1341: Missing// SAFETY:comment on raw pointer dereference*self.data(). ๐ดsrc/array.rs:1353: Missing// SAFETY:comment on pointer arithmeticslf.data().offset(offset). ๐ดsrc/array.rs:1384: Missing// SAFETY:comment on pointer arithmeticdata_ptr.offset(...). ๐ดsrc/array.rs:1474: Missing// SAFETY:comment onself.cast_unchecked()justifying transparent memory compatibility withPyUntypedArray. ๐ดsrc/array.rs:1479: Missing// SAFETY:comment on(*self.as_array_ptr()).data.cast(). ๐ดsrc/array.rs:1578,1588: Missing// SAFETY:comments onRawArrayView::from_shape_ptrcalls. ๐ดsrc/array.rs:1598: Missing// SAFETY:comment onself.as_array()call. ๐ดsrc/array.rs:1608: Missing// SAFETY:comment onPyArray_CopyIntoinvocation. ๐ดsrc/array.rs:1620,1628: Missing// SAFETY:comments onPyArray_CastToTypeandcast_into_unchecked. ๐ดsrc/array.rs:1639,1640: Missing// SAFETY:comments onPyArray_Transpose. ๐ดsrc/array.rs:1655,1663: Missing// SAFETY:comments onPyArray_Newshape. ๐ดsrc/untyped_array.rs:62: Missing# Safetydoc comment onunsafe impl PyTypeInfo for PyUntypedArray. ๐ดsrc/untyped_array.rs:67,71: Missing// SAFETY:comments on FFI type checking. ๐ดsrc/untyped_array.rs:135,161,171,176,200: Missing// SAFETY:comments on rawPyArrayObjectflag and dimension inspections. ๐ดsrc/untyped_array.rs:229,262: Missing// SAFETY:comments onslice::from_raw_partscalls for shape and strides. ๐ดsrc/untyped_array.rs:296: Missing// SAFETY:comment onBound::from_borrowed_ptr. ๐ดsrc/convert.rs:59,71: Missing// SAFETY:comments onPyArray::from_raw_partscalls. ๐ดsrc/convert.rs:163: Missing// SAFETY:comment oncopy_nonoverlappinginto uninitialized NumPy array buffer. ๐ดsrc/convert.rs:172: Missing// SAFETY:comment on raw pointer writingdata_ptr.write. ๐ดsrc/convert.rs:202: Missing// SAFETY:comment onnalgebramatrix conversion. ๐ดsrc/slice_container.rs:28,52: Missing# Safetydoc comments ondrop_boxed_sliceanddrop_vec. Missing// SAFETY:comments inside onBox::from_rawandVec::from_raw_parts. ๐ดsrc/slice_container.rs:87: Missing// SAFETY:comment on invoking type-erased destructor function pointer(self.drop)(...). ๐ดsrc/borrow/mod.rs:282: Missing// SAFETY:comment onself.array.get(index). ๐ดsrc/borrow/mod.rs:343: Missing// SAFETY:comment onself.array.try_as_matrix(). ๐ดsrc/borrow/shared.rs:42: Missing// SAFETY:comment onunsafe impl Send for Shared. ๐ดsrc/borrow/shared.rs:46,60,78,88: Missing# Safetydoc comments on extern "C" borrow checking API functions. ๐ดsrc/borrow/shared.rs:103,105: Missing// SAFETY:comments onSendandSyncforSharedPtr. ๐ดsrc/borrow/shared.rs:180,192,205,213: Missing// SAFETY:comments on dynamic C API function pointer invocations. ๐ดsrc/borrow/shared.rs:372,376,387,398,405,427,434: Missing// SAFETY:comments on raw base object traversing and slice constructions. ๐ดsrc/datetime.rs:156,193: Missing// SAFETY:comments onunsafe impl Element for DatetimeandTimedelta. ๐ดsrc/datetime.rs:160,197: Missing// SAFETY:comments onTypeDescriptors::new. ๐ดsrc/datetime.rs:218: Missing# Safetydoc comment onconst unsafe fn new. ๐กsrc/strings.rs:77,150: Missing// SAFETY:comments onunsafe impl Element for PyFixedStringandPyFixedUnicode. ๐ดsrc/strings.rs:83,156: Missing// SAFETY:comments onDTYPES.from_size. ๐ดsrc/strings.rs:175: Missing# Safetydoc comment onunsafe fn from_size. ๐กsrc/sum_products.rs:70,128,152: Missing// SAFETY:comments onPyArray_InnerProduct,PyArray_MatrixProduct, andPyArray_EinsteinSum. ๐ดsrc/npyffi/mod.rs:48: Missing// SAFETY:comment onPyArray_GetNDArrayCFeatureVersion. ๐ดsrc/npyffi/mod.rs:59,68,83: Missing# Safetydoc comments and internal// SAFETY:comments on dynamically loaded macro-generated FFI methods. ๐ดsrc/npyffi/array.rs:80,82: Missing// SAFETY:comments onSendandSyncforPyArrayAPI. ๐ดsrc/npyffi/array.rs:85: Missing# Safetydoc comment onunsafe fn get. ๐ดsrc/npyffi/array.rs:375,388,410,463,469: Missing# Safetydoc comments and internal// SAFETY:comments on FFI wrappers. ๐ดsrc/npyffi/objects.rs:97,103,121,133: Missing# Safetydoc comments and internal// SAFETY:comments on FFI accessor helpers. ๐ดsrc/npyffi/ufunc.rs:27,29: Missing// SAFETY:comments onSendandSyncforPyUFuncAPI. ๐ดsrc/npyffi/ufunc.rs:32: Missing# Safetydoc comment onunsafe fn get. ๐ด
- Dominant language
- Rust
- Stars
- 1.4k
- Forks
- 141
- Avg merge
- 16m
- Merged PRs (30d)
- 3
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up โ it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from PyO3/rust-numpy
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
PyO3/rust-numpy#565 ยท 1 comment ยท
-
Difficulty 3/5 1-2 days Newbie friendliness 58/100
PyO3/rust-numpy#563 ยท
-
Difficulty 3/5 1-2 days Newbie friendliness 45/100
PyO3/rust-numpy#547 ยท 2 comments ยท
-
Difficulty 2/5 1-3 hours Newbie friendliness 20/100
PyO3/rust-numpy#535 ยท
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
PyO3/rust-numpy#527 ยท 1 comment ยท
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ยท
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
gitbutlerapp/gitbutler#15998 ยท 1 comment ยท
-
bug triage:deciding
Difficulty 1/5 Under an hour Newbie friendliness 88/100
open-telemetry/otel-arrow#4132 ยท
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100