This is an automated email from the ASF dual-hosted git repository.

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 432fe12bf7 fix: stop `try_` functions from panicking (#10730)
432fe12bf7 is described below

commit 432fe12bf7a9b7e34ede8168b6dc30dc69c5d8e8
Author: Emil Ernerfeldt <[email protected]>
AuthorDate: Fri Aug 21 21:35:21 2026 -0700

    fix: stop `try_` functions from panicking (#10730)
    
    # Which issue does this PR close?
    
    * Part of https://github.com/apache/arrow-rs/issues/10553
    * Follow-up to https://github.com/apache/arrow-rs/pull/10656
    
    # Rationale for this change
    
    A `try_` function that returns a `Result` but still panics is the worst
    of both worlds: the caller writes error handling and gets a panic
    anyway.
    
    # What changes are included in this PR?
    
    * `FFI_ArrowSchema::try_new`: an interior nul byte in `format` was an
    `unwrap`, and the error path leaked the children.
    * `MutableArrayData::try_extend`: validates the array index and the
    `start..end` range, which used to underflow.
    * `MutableArrayData::try_extend_nulls`: errors instead of panicking when
    there is no null buffer.
    * `VariantArray::try_value`: errors on an out of bounds index, and on
    conflicting `value`/`typed_value`.
    
    `MutableBuffer::try_from_trusted_len_iter` still panics on a bad
    `size_hint`: that is part of its `unsafe` contract, and the docs now say
    so.
    
    # Are these changes tested?
    
    Yes, a test per error path.
    
    # Are there any user-facing changes?
    
    Panics turn into errors. `MutableArrayData::extend`/`extend_nulls` still
    panic as before.
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 arrow-arith/src/arity.rs                     |   6 +-
 arrow-buffer/src/buffer/mutable.rs           |   7 ++
 arrow-data/src/transform/mod.rs              | 116 ++++++++++++++++++++++-----
 arrow-schema/src/ffi.rs                      |  28 +++++--
 arrow/tests/array_transform.rs               |   9 ++-
 parquet-variant-compute/src/variant_array.rs |  51 +++++++++---
 6 files changed, 174 insertions(+), 43 deletions(-)

diff --git a/arrow-arith/src/arity.rs b/arrow-arith/src/arity.rs
index 42aa5b0889..b1256e0a70 100644
--- a/arrow-arith/src/arity.rs
+++ b/arrow-arith/src/arity.rs
@@ -247,10 +247,10 @@ where
 ///
 /// Like [`try_unary`] the function is only evaluated for non-null indices
 ///
-/// # Error
+/// # Errors
 ///
-/// Return an error if the arrays have different lengths or
-/// the operation is under erroneous
+/// Returns an error if the arrays have different lengths,
+/// or if the operation returns one.
 pub fn try_binary<A: ArrayAccessor, B: ArrayAccessor, F, O>(
     a: A,
     b: B,
diff --git a/arrow-buffer/src/buffer/mutable.rs 
b/arrow-buffer/src/buffer/mutable.rs
index f0b45304f8..1dcfaf67f4 100644
--- a/arrow-buffer/src/buffer/mutable.rs
+++ b/arrow-buffer/src/buffer/mutable.rs
@@ -1105,8 +1105,15 @@ impl MutableBuffer {
     /// if any of the items of the iterator is an error.
     /// Prefer this to `collect` whenever possible, as it is faster ~60% 
faster.
     ///
+    /// # Errors
+    ///
+    /// Returns the first error yielded by the iterator.
+    ///
     /// # Panics
     ///
+    /// Note that unlike the [`Err`] cases, these panics are violations of the 
safety contract
+    /// below, and are only checks that happen to be cheap enough to keep:
+    ///
     /// Panics if the iterator does not report an upper bound via `size_hint`, 
or if the
     /// reported length does not match the number of items produced before an 
error-free finish,
     /// or if allocating the required buffer fails for the same reasons as
diff --git a/arrow-data/src/transform/mod.rs b/arrow-data/src/transform/mod.rs
index 6571e0b658..7558d1e887 100644
--- a/arrow-data/src/transform/mod.rs
+++ b/arrow-data/src/transform/mod.rs
@@ -136,9 +136,8 @@ fn build_extend_null_bits(array: &ArrayData, use_nulls: 
bool) -> ExtendNullBits<
 pub struct MutableArrayData<'a> {
     /// Input arrays: the data being read FROM.
     ///
-    /// Note this is "dead code" because all actual references to the arrays 
are
-    /// stored in closures for extending values and nulls.
-    #[expect(dead_code)]
+    /// Note all actual reads of the arrays go through the closures for 
extending
+    /// values and nulls; these references are only kept for bounds checking.
     arrays: Vec<&'a ArrayData>,
 
     /// In progress output array: The data being written TO
@@ -768,13 +767,23 @@ impl<'a> MutableArrayData<'a> {
     /// * `end` - the end index of the chunk (exclusive)
     ///
     /// # Errors
-    /// Returns an error if offset arithmetic overflows the underlying integer 
type.
-    ///
-    /// # Panics
-    /// This function panics if there is an invalid index,
-    /// i.e. `index` >= the number of source arrays
-    /// or `end` > the length of the `index`th array
+    /// Returns an error if
+    /// * `index` >= the number of source arrays,
+    /// * `start..end` is not a valid range within the `index`th array, or
+    /// * offset arithmetic overflows the underlying integer type.
     pub fn try_extend(&mut self, index: usize, start: usize, end: usize) -> 
Result<(), ArrowError> {
+        let Some(array_len) = self.arrays.get(index).map(|array| array.len()) 
else {
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Source array index {index} is out of bounds: there are {} 
source arrays",
+                self.arrays.len()
+            )));
+        };
+        if end < start || array_len < end {
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Invalid range {start}..{end} for source array {index} of 
length {array_len}"
+            )));
+        }
+
         let len = end - start;
         (self.extend_null_bits[index])(&mut self.data, start, len);
         // Snapshot buffer lengths before attempting the extend so we can roll
@@ -795,17 +804,16 @@ impl<'a> MutableArrayData<'a> {
     /// Extends the in progress array with a region of the input arrays.
     ///
     /// # Panics
-    /// This function panics if there is an invalid index,
-    /// i.e. `index` >= the number of source arrays,
-    /// `end` > the length of the `index`th array,
-    /// or the offset type overflows (e.g. more than 2 GiB in a `StringArray`).
+    /// This function panics if
+    /// * `index` >= the number of source arrays,
+    /// * `start..end` is not a valid range within the `index`th array, or
+    /// * the offset type overflows (e.g. more than 2 GiB in a `StringArray`).
     #[deprecated(
         since = "59.0.0",
         note = "Use `try_extend` which returns an error on overflow instead of 
panicking"
     )]
     pub fn extend(&mut self, index: usize, start: usize, end: usize) {
-        self.try_extend(index, start, end)
-            .expect("extend failed due to offset overflow")
+        self.try_extend(index, start, end).expect("extend failed")
     }
 
     /// Extends the in progress array with null elements, ignoring the input 
arrays, returning an
@@ -814,10 +822,19 @@ impl<'a> MutableArrayData<'a> {
     /// Prefer this over [`extend_nulls`](Self::extend_nulls) to handle cases 
where the run-end
     /// counter overflows (relevant for `RunEndEncoded` arrays).
     ///
-    /// # Panics
+    /// # Errors
     ///
-    /// Panics if [`MutableArrayData`] not created with `use_nulls` or 
nullable source arrays
+    /// Returns an error if this [`MutableArrayData`] was not created with 
`use_nulls` and none
+    /// of the source arrays are nullable, or if the run-end counter overflows.
     pub fn try_extend_nulls(&mut self, len: usize) -> Result<(), ArrowError> {
+        if self.data.null_buffer.is_none() {
+            return Err(ArrowError::InvalidArgumentError(
+                "MutableArrayData cannot be extended with nulls: it was 
created with `use_nulls` \
+                 set to false and no source array is nullable"
+                    .to_owned(),
+            ));
+        }
+
         self.data.len += len;
         let bit_len = bit_util::ceil(self.data.len, 8);
         let nulls = self.data.null_buffer();
@@ -833,15 +850,14 @@ impl<'a> MutableArrayData<'a> {
     ///
     /// # Panics
     ///
-    /// Panics if [`MutableArrayData`] not created with `use_nulls` or 
nullable source arrays,
-    /// or if the run-end counter overflows for `RunEndEncoded` arrays.
+    /// Panics if this [`MutableArrayData`] was not created with `use_nulls` 
and none of the
+    /// source arrays are nullable, or if the run-end counter overflows.
     #[deprecated(
         since = "59.0.0",
         note = "Use `try_extend_nulls` which returns an error on overflow 
instead of panicking"
     )]
     pub fn extend_nulls(&mut self, len: usize) {
-        self.try_extend_nulls(len)
-            .expect("extend_nulls failed due to overflow")
+        self.try_extend_nulls(len).expect("extend_nulls failed")
     }
 
     /// Returns the current length
@@ -937,6 +953,64 @@ mod test {
     use arrow_schema::Field;
     use std::sync::Arc;
 
+    fn int64_array_data(values: Vec<i64>) -> ArrayData {
+        let len = values.len();
+        ArrayData::try_new(
+            DataType::Int64,
+            len,
+            None,
+            0,
+            vec![arrow_buffer::Buffer::from_slice_ref(&values)],
+            vec![],
+        )
+        .unwrap()
+    }
+
+    #[test]
+    fn test_try_extend_invalid_index_and_range() {
+        let array = int64_array_data(vec![1, 2, 3]);
+        let mut mutable = MutableArrayData::new(vec![&array], false, 3);
+
+        let err = mutable.try_extend(1, 0, 1).unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: Source array index 1 is out of bounds: 
there are 1 source arrays"
+        );
+
+        let err = mutable.try_extend(0, 0, 4).unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: Invalid range 0..4 for source array 0 of 
length 3"
+        );
+
+        // `end < start` used to underflow:
+        let err = mutable.try_extend(0, 2, 1).unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: Invalid range 2..1 for source array 0 of 
length 3"
+        );
+
+        // The bounds are inclusive of the full array:
+        mutable.try_extend(0, 3, 3).unwrap();
+        mutable.try_extend(0, 0, 3).unwrap();
+        assert_eq!(mutable.len(), 3);
+    }
+
+    #[test]
+    fn test_try_extend_nulls_without_null_buffer() {
+        let array = int64_array_data(vec![1, 2, 3]);
+        let mut mutable = MutableArrayData::new(vec![&array], false, 3);
+        let err = mutable.try_extend_nulls(1).unwrap_err();
+        assert!(
+            err.to_string().contains("cannot be extended with nulls"),
+            "unexpected error: {err}"
+        );
+
+        let mut mutable = MutableArrayData::new(vec![&array], true, 3);
+        mutable.try_extend_nulls(1).unwrap();
+        assert_eq!(mutable.len(), 1);
+    }
+
     #[test]
     fn test_list_append_with_capacities() {
         let array = ArrayData::new_empty(&DataType::List(Arc::new(Field::new(
diff --git a/arrow-schema/src/ffi.rs b/arrow-schema/src/ffi.rs
index 1885549cc2..3e1fe38082 100644
--- a/arrow-schema/src/ffi.rs
+++ b/arrow-schema/src/ffi.rs
@@ -132,17 +132,26 @@ unsafe extern "C" fn release_schema(schema: *mut 
FFI_ArrowSchema) {
 }
 
 impl FFI_ArrowSchema {
-    /// create a new [`FFI_ArrowSchema`]. This fails if the fields'
-    /// [`DataType`] is not supported.
+    /// create a new [`FFI_ArrowSchema`].
     ///
-    /// # Panics
+    /// # Errors
     ///
-    /// Panics if `format` contains an interior nul byte
+    /// Errors if the fields' [`DataType`] is not supported,
+    /// or if `format` contains an interior nul byte.
     pub fn try_new(
         format: &str,
         children: Vec<FFI_ArrowSchema>,
         dictionary: Option<FFI_ArrowSchema>,
     ) -> Result<Self, ArrowError> {
+        // Convert the format before leaking any of the children,
+        // so that an error here does not leak memory.
+        let format = CString::new(format).map_err(|err| {
+            ArrowError::CDataInterface(format!(
+                "Null byte at position {} not allowed in format",
+                err.nul_position()
+            ))
+        })?;
+
         let mut this = Self::empty();
 
         let children_ptr = children
@@ -151,7 +160,7 @@ impl FFI_ArrowSchema {
             .map(Box::into_raw)
             .collect::<Box<_>>();
 
-        this.format = CString::new(format).unwrap().into_raw();
+        this.format = format.into_raw();
         this.release = Some(release_schema);
         this.n_children = children_ptr.len() as i64;
 
@@ -932,6 +941,15 @@ mod tests {
         assert_eq!(restored, schema);
     }
 
+    #[test]
+    fn test_try_new_with_interior_nul_byte() {
+        let err = FFI_ArrowSchema::try_new("i\0nt", vec![], None).unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "C Data interface error: Null byte at position 1 not allowed in 
format"
+        );
+    }
+
     #[test]
     fn test_type() {
         round_trip_type(DataType::Int64);
diff --git a/arrow/tests/array_transform.rs b/arrow/tests/array_transform.rs
index 23a5d85391..1f82ca4927 100644
--- a/arrow/tests/array_transform.rs
+++ b/arrow/tests/array_transform.rs
@@ -1029,11 +1029,14 @@ fn test_extend_nulls() {
 }
 
 #[test]
-#[should_panic(expected = "MutableArrayData not nullable")]
-fn test_extend_nulls_panic() {
+fn test_extend_nulls_not_nullable() {
     let int = Int32Array::from(vec![1, 2, 3, 4]).into_data();
     let mut mutable = MutableArrayData::new(vec![&int], false, 4);
-    mutable.try_extend_nulls(2).unwrap();
+    let err = mutable.try_extend_nulls(2).unwrap_err();
+    assert!(
+        err.to_string().contains("cannot be extended with nulls"),
+        "unexpected error: {err}"
+    );
 }
 
 #[test]
diff --git a/parquet-variant-compute/src/variant_array.rs 
b/parquet-variant-compute/src/variant_array.rs
index c6de730481..b8a0894719 100644
--- a/parquet-variant-compute/src/variant_array.rs
+++ b/parquet-variant-compute/src/variant_array.rs
@@ -409,11 +409,13 @@ impl VariantArray {
     /// Use `try_value` if you need to handle conversion errors gracefully.
     ///
     /// # Panics
-    /// * if the index is out of bounds
-    /// * if the array value is null
-    /// * if `try_value` returns an error.
+    /// Panics if
+    /// * the index is out of bounds,
+    /// * the `metadata`/`value` bytes of the row are invalid, which includes 
reading a null row, or
+    /// * both `value` and `typed_value` are non-null for a non-struct 
`typed_value`.
     pub fn value(&self, index: usize) -> Variant<'_, '_> {
-        self.try_value(index).unwrap()
+        self.try_value(index)
+            .unwrap_or_else(|err| panic!("VariantArray::value({index}) failed: 
{err}"))
     }
 
     /// Return the [`Variant`] instance stored at the given row
@@ -421,16 +423,17 @@ impl VariantArray {
     /// Note: This method does not check for nulls and the value is arbitrary
     /// (but still well-defined) if [`is_null`](Self::is_null) returns true 
for the index.
     ///
-    /// # Panics
-    ///
-    /// Panics if
-    /// * the index is out of bounds
-    /// * the array value is null
-    ///
     /// # Errors
     ///
     /// Errors if
+    /// - the index is out of bounds
     /// - the data in `typed_value` cannot be interpreted as a valid `Variant`
+    /// - both `value` and `typed_value` are non-null for a non-struct 
`typed_value`
+    ///
+    /// # Panics
+    ///
+    /// Panics if the unshredded `metadata`/`value` bytes fail basic 
validation, since those are
+    /// read with [`Variant::new`]. This includes reading a row that is null.
     ///
     /// If this is a shredded variant but has no value at the shredded 
location, it
     /// will return [`Variant::Null`].
@@ -444,13 +447,22 @@ impl VariantArray {
     /// Note: Does not do deep validation of the [`Variant`], so it is up to 
the
     /// caller to ensure that the metadata and value were constructed 
correctly.
     pub fn try_value(&self, index: usize) -> Result<Variant<'_, '_>> {
+        if self.len() <= index {
+            return Err(ArrowError::InvalidArgumentError(format!(
+                "Index {index} out of bounds for VariantArray of length {}",
+                self.len()
+            )));
+        }
+
         let value = self.value_column();
         match self.typed_value_column() {
             // Always prefer typed_value, if available
             Some(typed_value) if typed_value.is_valid(index) => {
                 if !matches!(typed_value.data_type(), DataType::Struct(_)) && 
value.is_valid(index) {
                     // Only a partially shredded struct is allowed to have 
values for both columns
-                    panic!("Invalid variant, conflicting value and 
typed_value");
+                    return Err(ArrowError::InvalidArgumentError(
+                        "Invalid variant, conflicting value and 
typed_value".to_owned(),
+                    ));
                 }
                 typed_value_to_variant(typed_value, index)
             }
@@ -1595,6 +1607,23 @@ mod test {
         }
     }
 
+    #[test]
+    fn test_try_value_out_of_bounds() {
+        let mut b = VariantArrayBuilder::new(2);
+        b.append_variant(Variant::from(1_i8));
+        b.append_variant(Variant::Null);
+        let v = b.build();
+
+        assert_eq!(v.try_value(0).unwrap(), Variant::Int8(1));
+        assert_eq!(v.try_value(1).unwrap(), Variant::Null);
+
+        let err = v.try_value(2).unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: Index 2 out of bounds for VariantArray of 
length 2"
+        );
+    }
+
     #[test]
     fn test_variant_array_iterable() {
         let mut b = VariantArrayBuilder::new(6);

Reply via email to