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 ecd98b421 fix(rust/ffi): avoid potential overlapping &mut in exporter
(#4875)
ecd98b421 is described below
commit ecd98b42118c21547f509a11cc037f2faa902430
Author: David Li <[email protected]>
AuthorDate: Sun Oct 11 20:03:12 2026 -0700
fix(rust/ffi): avoid potential overlapping &mut in exporter (#4875)
Stacked on #4871.
Assisted-by: GPT-6.1 Sol
---
rust/driver_manager/tests/common/dummy.rs | 62 ++++++--
rust/driver_manager/tests/exporter_cancellation.rs | 156 ++++++++++++++++++++-
rust/ffi/src/driver_exporter.rs | 123 +++++++++++-----
3 files changed, 293 insertions(+), 48 deletions(-)
diff --git a/rust/driver_manager/tests/common/dummy.rs
b/rust/driver_manager/tests/common/dummy.rs
index acac42995..5d9b1e64d 100644
--- a/rust/driver_manager/tests/common/dummy.rs
+++ b/rust/driver_manager/tests/common/dummy.rs
@@ -15,6 +15,8 @@
// specific language governing permissions and limitations
// under the License.
+use std::collections::HashMap;
+
use adbc_core::error::{Error, Result, Status};
use adbc_core::options::{
InfoCode, ObjectDepth, OptionConnection, OptionDatabase, OptionStatement,
OptionValue,
@@ -51,29 +53,69 @@ impl Driver for DummyDriver {
}
#[derive(Default)]
-pub struct DummyDatabase {}
+pub struct DummyDatabase {
+ options: HashMap<OptionDatabase, OptionValue>,
+}
+
+impl DummyDatabase {
+ fn option(&self, key: OptionDatabase) -> Result<&OptionValue> {
+ // Let concurrent exported getters overlap while their driver borrows
are live.
+ std::thread::yield_now();
+ self.options.get(&key).ok_or_else(|| {
+ Error::with_message_and_status(
+ format!("Option key not found: {key:?}"),
+ Status::NotFound,
+ )
+ })
+ }
+}
impl Optionable for DummyDatabase {
type Option = OptionDatabase;
- fn set_option(&mut self, _key: Self::Option, _value: OptionValue) ->
Result<()> {
+ fn set_option(&mut self, key: Self::Option, value: OptionValue) ->
Result<()> {
+ self.options.insert(key, value);
Ok(())
}
- fn get_option_bytes(&self, _key: Self::Option) -> Result<Vec<u8>> {
- todo!()
+ fn get_option_bytes(&self, key: Self::Option) -> Result<Vec<u8>> {
+ match self.option(key)? {
+ OptionValue::Bytes(value) => Ok(value.clone()),
+ _ => Err(Error::with_message_and_status(
+ "Expected bytes option",
+ Status::InvalidState,
+ )),
+ }
}
- fn get_option_double(&self, _key: Self::Option) -> Result<f64> {
- todo!()
+ fn get_option_double(&self, key: Self::Option) -> Result<f64> {
+ match self.option(key)? {
+ OptionValue::Double(value) => Ok(*value),
+ _ => Err(Error::with_message_and_status(
+ "Expected double option",
+ Status::InvalidState,
+ )),
+ }
}
- fn get_option_int(&self, _key: Self::Option) -> Result<i64> {
- todo!()
+ fn get_option_int(&self, key: Self::Option) -> Result<i64> {
+ match self.option(key)? {
+ OptionValue::Int(value) => Ok(*value),
+ _ => Err(Error::with_message_and_status(
+ "Expected integer option",
+ Status::InvalidState,
+ )),
+ }
}
- fn get_option_string(&self, _key: Self::Option) -> Result<String> {
- todo!()
+ fn get_option_string(&self, key: Self::Option) -> Result<String> {
+ match self.option(key)? {
+ OptionValue::String(value) => Ok(value.clone()),
+ _ => Err(Error::with_message_and_status(
+ "Expected string option",
+ Status::InvalidState,
+ )),
+ }
}
}
diff --git a/rust/driver_manager/tests/exporter_cancellation.rs
b/rust/driver_manager/tests/exporter_cancellation.rs
index 186dd47e3..7acd3b7d5 100644
--- a/rust/driver_manager/tests/exporter_cancellation.rs
+++ b/rust/driver_manager/tests/exporter_cancellation.rs
@@ -18,13 +18,15 @@
// Test that the driver exporter and cancellation do not have undefined
// behavior by introducing overlapping mutable borrows. Run under Miri.
-use std::sync::{Arc, Mutex};
+use std::sync::atomic::{AtomicPtr, Ordering};
+use std::sync::{Arc, Barrier, Mutex};
-use adbc_core::error::{Result, Status};
+use adbc_core::constants::ADBC_STATUS_OK;
+use adbc_core::error::{Error, Result, Status};
use adbc_core::options::AdbcVersion;
use adbc_core::{CancelHandle, Connection, Database, Driver, Statement};
use adbc_driver_manager::ManagedDriver;
-use adbc_ffi::FFI_AdbcDriverInitFunc;
+use adbc_ffi::{FFI_AdbcDatabase, FFI_AdbcDriverInitFunc, FFI_AdbcError,
FFIDriver};
use arrow_array::{RecordBatch, RecordBatchReader};
use arrow_schema::{ArrowError, Schema, SchemaRef};
@@ -105,3 +107,151 @@ fn connection_cancellation() -> Result<()> {
guard.join().unwrap();
Ok(())
}
+
+// Exercise database getters directly (avoiding driver manager's lock) to test
+// for potential UB in the exporter under concurrent access.
+fn concurrent_database_getters(initialized: bool) -> Result<()> {
+ let driver = common::dummy::DummyDriver::ffi_driver();
+ let new = driver.DatabaseNew.unwrap();
+ let init = driver.DatabaseInit.unwrap();
+ let release = driver.DatabaseRelease.unwrap();
+ let set_string = driver.DatabaseSetOption.unwrap();
+ let set_bytes = driver.DatabaseSetOptionBytes.unwrap();
+ let set_int = driver.DatabaseSetOptionInt.unwrap();
+ let set_double = driver.DatabaseSetOptionDouble.unwrap();
+ let get_string = driver.DatabaseGetOption.unwrap();
+ let get_bytes = driver.DatabaseGetOptionBytes.unwrap();
+ let get_int = driver.DatabaseGetOptionInt.unwrap();
+ let get_double = driver.DatabaseGetOptionDouble.unwrap();
+
+ let mut database = FFI_AdbcDatabase::default();
+ let mut error = FFI_AdbcError::default();
+ // Use the same raw pointer throughout: workers must not create &mut
borrows
+ // of the shared handle, and passing through AtomicPtr preserves
provenance.
+ let database_ptr = &raw mut database;
+ unsafe {
+ assert_eq!(new(database_ptr, &mut error), ADBC_STATUS_OK);
+ assert_eq!(
+ set_string(
+ database_ptr,
+ c"string".as_ptr(),
+ c"hello".as_ptr(),
+ &mut error
+ ),
+ ADBC_STATUS_OK
+ );
+ assert_eq!(
+ set_bytes(
+ database_ptr,
+ c"bytes".as_ptr(),
+ b"hello".as_ptr(),
+ 5,
+ &mut error
+ ),
+ ADBC_STATUS_OK
+ );
+ assert_eq!(
+ set_int(database_ptr, c"int".as_ptr(), 42, &mut error),
+ ADBC_STATUS_OK
+ );
+ assert_eq!(
+ set_double(database_ptr, c"double".as_ptr(), 1.25, &mut error),
+ ADBC_STATUS_OK
+ );
+ if initialized {
+ assert_eq!(init(database_ptr, &mut error), ADBC_STATUS_OK);
+ // Also exercise the setter against the initialized database mutex.
+ assert_eq!(
+ set_int(database_ptr, c"int".as_ptr(), 42, &mut error),
+ ADBC_STATUS_OK
+ );
+ }
+ }
+
+ let shared = AtomicPtr::new(database_ptr);
+ let barrier = Barrier::new(2);
+ let result = std::thread::scope(|scope| -> Result<()> {
+ let mut workers = Vec::new();
+ for _ in 0..2 {
+ workers.push(scope.spawn(|| {
+ let database = shared.load(Ordering::Relaxed);
+ let mut error = FFI_AdbcError::default();
+ barrier.wait();
+ for _ in 0..4 {
+ let mut string = [0; 6];
+ let mut length = string.len();
+ unsafe {
+ assert_eq!(
+ get_string(
+ database,
+ c"string".as_ptr(),
+ string.as_mut_ptr().cast(),
+ &mut length,
+ &mut error
+ ),
+ ADBC_STATUS_OK
+ );
+ }
+ assert_eq!(length, 6);
+ assert_eq!(&string, b"hello\0");
+
+ let mut bytes = [0; 5];
+ let mut length = bytes.len();
+ unsafe {
+ assert_eq!(
+ get_bytes(
+ database,
+ c"bytes".as_ptr(),
+ bytes.as_mut_ptr(),
+ &mut length,
+ &mut error
+ ),
+ ADBC_STATUS_OK
+ );
+ }
+ assert_eq!(length, 5);
+ assert_eq!(&bytes, b"hello");
+
+ let mut int = 0;
+ unsafe {
+ assert_eq!(
+ get_int(database, c"int".as_ptr(), &mut int, &mut
error),
+ ADBC_STATUS_OK
+ );
+ }
+ assert_eq!(int, 42);
+
+ let mut double = 0.0;
+ unsafe {
+ assert_eq!(
+ get_double(database, c"double".as_ptr(), &mut
double, &mut error),
+ ADBC_STATUS_OK
+ );
+ }
+ assert_eq!(double, 1.25);
+ }
+ }));
+ }
+ for worker in workers {
+ worker.join().map_err(|_| {
+ Error::with_message_and_status("Database getter worker
panicked", Status::Internal)
+ })?;
+ }
+ Ok(())
+ });
+
+ unsafe {
+ assert_eq!(release(database_ptr, &mut error), ADBC_STATUS_OK);
+ }
+ result
+}
+
+#[test]
+fn database_getters_before_init() -> Result<()> {
+ concurrent_database_getters(false)
+}
+
+#[test]
+fn database_getters_after_init() -> Result<()> {
+ concurrent_database_getters(true)
+}
diff --git a/rust/ffi/src/driver_exporter.rs b/rust/ffi/src/driver_exporter.rs
index cd356768f..72aa43add 100644
--- a/rust/ffi/src/driver_exporter.rs
+++ b/rust/ffi/src/driver_exporter.rs
@@ -43,21 +43,31 @@ type StatementType<DriverType> =
enum ExportedDatabase<DriverType: Driver> {
/// Pre-init options
- Options(HashMap<OptionDatabase, OptionValue>),
+ Options(Mutex<HashMap<OptionDatabase, OptionValue>>),
/// Initialized database
- Database(DatabaseType<DriverType>),
+ Database(Mutex<DatabaseType<DriverType>>),
}
+type DatabaseTuple<'a, DriverType> = (
+ Option<std::sync::MutexGuard<'a, HashMap<OptionDatabase, OptionValue>>>,
+ Option<std::sync::MutexGuard<'a, DatabaseType<DriverType>>>,
+);
+
impl<DriverType: Driver> ExportedDatabase<DriverType> {
- fn tuple(
- &mut self,
- ) -> (
- Option<&mut HashMap<OptionDatabase, OptionValue>>,
- Option<&mut DatabaseType<DriverType>>,
- ) {
+ fn tuple(&self) -> Result<DatabaseTuple<'_, DriverType>> {
match self {
- Self::Options(options) => (Some(options), None),
- Self::Database(database) => (None, Some(database)),
+ Self::Options(options) => options
+ .lock()
+ .map_err(|_| {
+ Error::with_message_and_status("Database mutex poisoned",
Status::Internal)
+ })
+ .map(|guard| (Some(guard), None)),
+ Self::Database(database) => database
+ .lock()
+ .map_err(|_| {
+ Error::with_message_and_status("Database mutex poisoned",
Status::Internal)
+ })
+ .map(|guard| (None, Some(guard))),
}
}
}
@@ -575,10 +585,10 @@ fn check_poison() -> Result<()> {
// Database
unsafe fn database_private_data<'a, DriverType: Driver>(
- database: &mut FFI_AdbcDatabase,
-) -> Result<&'a mut ExportedDatabase<DriverType>> {
- let exported = database.private_data as *mut ExportedDatabase<DriverType>;
- exported.as_mut().ok_or(Error::with_message_and_status(
+ database: *mut FFI_AdbcDatabase,
+) -> Result<&'a ExportedDatabase<DriverType>> {
+ let exported = (*database).private_data as *mut
ExportedDatabase<DriverType>;
+ exported.as_ref().ok_or(Error::with_message_and_status(
"Uninitialized database",
Status::InvalidState,
))
@@ -586,7 +596,7 @@ unsafe fn database_private_data<'a, DriverType: Driver>(
// SAFETY: Will panic if `key` is null.
unsafe fn database_set_option_impl<DriverType: Driver, Value:
Into<OptionValue>>(
- database: &mut FFI_AdbcDatabase,
+ database: *mut FFI_AdbcDatabase,
key: *const c_char,
value: Value,
error: *mut FFI_AdbcError,
@@ -598,9 +608,21 @@ unsafe fn database_set_option_impl<DriverType: Driver,
Value: Into<OptionValue>>
match exported {
ExportedDatabase::Options(options) => {
+ let mut options = check_err!(
+ options.lock().map_err(|_| {
+ Error::with_message_and_status("Database mutex poisoned",
Status::Internal)
+ }),
+ error
+ );
options.insert(key.into(), value.into());
}
ExportedDatabase::Database(database) => {
+ let mut database = check_err!(
+ database.lock().map_err(|_| {
+ Error::with_message_and_status("Database mutex poisoned",
Status::Internal)
+ }),
+ error
+ );
check_err!(database.set_option(key.into(), value.into()), error);
}
}
@@ -614,7 +636,9 @@ extern "C" fn database_new<DriverType: Driver>(
) -> AdbcStatusCode {
catch_panic(error, || {
let database = pointer_as_mut!(database, error);
- let exported =
Box::new(ExportedDatabase::<DriverType>::Options(HashMap::new()));
+ let exported =
Box::new(ExportedDatabase::<DriverType>::Options(Mutex::new(
+ HashMap::new(),
+ )));
database.private_data = Box::into_raw(exported) as *mut c_void;
ADBC_STATUS_OK
@@ -628,15 +652,28 @@ extern "C" fn database_init<DriverType: Driver + Default>(
catch_panic(error, || {
let database = pointer_as_mut!(database, error);
+ // Initialization requires exclusive access to the database handle.
let exported = check_err!(
- unsafe { database_private_data::<DriverType>(database) },
+ unsafe {
+ let exported = database.private_data as *mut
ExportedDatabase<DriverType>;
+ exported.as_mut().ok_or(Error::with_message_and_status(
+ "Uninitialized database",
+ Status::InvalidState,
+ ))
+ },
error
);
if let ExportedDatabase::Options(options) = exported {
+ let options = check_err!(
+ options.get_mut().map_err(|_| {
+ Error::with_message_and_status("Database mutex poisoned",
Status::Internal)
+ }),
+ error
+ );
let mut driver = DriverType::default();
let database =
check_err!(driver.new_database_with_opts(options.clone()), error);
- *exported = ExportedDatabase::Database(database);
+ *exported = ExportedDatabase::Database(Mutex::new(database));
} else {
check_err!(
Err(Error::with_message_and_status(
@@ -682,7 +719,7 @@ extern "C" fn database_set_option<DriverType: Driver>(
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
catch_panic(error, || {
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
check_not_null!(key, error);
check_not_null!(value, error);
@@ -698,7 +735,7 @@ extern "C" fn database_set_option_int<DriverType: Driver>(
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
catch_panic(error, || {
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
check_not_null!(key, error);
unsafe { database_set_option_impl::<DriverType, i64>(database, key,
value, error) }
@@ -712,7 +749,7 @@ extern "C" fn database_set_option_double<DriverType:
Driver>(
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
catch_panic(error, || {
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
check_not_null!(key, error);
unsafe { database_set_option_impl::<DriverType, f64>(database, key,
value, error) }
@@ -727,7 +764,7 @@ extern "C" fn database_set_option_bytes<DriverType: Driver>(
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
catch_panic(error, || {
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
check_not_null!(key, error);
check_not_null!(value, error);
@@ -744,7 +781,7 @@ extern "C" fn database_get_option<DriverType: Driver>(
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
catch_panic(error, || {
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
check_not_null!(key, error);
check_not_null!(value, error);
check_not_null!(length, error);
@@ -753,9 +790,9 @@ extern "C" fn database_get_option<DriverType: Driver>(
unsafe { database_private_data::<DriverType>(database) },
error
);
- let (options, database) = exported.tuple();
+ let (mut options, mut database) = check_err!(exported.tuple(), error);
- let optvalue = unsafe { get_option(database, options, key) };
+ let optvalue = unsafe { get_option(database.as_deref_mut(),
options.as_deref_mut(), key) };
let optvalue = check_err!(optvalue, error);
check_err!(unsafe { copy_string(&optvalue, value, length) }, error);
@@ -770,7 +807,7 @@ extern "C" fn database_get_option_int<DriverType: Driver>(
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
catch_panic(error, || {
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
check_not_null!(key, error);
check_not_null!(value, error);
@@ -778,9 +815,12 @@ extern "C" fn database_get_option_int<DriverType: Driver>(
unsafe { database_private_data::<DriverType>(database) },
error
);
- let (options, database) = exported.tuple();
+ let (mut options, mut database) = check_err!(exported.tuple(), error);
- let optvalue = check_err!(unsafe { get_option_int(database, options,
key) }, error);
+ let optvalue = check_err!(
+ unsafe { get_option_int(database.as_deref_mut(),
options.as_deref_mut(), key) },
+ error
+ );
unsafe { std::ptr::write_unaligned(value, optvalue) };
ADBC_STATUS_OK
@@ -794,7 +834,7 @@ extern "C" fn database_get_option_double<DriverType:
Driver>(
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
catch_panic(error, || {
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
check_not_null!(key, error);
check_not_null!(value, error);
@@ -802,9 +842,12 @@ extern "C" fn database_get_option_double<DriverType:
Driver>(
unsafe { database_private_data::<DriverType>(database) },
error
);
- let (options, database) = exported.tuple();
+ let (mut options, mut database) = check_err!(exported.tuple(), error);
- let optvalue = check_err!(unsafe { get_option_double(database,
options, key) }, error);
+ let optvalue = check_err!(
+ unsafe { get_option_double(database.as_deref_mut(),
options.as_deref_mut(), key) },
+ error
+ );
unsafe { std::ptr::write_unaligned(value, optvalue) };
ADBC_STATUS_OK
@@ -819,7 +862,7 @@ extern "C" fn database_get_option_bytes<DriverType: Driver>(
error: *mut FFI_AdbcError,
) -> AdbcStatusCode {
catch_panic(error, || {
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
check_not_null!(key, error);
check_not_null!(value, error);
check_not_null!(length, error);
@@ -828,9 +871,10 @@ extern "C" fn database_get_option_bytes<DriverType:
Driver>(
unsafe { database_private_data::<DriverType>(database) },
error
);
- let (options, database) = exported.tuple();
+ let (mut options, mut database) = check_err!(exported.tuple(), error);
- let optvalue = unsafe { get_option_bytes(database, options, key) };
+ let optvalue =
+ unsafe { get_option_bytes(database.as_deref_mut(),
options.as_deref_mut(), key) };
let optvalue = check_err!(optvalue, error);
unsafe { copy_bytes(&optvalue, value, length) };
@@ -917,7 +961,7 @@ extern "C" fn connection_init<DriverType: Driver>(
) -> AdbcStatusCode {
catch_panic(error, || {
let connection = pointer_as_mut!(connection, error);
- let database = pointer_as_mut!(database, error);
+ check_not_null!(database, error);
// It's OK to have a &mut here since the caller can't yet get a cancel
handle
let exported_connection = check_err!(
@@ -948,6 +992,15 @@ extern "C" fn connection_init<DriverType: Driver>(
error
);
+ let database = check_err!(
+ database.lock().map_err(|_| {
+ Error::with_message_and_status(
+ "Database mutex poisoned",
+ Status::Internal,
+ )
+ }),
+ error
+ );
database.new_connection_with_opts(options.clone())
}
_ => Err(Error::with_message_and_status(