This is an automated email from the ASF dual-hosted git repository.
lidavidm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new f088fc728 fix(rust/ffi)!: importing partitions is unsafe (#4876)
f088fc728 is described below
commit f088fc72823be58986dc5c0d24c21b8bd15f039d
Author: David Li <[email protected]>
AuthorDate: Sun Oct 11 15:58:56 2026 -0700
fix(rust/ffi)!: importing partitions is unsafe (#4876)
We can't validate that the pointers and such are valid, so we shouldn't
make the conversion safe - instead it must be unsafe.
Assisted-by: GPT-6.1 Sol
---
rust/driver_manager/src/lib.rs | 4 +-
rust/ffi/src/lib.rs | 2 +-
rust/ffi/src/types.rs | 134 ++++++++++++++++++++++++++++++++++++-----
3 files changed, 122 insertions(+), 18 deletions(-)
diff --git a/rust/driver_manager/src/lib.rs b/rust/driver_manager/src/lib.rs
index f42bcdc58..ea7c071b7 100644
--- a/rust/driver_manager/src/lib.rs
+++ b/rust/driver_manager/src/lib.rs
@@ -1480,7 +1480,9 @@ impl Statement for ManagedStatement {
check_status(status, error)?;
let result = PartitionedResult {
- partitions: partitions.into(),
+ // SAFETY: The driver must return valid partition allocations and
a valid
+ // release callback according to the ADBC FFI contract.
+ partitions: unsafe { adbc_ffi::import_partitions(partitions) }?,
schema: (&schema).try_into()?,
rows_affected,
};
diff --git a/rust/ffi/src/lib.rs b/rust/ffi/src/lib.rs
index c171bc522..c6fcc7d22 100644
--- a/rust/ffi/src/lib.rs
+++ b/rust/ffi/src/lib.rs
@@ -48,5 +48,5 @@ pub mod methods;
pub(crate) mod types;
pub use types::{
FFI_AdbcConnection, FFI_AdbcDatabase, FFI_AdbcDriver,
FFI_AdbcDriverInitFunc, FFI_AdbcError,
- FFI_AdbcErrorDetail, FFI_AdbcPartitions, FFI_AdbcStatement, export_error,
+ FFI_AdbcErrorDetail, FFI_AdbcPartitions, FFI_AdbcStatement, export_error,
import_partitions,
};
diff --git a/rust/ffi/src/types.rs b/rust/ffi/src/types.rs
index 87bcbaec3..264ee7ba1 100644
--- a/rust/ffi/src/types.rs
+++ b/rust/ffi/src/types.rs
@@ -212,19 +212,49 @@ macro_rules! driver_method {
};
}
-impl From<FFI_AdbcPartitions> for Partitions {
- fn from(value: FFI_AdbcPartitions) -> Self {
- let mut partitions = Vec::with_capacity(value.num_partitions);
- for p in 0..value.num_partitions {
- let partition = unsafe {
- let ptr = *value.partitions.add(p);
- let len = *value.partition_lengths.add(p);
- std::slice::from_raw_parts(ptr, len)
- };
- partitions.push(partition.to_vec());
+/// Copy FFI partitions into owned Rust partitions, releasing the FFI value.
+///
+/// Null arrays are accepted only when there are no partitions. Null partition
+/// data pointers are accepted only for zero-length partitions.
+///
+/// # Safety
+///
+/// Non-null arrays must be aligned and contain `num_partitions` initialized
+/// entries. Each nonempty, non-null partition must point to an initialized,
+/// readable allocation of at least its corresponding length. Each array or
+/// partition must fit within a single allocation, with a byte size no greater
+/// than `isize::MAX`. This memory must remain valid and must not be mutated
+/// during the import. If present, the release callback must be safe to call
+/// with this value, including when import returns an error.
+pub unsafe fn import_partitions(value: FFI_AdbcPartitions) ->
Result<Partitions, Error> {
+ if value.num_partitions == 0 {
+ return Ok(Vec::new());
+ } else if value.partitions.is_null() || value.partition_lengths.is_null() {
+ return Err(Error::with_message_and_status(
+ "partition arrays must not be null when num_partitions is nonzero",
+ Status::Internal,
+ ));
+ }
+
+ let mut partitions = Vec::with_capacity(value.num_partitions);
+ for p in 0..value.num_partitions {
+ // SAFETY: The caller guarantees readable, initialized metadata arrays.
+ let (ptr, len) = unsafe { (*value.partitions.add(p),
*value.partition_lengths.add(p)) };
+ if len == 0 {
+ partitions.push(Vec::new());
+ continue;
}
- partitions
+ if ptr.is_null() {
+ return Err(Error::with_message_and_status(
+ format!("partition {p} data pointer must not be null when its
length is nonzero"),
+ Status::Internal,
+ ));
+ }
+ // SAFETY: The caller guarantees the non-null partition is readable
for len bytes.
+ let partition = unsafe { std::slice::from_raw_parts(ptr, len) };
+ partitions.push(partition.to_vec());
}
+ Ok(partitions)
}
// Taken from `Vec::into_raw_parts` which is currently nightly-only.
@@ -711,11 +741,83 @@ mod tests {
}
#[test]
- fn test_partitions_roundtrip() {
- let partitions_expected: Partitions = vec![b"A".into(), b"BB".into(),
b"CCC".into()];
- let partitions_ffi: FFI_AdbcPartitions =
partitions_expected.clone().into();
- let partitions_actual: Partitions = partitions_ffi.into();
- assert_eq!(partitions_expected, partitions_actual);
+ fn test_partitions_roundtrip() -> Result<(), Error> {
+ let inputs: [Partitions; 3] = [
+ vec![],
+ vec![vec![], Vec::with_capacity(8)],
+ vec![b"A".into(), vec![], b"BB".into(), b"CCC".into()],
+ ];
+ for partitions_expected in inputs {
+ let partitions_ffi: FFI_AdbcPartitions =
partitions_expected.clone().into();
+ // SAFETY: The metadata, allocations, and callback come from the
Rust exporter.
+ let partitions_actual = unsafe { import_partitions(partitions_ffi)
}?;
+ assert_eq!(partitions_expected, partitions_actual);
+ }
+ Ok(())
+ }
+
+ #[test]
+ fn test_import_partitions_null_pointers_and_release() {
+ use std::sync::atomic::{AtomicUsize, Ordering};
+
+ unsafe extern "C" fn release(partitions: *mut FFI_AdbcPartitions) {
+ // SAFETY: Each test input points to a live local counter, and Drop
+ // passes a valid pointer to the consumed FFI value.
+ unsafe {
+ let partitions = &mut *partitions;
+ let counter = &*partitions.private_data.cast::<AtomicUsize>();
+ counter.fetch_add(1, Ordering::SeqCst);
+ partitions.release = None;
+ }
+ }
+
+ let counter = AtomicUsize::new(0);
+ let mut data = *b"A";
+ let mut pointers = [data.as_mut_ptr(), null_mut()];
+ let mut lengths = [1, 1];
+ let mut empty_lengths = [1, 0];
+ let pointers_ptr = pointers.as_mut_ptr();
+ let lengths_ptr = lengths.as_mut_ptr();
+ let empty_lengths_ptr = empty_lengths.as_mut_ptr();
+ let inputs = [
+ (0, null_mut(), null_mut(), Some(vec![])),
+ (1, null_mut(), null_mut(), None),
+ (1, null_mut(), lengths_ptr, None),
+ (1, pointers_ptr, null_mut(), None),
+ (1, pointers_ptr.wrapping_add(1), lengths_ptr, None),
+ (2, pointers_ptr, lengths_ptr, None),
+ (
+ 2,
+ pointers_ptr,
+ empty_lengths_ptr,
+ Some(vec![b"A".to_vec(), vec![]]),
+ ),
+ ];
+
+ for (num_partitions, partitions, partition_lengths, expected) in
inputs {
+ counter.store(0, Ordering::SeqCst);
+ let value = FFI_AdbcPartitions {
+ num_partitions,
+ partitions,
+ partition_lengths,
+ private_data: (&counter as *const
AtomicUsize).cast_mut().cast(),
+ release: Some(release),
+ };
+ // SAFETY: All non-null arrays and nonempty data refer to live
local
+ // allocations of sufficient size. The callback only accesses the
counter.
+ let result = unsafe { import_partitions(value) };
+ match (result, expected) {
+ (Ok(actual), Some(expected)) => assert_eq!(actual, expected),
+ (Err(error), None) => {
+ assert_eq!(error.status, Status::Internal);
+ assert!(error.message.contains("null"));
+ }
+ (actual, expected) => {
+ assert_eq!(actual.ok(), expected, "unexpected import
result");
+ }
+ }
+ assert_eq!(counter.load(Ordering::SeqCst), 1);
+ }
}
#[test]