Files
Sprimo/skills/unsafe-checker/rules/ffi-06-string-abi.md
2026-02-12 22:58:33 +08:00

3.1 KiB

id, original_id, level, impact
id original_id level impact
ffi-06 P.UNS.FFI.06 P HIGH

Ensure C-ABI Compatibility for Strings Between Rust and C

Summary

When passing strings across FFI, ensure both sides agree on encoding, null-termination, and memory ownership.

Rationale

  • Rust strings are UTF-8, C strings are byte arrays
  • C expects null termination, Rust strings don't have it
  • Memory ownership must be explicit to avoid leaks/double-frees

String Passing Patterns

Rust to C (Caller Allocates)

use std::ffi::CString;
use std::os::raw::c_char;

extern "C" {
    fn c_process_string(s: *const c_char);
}

fn rust_to_c(s: &str) -> Result<(), std::ffi::NulError> {
    let c_string = CString::new(s)?;
    // c_string lives until end of scope
    unsafe {
        c_process_string(c_string.as_ptr());
    }
    // c_string dropped here, memory freed
    Ok(())
}

C to Rust (C Allocates, Rust Borrows)

use std::ffi::CStr;
use std::os::raw::c_char;

extern "C" {
    fn c_get_string() -> *const c_char;
}

fn c_to_rust() -> Option<String> {
    let ptr = unsafe { c_get_string() };
    if ptr.is_null() {
        return None;
    }
    // Borrow from C, don't take ownership
    let c_str = unsafe { CStr::from_ptr(ptr) };
    Some(c_str.to_string_lossy().into_owned())
}

C to Rust (Ownership Transfer)

extern "C" {
    fn c_create_string() -> *mut c_char;
    fn c_free_string(s: *mut c_char);
}

struct CAllocatedString {
    ptr: *mut c_char,
}

impl CAllocatedString {
    fn new() -> Option<Self> {
        let ptr = unsafe { c_create_string() };
        if ptr.is_null() {
            None
        } else {
            Some(Self { ptr })
        }
    }

    fn as_str(&self) -> &str {
        let c_str = unsafe { CStr::from_ptr(self.ptr) };
        c_str.to_str().unwrap_or("")
    }
}

impl Drop for CAllocatedString {
    fn drop(&mut self) {
        unsafe { c_free_string(self.ptr); }
    }
}

Rust to C (Ownership Transfer)

extern "C" {
    fn c_take_ownership(s: *mut c_char);  // C will free
}

fn give_to_c(s: &str) -> Result<(), std::ffi::NulError> {
    let c_string = CString::new(s)?;
    let ptr = c_string.into_raw();  // Don't drop CString

    unsafe {
        c_take_ownership(ptr);
        // C now owns this memory
        // To free it back in Rust: let _ = CString::from_raw(ptr);
    }
    Ok(())
}

Encoding Considerations

// UTF-8 to platform encoding
use std::ffi::OsString;
use std::os::unix::ffi::OsStrExt;

fn to_platform_string(s: &str) -> CString {
    // On Unix, UTF-8 usually works
    CString::new(s).unwrap()
}

#[cfg(windows)]
fn to_wide_string(s: &str) -> Vec<u16> {
    use std::os::windows::ffi::OsStrExt;
    std::ffi::OsStr::new(s)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect()
}

Checklist

  • Is the string null-terminated when passed to C?
  • Who allocates the memory? Who frees it?
  • Is the encoding (UTF-8, ASCII, platform) documented?
  • Am I handling conversion errors (interior nulls, invalid UTF-8)?
  • ffi-01: Use CString/CStr at FFI boundaries
  • ffi-02: Read std::ffi documentation