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 732726e82 fix(rust/ffi): preserve caller's AdbcError private_data on
the 1.0.0 path (#4473)
732726e82 is described below
commit 732726e825047977ef9d355a4b9d530a66557c7b
Author: Fredrik Fornwall <[email protected]>
AuthorDate: Thu Jul 9 00:42:50 2026 +0200
fix(rust/ffi): preserve caller's AdbcError private_data on the 1.0.0 path
(#4473)
Fixes #4472.
---------
Signed-off-by: Fredrik Fornwall <[email protected]>
Co-authored-by: David Li <[email protected]>
---
rust/ffi/src/driver_exporter.rs | 50 ++++------
rust/ffi/src/lib.rs | 2 +-
rust/ffi/src/types.rs | 197 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 216 insertions(+), 33 deletions(-)
diff --git a/rust/ffi/src/driver_exporter.rs b/rust/ffi/src/driver_exporter.rs
index b1bb5e90a..ca483c5a4 100644
--- a/rust/ffi/src/driver_exporter.rs
+++ b/rust/ffi/src/driver_exporter.rs
@@ -238,12 +238,7 @@ macro_rules! check_err {
Err(error) => {
let error = adbc_core::error::Error::from(error);
let status: adbc_core::error::AdbcStatusCode =
error.status.into();
- if !$err_out.is_null() {
- let mut ffi_error =
-
$crate::FFI_AdbcError::try_from(error).unwrap_or_else(Into::into);
- ffi_error.private_driver = unsafe {
(*$err_out).private_driver };
- unsafe { std::ptr::write_unaligned($err_out, ffi_error) };
- }
+ unsafe { $crate::export_error($err_out, error) };
return status;
}
}
@@ -280,16 +275,11 @@ macro_rules! pointer_as_mut {
match unsafe { $ptr.as_mut() } {
Some(p) => p,
None => {
- if !$err_out.is_null() {
- let error =
adbc_core::error::Error::with_message_and_status(
- format!("Passed null pointer for argument {:?}",
stringify!($ptr)),
- adbc_core::error::Status::InvalidArguments,
- );
- let mut ffi_error =
-
$crate::FFI_AdbcError::try_from(error).unwrap_or_else(Into::into);
- ffi_error.private_driver = unsafe {
(*$err_out).private_driver };
- unsafe { std::ptr::write_unaligned($err_out, ffi_error) };
- }
+ let error = adbc_core::error::Error::with_message_and_status(
+ format!("Passed null pointer for argument {:?}",
stringify!($ptr)),
+ adbc_core::error::Status::InvalidArguments,
+ );
+ unsafe { $crate::export_error($err_out, error) };
return adbc_core::error::Status::InvalidArguments.into();
}
}
@@ -507,22 +497,18 @@ fn catch_panic<F: FnOnce() -> AdbcStatusCode +
std::panic::UnwindSafe>(
Err(cause) => {
POISON.store(true, std::sync::atomic::Ordering::Release);
- if !error.is_null() {
- let message = if let Some(s) = cause.downcast_ref::<&str>() {
- s.to_string()
- } else if let Some(s) = cause.downcast_ref::<String>() {
- s.clone()
- } else {
- "Unknown panic".to_string()
- };
- let err = Error::with_message_and_status(
- format!("Uncaught panic in driver: {message}"),
- Status::Internal,
- );
- let mut ffi_error =
FFI_AdbcError::try_from(err).unwrap_or_else(Into::into);
- ffi_error.private_driver = unsafe { (*error).private_driver };
- unsafe { std::ptr::write_unaligned(error, ffi_error) };
- }
+ let message = if let Some(s) = cause.downcast_ref::<&str>() {
+ s.to_string()
+ } else if let Some(s) = cause.downcast_ref::<String>() {
+ s.clone()
+ } else {
+ "Unknown panic".to_string()
+ };
+ let err = Error::with_message_and_status(
+ format!("Uncaught panic in driver: {message}"),
+ Status::Internal,
+ );
+ unsafe { crate::export_error(error, err) };
Status::Internal.into()
}
}
diff --git a/rust/ffi/src/lib.rs b/rust/ffi/src/lib.rs
index 0805d2f0e..c171bc522 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,
+ FFI_AdbcErrorDetail, FFI_AdbcPartitions, FFI_AdbcStatement, export_error,
};
diff --git a/rust/ffi/src/types.rs b/rust/ffi/src/types.rs
index 033eb7c21..01f27e0a2 100644
--- a/rust/ffi/src/types.rs
+++ b/rust/ffi/src/types.rs
@@ -45,6 +45,15 @@ pub struct FFI_AdbcError {
pub private_driver: *const FFI_AdbcDriver,
}
+#[repr(C)]
+#[derive(Debug)]
+pub struct FFI_AdbcErrorV100 {
+ pub message: *mut c_char,
+ pub vendor_code: i32,
+ pub sqlstate: [c_char; 5],
+ pub release: Option<unsafe extern "C" fn(*mut Self)>,
+}
+
#[repr(C)]
#[derive(Debug)]
pub struct FFI_AdbcErrorDetail {
@@ -555,6 +564,37 @@ impl TryFrom<Error> for FFI_AdbcError {
}
}
+impl From<NulError> for FFI_AdbcErrorV100 {
+ fn from(value: NulError) -> Self {
+ let message = CString::new(format!(
+ "Interior null byte was found at position {}",
+ value.nul_position()
+ ))
+ .unwrap();
+ FFI_AdbcErrorV100 {
+ message: message.into_raw(),
+ vendor_code: 0,
+ sqlstate: [0; 5],
+ release: Some(release_ffi_error_v100),
+ }
+ }
+}
+
+impl TryFrom<Error> for FFI_AdbcErrorV100 {
+ type Error = NulError;
+
+ fn try_from(value: Error) -> Result<Self, Self::Error> {
+ // TODO: instead of dying on an interior NUL, just truncate or replace
+ let message = CString::new(value.message)?;
+ Ok(FFI_AdbcErrorV100 {
+ message: message.into_raw(),
+ release: Some(release_ffi_error_v100),
+ vendor_code: value.vendor_code,
+ sqlstate: value.sqlstate,
+ })
+ }
+}
+
unsafe extern "C" fn release_ffi_error(error: *mut FFI_AdbcError) {
match error.as_mut() {
None => (),
@@ -579,6 +619,44 @@ unsafe extern "C" fn release_ffi_error(error: *mut
FFI_AdbcError) {
}
}
+unsafe extern "C" fn release_ffi_error_v100(error: *mut FFI_AdbcErrorV100) {
+ match error.as_mut() {
+ None => (),
+ Some(error) => {
+ if !error.message.is_null() {
+ // SAFETY: `error.message` was necessarily obtained with
`CString::into_raw`.
+ drop(CString::from_raw(error.message));
+ error.message = null_mut();
+ }
+ error.release = None;
+ }
+ }
+}
+
+/// Export an error into an FFI error, accounting for the v1.0.0/v1.1.0 ABI
+/// extension.
+#[doc(hidden)]
+pub unsafe fn export_error(err_out: *mut FFI_AdbcError, error: Error) {
+ if err_out.is_null() {
+ return;
+ }
+
+ let is_v110 = (*err_out).vendor_code ==
constants::ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA;
+ if let Some(release) = (*err_out).release {
+ release(err_out);
+ }
+ if is_v110 {
+ let mut ffi_error =
FFI_AdbcError::try_from(error).unwrap_or_else(Into::into);
+ ffi_error.vendor_code = constants::ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA;
+ ffi_error.private_driver = (*err_out).private_driver;
+ std::ptr::write_unaligned(err_out, ffi_error);
+ } else {
+ let ffi_error =
FFI_AdbcErrorV100::try_from(error).unwrap_or_else(Into::into);
+ let out = &mut *(err_out as *mut FFI_AdbcErrorV100);
+ std::ptr::write_unaligned(out, ffi_error);
+ }
+}
+
impl Drop for FFI_AdbcError {
fn drop(&mut self) {
if let Some(release) = self.release {
@@ -690,4 +768,123 @@ mod tests {
assert!(ffi_error.release.is_none());
// Drop here is a no-op because release is None.
}
+
+ #[test]
+ fn test_set_error_out_preserves_caller_private_fields() {
+ // A caller that did not opt into the 1.1.0 layout leaves
`vendor_code` at
+ // something other than the sentinel. `export_error` must then leave
+ // `private_data` / `private_driver` untouched, even when the error
carries
+ // details that would otherwise be stashed in `private_data`. This
mirrors
+ // the C++ validation suite's `StatementTest.ErrorCompatibility`.
+ let mut canary_data = 0x1234;
+ let canary_ptr = (&raw mut canary_data) as *mut c_void;
+ let canary_driver: FFI_AdbcDriver = Default::default();
+ let mut out = FFI_AdbcError {
+ message: null_mut(),
+ vendor_code: 0,
+ sqlstate: [0; 5],
+ release: None,
+ private_data: canary_ptr,
+ private_driver: &raw const canary_driver,
+ };
+
+ let error = Error {
+ message: "boom".into(),
+ status: Status::Internal,
+ vendor_code: 0,
+ sqlstate: [0; 5],
+ details: Some(vec![("key".to_string(), b"value".to_vec())]),
+ };
+ unsafe { export_error(&mut out, error) };
+
+ // The extended fields are untouched.
+ assert_eq!(out.private_data, canary_ptr);
+ assert_eq!(out.private_driver, &raw const canary_driver);
+ // The message was written and a message-only release installed.
+ assert!(!out.message.is_null());
+ assert!(out.release.is_some());
+
+ // Releasing frees only the message and must not touch the canary.
+ unsafe { (out.release.unwrap())(&mut out) };
+ assert!(out.message.is_null());
+ assert!(out.release.is_none());
+ assert_eq!(out.private_data, canary_ptr);
+ assert_eq!(out.private_driver, &raw const canary_driver);
+ // Drop here is a no-op because release is None.
+ }
+
+ #[test]
+ fn test_set_error_out_extended_layout_overwrites() {
+ // A caller that opted in (vendor_code == sentinel) gets the full
struct,
+ // including details in `private_data`; `private_driver` is preserved
so the
+ // driver manager keeps its handle.
+ let canary_driver: FFI_AdbcDriver = Default::default();
+ let mut out = FFI_AdbcError {
+ message: null_mut(),
+ vendor_code: constants::ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA,
+ sqlstate: [0; 5],
+ release: None,
+ private_data: null_mut(),
+ private_driver: &raw const canary_driver,
+ };
+
+ let error = Error {
+ message: "boom".into(),
+ status: Status::Internal,
+ vendor_code: 0,
+ sqlstate: [0; 5],
+ details: Some(vec![("key".to_string(), b"value".to_vec())]),
+ };
+ unsafe { export_error(&mut out, error) };
+
+ assert!(!out.message.is_null());
+ assert!(!out.private_data.is_null()); // details stashed here
+ assert_eq!(out.private_driver, &raw const canary_driver); // preserved
+ // The sentinel survives: consumers may only read private_data while
+ // vendor_code holds it (adbc.h), so the details above stay reachable.
+ assert_eq!(
+ out.vendor_code,
+ constants::ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA
+ );
+ assert!(out.release.is_some());
+
+ // The full release frees both the message and the details in
private_data.
+ unsafe { (out.release.unwrap())(&mut out) };
+ assert!(out.message.is_null());
+ assert!(out.private_data.is_null());
+ // Drop here is a no-op because release is None.
+ }
+
+ #[test]
+ fn test_set_error_out_releases_a_previous_error_on_reuse() {
+ // Writing into a struct that still holds an earlier error releases it
+ // first (as the C reference does), instead of leaking its message.
+ let mut out = FFI_AdbcError {
+ message: null_mut(),
+ vendor_code: 0,
+ sqlstate: [0; 5],
+ release: None,
+ private_data: null_mut(),
+ private_driver: null(),
+ };
+ let error = |msg: &str| Error {
+ message: msg.into(),
+ status: Status::Internal,
+ vendor_code: 0,
+ sqlstate: [0; 5],
+ details: None,
+ };
+
+ unsafe { export_error(&mut out, error("first")) };
+ let first_message = out.message;
+ assert!(!first_message.is_null());
+
+ unsafe { export_error(&mut out, error("second")) };
+ assert!(!out.message.is_null());
+ let message = unsafe { CStr::from_ptr(out.message) };
+ assert_eq!(message.to_str().unwrap(), "second");
+
+ unsafe { (out.release.unwrap())(&mut out) };
+ assert!(out.message.is_null());
+ }
}