@@ -560,17 +560,22 @@ unsafe extern "C" fn release_ffi_error(error: *mut FFI_AdbcError) {
560560 match error. as_mut ( ) {
561561 None => ( ) ,
562562 Some ( error) => {
563- // SAFETY: `error.message` was necessarily obtained with `CString::into_raw`.
564- // Additionally, C should not modify the string's length.
565- drop ( CString :: from_raw ( error. message ) ) ;
563+ if !error. message . is_null ( ) {
564+ // SAFETY: `error.message` was necessarily obtained with `CString::into_raw`.
565+ // Additionally, C should not modify the string's length.
566+ drop ( CString :: from_raw ( error. message ) ) ;
567+ error. message = null_mut ( ) ;
568+ }
566569
567570 if !error. private_data . is_null ( ) {
568571 // SAFETY: `error.private_data` was necessarily obtained with `Box::into_raw`.
569572 // Additionally, the boxed data is necessarily `ErrorPrivateData`.
570573 // Finally, C should call the release function only once.
571574 let private_data = Box :: from_raw ( error. private_data as * mut ErrorPrivateData ) ;
572575 drop ( private_data) ;
576+ error. private_data = null_mut ( ) ;
573577 }
578+ error. release = None ;
574579 }
575580 }
576581}
@@ -626,4 +631,64 @@ mod tests {
626631 let partitions_actual: Partitions = partitions_ffi. into ( ) ;
627632 assert_eq ! ( partitions_expected, partitions_actual) ;
628633 }
634+
635+ #[ test]
636+ fn test_release_ffi_error_nulls_fields ( ) {
637+ // Use details to ensure private_data is non-null before release.
638+ let error = Error {
639+ message : "test error" . into ( ) ,
640+ status : Status :: Unknown ,
641+ vendor_code : 0 ,
642+ sqlstate : [ 0 ; 5 ] ,
643+ details : Some ( vec ! [ ( "key" . to_string( ) , b"value" . to_vec( ) ) ] ) ,
644+ } ;
645+ let mut ffi_error: FFI_AdbcError = error. try_into ( ) . unwrap ( ) ;
646+ assert ! ( !ffi_error. message. is_null( ) ) ;
647+ assert ! ( !ffi_error. private_data. is_null( ) ) ;
648+
649+ unsafe { release_ffi_error ( & mut ffi_error) } ;
650+
651+ assert ! ( ffi_error. message. is_null( ) ) ;
652+ assert ! ( ffi_error. private_data. is_null( ) ) ;
653+ assert ! ( ffi_error. release. is_none( ) ) ;
654+ // Drop here is a no-op because release is None.
655+ }
656+
657+ #[ test]
658+ fn test_release_ffi_error_idempotent ( ) {
659+ // Calling release twice must not double-free the message pointer.
660+ let error = Error {
661+ message : "test error" . into ( ) ,
662+ status : Status :: Unknown ,
663+ vendor_code : 0 ,
664+ sqlstate : [ 0 ; 5 ] ,
665+ details : None ,
666+ } ;
667+ let mut ffi_error: FFI_AdbcError = error. try_into ( ) . unwrap ( ) ;
668+
669+ unsafe {
670+ release_ffi_error ( & mut ffi_error) ;
671+ release_ffi_error ( & mut ffi_error) ;
672+ }
673+ // Drop here is a no-op because release is None.
674+ }
675+
676+ #[ test]
677+ fn test_release_ffi_error_null_message ( ) {
678+ // A null message must not cause UB or a panic during release.
679+ let mut ffi_error = FFI_AdbcError {
680+ message : null_mut ( ) ,
681+ vendor_code : 0 ,
682+ sqlstate : [ 0 ; 5 ] ,
683+ release : Some ( release_ffi_error) ,
684+ private_data : null_mut ( ) ,
685+ private_driver : null ( ) ,
686+ } ;
687+
688+ unsafe { release_ffi_error ( & mut ffi_error) } ;
689+
690+ assert ! ( ffi_error. message. is_null( ) ) ;
691+ assert ! ( ffi_error. release. is_none( ) ) ;
692+ // Drop here is a no-op because release is None.
693+ }
629694}
0 commit comments