Skip to content

Commit c4f286c

Browse files
authored
fix(rust/ffi): fix release_ffi_error to properly null values (#4181)
Fixes #4178
1 parent c3216f1 commit c4f286c

1 file changed

Lines changed: 68 additions & 3 deletions

File tree

rust/ffi/src/types.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)