This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch alamb/coalesce-copy-rows-safe
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/alamb/coalesce-copy-rows-safe
by this push:
new cd3e3cf602 Mark filter-based append helpers unsafe and check filter
length in callers
cd3e3cf602 is described below
commit cd3e3cf60229cb593a154ddf838f83b2c44437bc
Author: Andrew Lamb <[email protected]>
AuthorDate: Wed Oct 21 15:46:01 2026 -0400
Mark filter-based append helpers unsafe and check filter length in callers
append_values_by_indices, append_values_by_slices and
append_views_by_filter write through raw pointers using indices taken from a
FilterPredicate, relying on the predicate matching the source array. Mark them
unsafe fn with documented preconditions, and have the safe copy_rows_by_filter
callers verify filter_len <= source len before calling them, returning an error
otherwise.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---
arrow-select/src/coalesce.rs | 16 ++++++
arrow-select/src/coalesce/byte_view.rs | 53 ++++++++++++++++++--
arrow-select/src/coalesce/primitive.rs | 92 +++++++++++++++++++++++++++-------
3 files changed, 140 insertions(+), 21 deletions(-)
diff --git a/arrow-select/src/coalesce.rs b/arrow-select/src/coalesce.rs
index 6f5378a8f5..3687243287 100644
--- a/arrow-select/src/coalesce.rs
+++ b/arrow-select/src/coalesce.rs
@@ -790,6 +790,22 @@ fn create_in_progress_array(data_type: &DataType,
batch_size: usize) -> Box<dyn
}
}
+/// Returns an error if `filter` selects rows beyond `source_len`.
+///
+/// Specialized [`InProgressArray`] implementations rely on this check before
+/// copying rows selected by a [`FilterPredicate`] with unchecked indexing:
+/// every index the predicate yields is less than its filter length, so once
+/// `filter.filter_len() <= source_len` all selected rows are in bounds.
+fn check_filter_len(filter: &FilterPredicate, source_len: usize) -> Result<(),
ArrowError> {
+ let filter_len = filter.filter_len();
+ if filter_len > source_len {
+ return Err(ArrowError::InvalidArgumentError(format!(
+ "Filter predicate of length {filter_len} is larger than source
array of length {source_len}"
+ )));
+ }
+ Ok(())
+}
+
/// Incrementally builds up arrays
///
/// [`GenericInProgressArray`] is the default implementation that buffers
diff --git a/arrow-select/src/coalesce/byte_view.rs
b/arrow-select/src/coalesce/byte_view.rs
index 74c10b3fd7..d86d6810e0 100644
--- a/arrow-select/src/coalesce/byte_view.rs
+++ b/arrow-select/src/coalesce/byte_view.rs
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-use crate::coalesce::InProgressArray;
+use crate::coalesce::{InProgressArray, check_filter_len};
use crate::filter::{FilterPredicate, FilterSelection};
use arrow_array::cast::AsArray;
use arrow_array::types::ByteViewType;
@@ -121,13 +121,22 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
self.completed.push(buffer);
}
- fn append_views_by_filter(&mut self, views: &[u128], filter:
&FilterPredicate) {
+ /// Append the `views` selected by `filter` to `self.views`.
+ ///
+ /// # Safety
+ ///
+ /// Every row selected by `filter` must be less than `views.len()`, i.e.
+ /// `filter.filter_len() <= views.len()`.
+ unsafe fn append_views_by_filter(&mut self, views: &[u128], filter:
&FilterPredicate) {
let selected_count = filter.count();
let current_len = self.views.len();
self.views.reserve(selected_count);
let mut written = 0;
+ // SAFETY: the caller guarantees all selected rows are in bounds,
+ // `FilterPredicate` guarantees exactly `selected_count` rows are
+ // selected, and capacity was reserved above.
unsafe {
let mut out =
self.views.spare_capacity_mut().as_mut_ptr().cast::<u128>();
@@ -448,8 +457,14 @@ impl<B: ByteViewType> InProgressArray for
InProgressByteViewArray<B> {
));
}
+ if let Err(e) = check_filter_len(filter, s.len()) {
+ self.source = Some(source);
+ return Err(e);
+ }
+
self.append_nulls_by_filter(filter, s.nulls());
- self.append_views_by_filter(s.views(), filter);
+ // SAFETY: `check_filter_len` verified all selected rows are in bounds
+ unsafe { self.append_views_by_filter(s.views(), filter) };
self.source = Some(source);
Ok(())
@@ -464,9 +479,11 @@ impl<B: ByteViewType> InProgressArray for
InProgressByteViewArray<B> {
// The source views reference no external buffers, so they must all be
// inline and we can copy just the nulls and views.
if s.data_buffers().is_empty() {
+ check_filter_len(filter, s.len())?;
self.ensure_capacity();
self.append_nulls_by_filter(filter, s.nulls());
- self.append_views_by_filter(s.views(), filter);
+ // SAFETY: `check_filter_len` verified all selected rows are in
bounds
+ unsafe { self.append_views_by_filter(s.views(), filter) };
return Ok(());
}
@@ -736,4 +753,32 @@ mod tests {
assert_eq!(in_progress_size_with_split,
in_progress_size_without_split);
}
+
+ #[test]
+ fn test_copy_rows_by_filter_rejects_filter_longer_than_source() {
+ let values: Vec<Option<&[u8]>> = vec![Some(b"a"), Some(b"b"),
Some(b"c")];
+ let array = BinaryViewArray::from_iter(values);
+ assert!(array.data_buffers().is_empty());
+ let array: ArrayRef = Arc::new(array);
+
+ let filter = BooleanArray::from_iter((0..8).map(|idx| Some(idx != 3)));
+ let predicate = FilterBuilder::new(&filter).build();
+
+ let mut in_progress =
InProgressByteViewArray::<BinaryViewType>::new(8);
+ in_progress.set_source(Some(Arc::clone(&array)));
+ let err = in_progress.copy_rows_by_filter(&predicate).unwrap_err();
+ assert!(
+ err.to_string().contains("larger than source array"),
+ "unexpected error: {err}"
+ );
+
+ let mut in_progress =
InProgressByteViewArray::<BinaryViewType>::new(8);
+ let err = in_progress
+ .copy_rows_by_filter_from(array, &predicate)
+ .unwrap_err();
+ assert!(
+ err.to_string().contains("larger than source array"),
+ "unexpected error: {err}"
+ );
+ }
}
diff --git a/arrow-select/src/coalesce/primitive.rs
b/arrow-select/src/coalesce/primitive.rs
index b547409c75..159bc084a9 100644
--- a/arrow-select/src/coalesce/primitive.rs
+++ b/arrow-select/src/coalesce/primitive.rs
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-use crate::coalesce::InProgressArray;
+use crate::coalesce::{InProgressArray, check_filter_len};
use crate::filter::{FilterIndices, FilterPredicate, FilterSelection,
FilterSlices};
use arrow_array::cast::AsArray;
use arrow_array::{Array, ArrayRef, ArrowPrimitiveType, PrimitiveArray};
@@ -61,7 +61,15 @@ impl<T: ArrowPrimitiveType> InProgressPrimitiveArray<T> {
}
}
- fn append_values_by_indices(
+ /// Append the `values` at `indices` to `current`.
+ ///
+ /// # Safety
+ ///
+ /// * Every index yielded by `indices` must be less than `values.len()`
+ /// * `indices` must yield exactly `selected_count` indices
+ /// * `current` must have spare capacity for at least `selected_count`
+ /// additional elements
+ unsafe fn append_values_by_indices(
current: &mut Vec<T::Native>,
values: &[T::Native],
indices: FilterIndices<'_>,
@@ -70,6 +78,8 @@ impl<T: ArrowPrimitiveType> InProgressPrimitiveArray<T> {
let current_len = current.len();
let mut written = 0;
+ // SAFETY: the caller guarantees all indices are in bounds, and that
+ // `current` has capacity for `selected_count` more elements.
unsafe {
let mut out = current
.spare_capacity_mut()
@@ -77,7 +87,6 @@ impl<T: ArrowPrimitiveType> InProgressPrimitiveArray<T> {
.cast::<T::Native>();
indices.for_each(|idx| {
- // SAFETY: indices are derived from the filter predicate for
this source.
out.write(*values.get_unchecked(idx));
out = out.add(1);
written += 1;
@@ -89,7 +98,16 @@ impl<T: ArrowPrimitiveType> InProgressPrimitiveArray<T> {
debug_assert_eq!(written, selected_count);
}
- fn append_values_by_slices(
+ /// Append the `values` in each `start..end` range of `slices` to
`current`.
+ ///
+ /// # Safety
+ ///
+ /// * Every `(start, end)` yielded by `slices` must satisfy
+ /// `start <= end <= values.len()`
+ /// * `slices` must cover exactly `selected_count` elements in total
+ /// * `current` must have spare capacity for at least `selected_count`
+ /// additional elements
+ unsafe fn append_values_by_slices(
current: &mut Vec<T::Native>,
values: &[T::Native],
slices: FilterSlices<'_>,
@@ -98,6 +116,8 @@ impl<T: ArrowPrimitiveType> InProgressPrimitiveArray<T> {
let current_len = current.len();
let mut written = 0;
+ // SAFETY: the caller guarantees all slices are in bounds, and that
+ // `current` has capacity for `selected_count` more elements.
unsafe {
let mut out = current
.spare_capacity_mut()
@@ -106,7 +126,6 @@ impl<T: ArrowPrimitiveType> InProgressPrimitiveArray<T> {
slices.for_each(|(start, end)| {
let len = end - start;
- // SAFETY: slices are derived from the filter predicate for
this source.
std::ptr::copy_nonoverlapping(values.as_ptr().add(start), out,
len);
out = out.add(len);
written += len;
@@ -175,29 +194,41 @@ impl<T: ArrowPrimitiveType + Debug> InProgressArray for
InProgressPrimitiveArray
FilterSelection::Indices(indices) => {
self.ensure_capacity();
let s = primitive_source::<T>(self.source.as_ref())?;
+ check_filter_len(filter, s.len())?;
append_filtered_nulls(&mut self.nulls, s.nulls(), filter);
self.current.reserve(filter.count());
- Self::append_values_by_indices(
- &mut self.current,
- s.values(),
- indices,
- filter.count(),
- );
+ // SAFETY: `check_filter_len` verified all selected indices are
+ // in bounds, `FilterPredicate` guarantees `filter.count()`
+ // rows are selected, and capacity was reserved above.
+ unsafe {
+ Self::append_values_by_indices(
+ &mut self.current,
+ s.values(),
+ indices,
+ filter.count(),
+ )
+ };
Ok(())
}
FilterSelection::Slices(slices) => {
self.ensure_capacity();
let s = primitive_source::<T>(self.source.as_ref())?;
+ check_filter_len(filter, s.len())?;
append_filtered_nulls(&mut self.nulls, s.nulls(), filter);
self.current.reserve(filter.count());
- Self::append_values_by_slices(
- &mut self.current,
- s.values(),
- slices,
- filter.count(),
- );
+ // SAFETY: `check_filter_len` verified all selected slices are
+ // in bounds, `FilterPredicate` guarantees `filter.count()`
+ // rows are selected, and capacity was reserved above.
+ unsafe {
+ Self::append_values_by_slices(
+ &mut self.current,
+ s.values(),
+ slices,
+ filter.count(),
+ )
+ };
Ok(())
}
// Other selection shapes reuse the generic copy_rows path.
@@ -372,4 +403,31 @@ mod tests {
"null buffer must be included in size(): with_nulls={with_nulls}
all_valid={all_valid}"
);
}
+
+ #[test]
+ fn test_copy_rows_by_filter_rejects_filter_longer_than_source() {
+ let source = Int32Array::from(vec![1, 2, 3]);
+ let mut in_progress = InProgressPrimitiveArray::<Int32Type>::new(8,
DataType::Int32);
+ in_progress.set_source(Some(Arc::new(source)));
+
+ // Index iterator: a sparse filter longer than the source
+ let filter = BooleanArray::from_iter((0..21).map(|idx| Some(idx ==
20)));
+ let predicate = FilterBuilder::new(&filter).build();
+ assert!(matches!(predicate.selection(), FilterSelection::Indices(_)));
+ let err = in_progress.copy_rows_by_filter(&predicate).unwrap_err();
+ assert!(
+ err.to_string().contains("larger than source array"),
+ "unexpected error: {err}"
+ );
+
+ // Slice iterator: a dense filter longer than the source
+ let filter = BooleanArray::from_iter((0..8).map(|idx| Some(idx != 3)));
+ let predicate = FilterBuilder::new(&filter).build();
+ assert!(matches!(predicate.selection(), FilterSelection::Slices(_)));
+ let err = in_progress.copy_rows_by_filter(&predicate).unwrap_err();
+ assert!(
+ err.to_string().contains("larger than source array"),
+ "unexpected error: {err}"
+ );
+ }
}