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

sunchao 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 4a0621d2a7 fix: validate union child bounds without truncating lengths 
(#10847)
4a0621d2a7 is described below

commit 4a0621d2a770ba893479b63354d2b503081bec73
Author: Chao Sun <[email protected]>
AuthorDate: Tue Sep 8 13:12:09 2026 -0700

    fix: validate union child bounds without truncating lengths (#10847)
    
    ## Why are the changes needed?
    
    ### Which issue does this PR close?
    
    Closes https://github.com/apache/arrow-rs/issues/10845.
    
    ### Rationale for this change
    
    A dense Union can have a child longer than `i32::MAX` while referencing
    only valid `i32` offsets. `UnionArray::try_new` truncates child lengths
    to `i32`, so the length can collide with its missing-type sentinel or
    fail the bounds check. A `NullArray` reproduces this without a large
    allocation.
    
    ## What changes were proposed in this PR?
    
    ### What changes are included in this PR?
    
    Keep child lengths as `Option<usize>`, separating a missing type ID from
    a valid length. Reject negative offsets before converting them to
    `usize` for comparison. Add coverage for lengths `i32::MAX + 1` and
    `i32::MAX + 2`, offsets zero and `i32::MAX`, and invalid negative
    offsets.
    
    ### Are there any user-facing changes?
    
    Previously rejected valid Unions are accepted. Invalid type IDs and
    out-of-bounds offsets still return errors. No public API changes.
    
    ## How was this PR tested?
    
    ### Are these changes tested?
    
    - The large-child test failed on the unchanged base with the type-ID
    error, then passed with the fix; both constructed arrays pass full Arrow
    validation.
    - `cargo test --offline -p arrow-array --lib`: 719 passed, 1 ignored.
    - `cargo clippy --offline -p arrow-array --all-targets --all-features --
    -D warnings`: passed.
    - `cargo fmt --all -- --check`: passed.
    - Independent source review checked missing IDs, empty children, signed
    offsets, and the safety of the lookup after type-ID validation.
    
    AI assistance: Codex generated the implementation, regression tests, and
    PR text, and performed the stated local checks and source review. This
    does not claim a separate human review.
---
 arrow-array/src/array/union_array.rs | 43 +++++++++++++++++++++++++++++++-----
 1 file changed, 37 insertions(+), 6 deletions(-)

diff --git a/arrow-array/src/array/union_array.rs 
b/arrow-array/src/array/union_array.rs
index 1a4d2c6654..17b740c29c 100644
--- a/arrow-array/src/array/union_array.rs
+++ b/arrow-array/src/array/union_array.rs
@@ -207,15 +207,15 @@ impl UnionArray {
 
         // Create mapping from type id to array lengths.
         let max_id = fields.iter().map(|(i, _)| i).max().unwrap_or_default() 
as usize;
-        let mut array_lens = vec![i32::MIN; max_id + 1];
+        let mut array_lens = vec![None; max_id + 1];
         for (cd, (field_id, _)) in children.iter().zip(fields.iter()) {
-            array_lens[field_id as usize] = cd.len() as i32;
+            array_lens[field_id as usize] = Some(cd.len());
         }
 
         // Type id values must match one of the fields.
         for id in &type_ids {
             match array_lens.get(*id as usize) {
-                Some(x) if *x != i32::MIN => {}
+                Some(Some(_)) => {}
                 _ => {
                     return Err(ArrowError::InvalidArgumentError(
                         "Type Ids values must match one of the field type 
ids".to_owned(),
@@ -227,8 +227,9 @@ impl UnionArray {
         // Check the value offsets are in bounds.
         if let Some(offsets) = &offsets {
             let mut iter = type_ids.iter().zip(offsets.iter());
-            if iter.any(|(type_id, &offset)| offset < 0 || offset >= 
array_lens[*type_id as usize])
-            {
+            if iter.any(|(type_id, &offset)| {
+                offset < 0 || offset as usize >= array_lens[*type_id as 
usize].unwrap()
+            }) {
                 return Err(ArrowError::InvalidArgumentError(
                     "Offsets must be non-negative and within the length of the 
Array".to_owned(),
                 ));
@@ -1042,7 +1043,7 @@ mod tests {
     use crate::builder::UnionBuilder;
     use crate::cast::AsArray;
     use crate::types::{Float32Type, Float64Type, Int32Type, Int64Type};
-    use crate::{Float64Array, Int32Array, Int64Array, StringArray};
+    use crate::{Float64Array, Int32Array, Int64Array, NullArray, StringArray};
     use crate::{Int8Array, RecordBatch};
     use arrow_buffer::Buffer;
     use arrow_schema::{Field, Schema};
@@ -1844,6 +1845,27 @@ mod tests {
         assert_eq!(array.len(), 7);
     }
 
+    #[test]
+    fn test_dense_union_large_child() {
+        let fields =
+            UnionFields::try_new([3], [Field::new("nulls", DataType::Null, 
true)]).unwrap();
+
+        // NullArray represents these lengths without allocating a values 
buffer.
+        for child_len in [i32::MAX as usize + 1, i32::MAX as usize + 2] {
+            let array = UnionArray::try_new(
+                fields.clone(),
+                vec![3, 3].into(),
+                Some(vec![0, i32::MAX].into()),
+                vec![Arc::new(NullArray::new(child_len))],
+            )
+            .unwrap();
+
+            assert_eq!(array.child(3).len(), child_len);
+            assert_eq!(array.value(1).len(), 1);
+            array.to_data().validate_full().unwrap();
+        }
+    }
+
     #[test]
     fn test_invalid() {
         let fields = UnionFields::try_new(
@@ -1899,6 +1921,15 @@ mod tests {
             "Invalid argument error: Offsets must be non-negative and within 
the length of the Array"
         );
 
+        let offsets = Some(vec![0, -1, 0].into());
+        let err = UnionArray::try_new(fields.clone(), type_ids.clone(), 
offsets, children.clone())
+            .unwrap_err();
+
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: Offsets must be non-negative and within 
the length of the Array"
+        );
+
         let offsets = Some(vec![0, 1].into());
         let err =
             UnionArray::try_new(fields.clone(), type_ids.clone(), offsets, 
children).unwrap_err();

Reply via email to