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 f74d9bf054 Return errors for undersized ArrayData validity buffers 
(#11044)
f74d9bf054 is described below

commit f74d9bf05409ba56ee43314489688f0ec53e7b80
Author: Yifan Chen <[email protected]>
AuthorDate: Thu Sep 17 21:02:40 2026 -0700

    Return errors for undersized ArrayData validity buffers (#11044)
    
    # Which issue does this PR close?
    
    Closes #7124.
    
    # Rationale for this change
    
    Validated IPC decoding uses `ArrayDataBuilder`, which constructs a
    `BooleanBuffer` before checking whether the validity buffer is large
    enough. Malformed input can therefore panic instead of returning an
    `ArrowError`. `ArrayData::try_new` already performs the required check,
    but its direct builder callers do not benefit from it.
    
    # What changes are included in this PR?
    
    Move the existing bounds check into the builder's validated path, before
    `BooleanBuffer` construction. Keep the error wording, checked
    length-plus-offset arithmetic, and unsafe skip-validation behavior.
    
    Builder tests cover a short validity buffer with and without an offset,
    two valid boundary controls, and length-plus-offset overflow. The public
    `StreamReader` regression uses the original #7124 reproduction,
    serializing a deliberately malformed array. Review follow-ups remove the
    hand-encoded IPC fixture and null-count/validity permutations, and place
    the stream regression in the existing reader unit tests. Production
    logic is unchanged.
    
    # Are these changes tested?
    
    The short-buffer, overflow and IPC regressions were verified to panic
    without the production fix and pass with it. After moving the unchanged
    stream regression into `arrow-ipc/src/reader.rs`, validation on Rust
    1.98.0 / macOS aarch64:
    
    - `cargo test --locked -p arrow-ipc --lib reader::tests --all-features`:
    53 passed, including the moved regression.
    - `cargo clippy --locked -p arrow-ipc --lib --tests --all-features -- -D
    warnings`, workspace formatting, touched-file Typos and diff whitespace
    checks pass.
    - The preceding test-simplification revision passed all 60 arrow-data
    library tests and 4 focused all-feature builder tests. Builder
    source/tests are unchanged by this move; those commands were not
    repeated.
    
    Explicitly forcing `arrow-data/force_validate` and
    `arrow-array/force_validate` into the IPC test rejects its deliberately
    invalid array during fixture construction, before decoding. That
    combination cannot run this original-issue reproduction. The earlier
    hand-encoded fixture did support it; the limitation is recorded rather
    than silently skipping the test.
    
    The broader workspace, release-mode and separate all-feature crate
    suites passed before this test-only simplification; those full suites
    were not rerun for the review edit. Shared-host benchmark measurements
    were inconclusive, so no performance conclusion is claimed.
    
    # Are there any user-facing changes?
    
    An undersized validity buffer now produces a recoverable error through
    the validated builder/IPC path instead of a panic. No public API
    signature changes.
    
    Implementation and regression tests generated with OpenAI Codex.
---
 arrow-data/src/data.rs  | 86 +++++++++++++++++++++++++++++++++++++++----------
 arrow-ipc/src/reader.rs | 24 ++++++++++++++
 2 files changed, 93 insertions(+), 17 deletions(-)

diff --git a/arrow-data/src/data.rs b/arrow-data/src/data.rs
index b911416506..e4e69ef2e4 100644
--- a/arrow-data/src/data.rs
+++ b/arrow-data/src/data.rs
@@ -333,21 +333,6 @@ impl ArrayData {
         buffers: Vec<Buffer>,
         child_data: Vec<ArrayData>,
     ) -> Result<Self, ArrowError> {
-        // we must check the length of `null_bit_buffer` first
-        // because we use this buffer to calculate `null_count`
-        // in `ArrayDataBuilder::build`.
-        if let Some(null_bit_buffer) = null_bit_buffer.as_ref() {
-            let len_plus_offset = checked_len_plus_offset(&data_type, len, 
offset)?;
-            let needed_len = bit_util::ceil(len_plus_offset, 8);
-            if null_bit_buffer.len() < needed_len {
-                return Err(ArrowError::InvalidArgumentError(format!(
-                    "null_bit_buffer size too small. got {} needed {}",
-                    null_bit_buffer.len(),
-                    needed_len
-                )));
-            }
-        }
-
         let builder = Self::inner_new_builder(
             data_type,
             len,
@@ -2331,6 +2316,21 @@ impl ArrayDataBuilder {
             skip_validation,
         } = self;
 
+        // SAFETY: `skip_validation` is only set to true using `unsafe` APIs.
+        let validate = !skip_validation.get() || cfg!(feature = 
"force_validate");
+        if validate && let Some(buffer) = null_bit_buffer.as_ref() {
+            // Check before constructing the BooleanBuffer, which would 
otherwise panic.
+            let len_plus_offset = checked_len_plus_offset(&data_type, len, 
offset)?;
+            let needed_len = bit_util::ceil(len_plus_offset, 8);
+            if buffer.len() < needed_len {
+                return Err(ArrowError::InvalidArgumentError(format!(
+                    "null_bit_buffer size too small. got {} needed {}",
+                    buffer.len(),
+                    needed_len
+                )));
+            }
+        }
+
         let nulls = nulls
             .or_else(|| {
                 let buffer = null_bit_buffer?;
@@ -2358,8 +2358,7 @@ impl ArrayDataBuilder {
             data.align_buffers();
         }
 
-        // SAFETY: `skip_validation` is only set to true using `unsafe` APIs
-        if !skip_validation.get() || cfg!(feature = "force_validate") {
+        if validate {
             data.validate_data()?;
         }
         Ok(data)
@@ -2954,6 +2953,59 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_builder_rejects_short_null_bit_buffer() {
+        for (len, offset) in [(8000, 0), (8, 1)] {
+            let err = ArrayData::builder(DataType::Int32)
+                .len(len)
+                .offset(offset)
+                .add_buffer(make_i32_buffer(len + offset))
+                .null_bit_buffer(Some(Buffer::from([0_u8])))
+                .build()
+                .unwrap_err();
+            assert_eq!(
+                err.to_string(),
+                format!(
+                    "Invalid argument error: null_bit_buffer size too small. 
got 1 needed {}",
+                    bit_util::ceil(len + offset, 8)
+                )
+            );
+        }
+    }
+
+    #[test]
+    fn test_builder_null_bit_buffer_length_overflow() {
+        let err = ArrayData::builder(DataType::Int32)
+            .len(usize::MAX)
+            .offset(1)
+            .null_bit_buffer(Some(Buffer::default()))
+            .build()
+            .unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            format!(
+                "Invalid argument error: Length {} with offset 1 overflows 
usize for Int32",
+                usize::MAX
+            )
+        );
+    }
+
+    #[test]
+    fn test_builder_accepts_valid_null_bit_buffer() {
+        for (len, offset) in [(8, 0), (7, 1)] {
+            let data = ArrayData::builder(DataType::Int32)
+                .len(len)
+                .offset(offset)
+                .add_buffer(make_i32_buffer(len + offset))
+                .null_bit_buffer(Some(Buffer::from([0_u8])))
+                .build()
+                .unwrap();
+            assert_eq!(data.len(), len);
+            assert_eq!(data.offset(), offset);
+            assert_eq!(data.null_count(), len);
+        }
+    }
+
     #[test]
     fn test_count_nulls() {
         let buffer = Buffer::from([0b00010110, 0b10011111]);
diff --git a/arrow-ipc/src/reader.rs b/arrow-ipc/src/reader.rs
index 52a026863b..c340649123 100644
--- a/arrow-ipc/src/reader.rs
+++ b/arrow-ipc/src/reader.rs
@@ -3586,6 +3586,30 @@ mod tests {
         assert_eq!(batch, roundtrip_batch);
     }
 
+    #[test]
+    fn test_stream_reader_rejects_short_validity_buffer() {
+        // Reproduce #7124: serialize an Int32Array with too few validity bits.
+        let data = ArrayDataBuilder::new(DataType::Int32)
+            .len(8000)
+            .add_buffer(ScalarBuffer::<i32>::from_iter(0..8000).into())
+            .nulls(Some(NullBuffer::from(&[true, false, true, false])));
+        let array: ArrayRef = unsafe { 
Arc::new(Int32Array::from(data.build_unchecked())) };
+        let batch = RecordBatch::try_from_iter([("a", array)]).unwrap();
+
+        let mut stream = Vec::new();
+        let mut writer =
+            crate::writer::StreamWriter::try_new(&mut stream, 
&batch.schema()).unwrap();
+        writer.write(&batch).unwrap();
+        writer.finish().unwrap();
+
+        let mut reader = StreamReader::try_new(Cursor::new(stream), 
None).unwrap();
+        let err = reader.next().unwrap().unwrap_err();
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: null_bit_buffer size too small. got 1 
needed 1000"
+        );
+    }
+
     #[test]
     fn test_invalid_struct_array_ipc_read_errors() {
         let a_field = Field::new("a", DataType::Int32, false);

Reply via email to