diff --git a/rust/driver_manager/tests/common/dummy.rs b/rust/driver_manager/tests/common/dummy.rs index acac42995c..5d9b1e64df 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, +} + +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> { - todo!() + fn get_option_bytes(&self, key: Self::Option) -> Result> { + 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 { - todo!() + fn get_option_double(&self, key: Self::Option) -> Result { + 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 { - todo!() + fn get_option_int(&self, key: Self::Option) -> Result { + 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 { - todo!() + fn get_option_string(&self, key: Self::Option) -> Result { + 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 186dd47e3a..7acd3b7d53 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 cd356768f5..72aa43add2 100644 --- a/rust/ffi/src/driver_exporter.rs +++ b/rust/ffi/src/driver_exporter.rs @@ -43,21 +43,31 @@ type StatementType = enum ExportedDatabase { /// Pre-init options - Options(HashMap), + Options(Mutex>), /// Initialized database - Database(DatabaseType), + Database(Mutex>), } +type DatabaseTuple<'a, DriverType> = ( + Option>>, + Option>>, +); + impl ExportedDatabase { - fn tuple( - &mut self, - ) -> ( - Option<&mut HashMap>, - Option<&mut DatabaseType>, - ) { + fn tuple(&self) -> Result> { 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> { - let exported = database.private_data as *mut ExportedDatabase; - exported.as_mut().ok_or(Error::with_message_and_status( + database: *mut FFI_AdbcDatabase, +) -> Result<&'a ExportedDatabase> { + let exported = (*database).private_data as *mut ExportedDatabase; + 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>( - 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> 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( ) -> AdbcStatusCode { catch_panic(error, || { let database = pointer_as_mut!(database, error); - let exported = Box::new(ExportedDatabase::::Options(HashMap::new())); + let exported = Box::new(ExportedDatabase::::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( 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::(database) }, + unsafe { + let exported = database.private_data as *mut ExportedDatabase; + 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( 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( 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::(database, key, value, error) } @@ -712,7 +749,7 @@ extern "C" fn database_set_option_double( 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::(database, key, value, error) } @@ -727,7 +764,7 @@ extern "C" fn database_set_option_bytes( 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( 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( unsafe { database_private_data::(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( 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( unsafe { database_private_data::(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( 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( unsafe { database_private_data::(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( 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( unsafe { database_private_data::(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( ) -> 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( 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(