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

2.9 KiB

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

Avoid Passing Strings Directly to C from Public Rust API

Summary

Use CString and CStr for string handling at FFI boundaries. Never pass Rust String or &str directly to C.

Rationale

  • Rust strings are UTF-8, not null-terminated
  • C strings require null terminator
  • Rust strings may contain interior null bytes
  • Memory layout differs between Rust String and C char*

Bad Example

extern "C" {
    fn c_print(s: *const u8);
    fn c_strlen(s: *const u8) -> usize;
}

// DON'T: Pass Rust string directly
fn bad_print(s: &str) {
    unsafe {
        c_print(s.as_ptr());  // Not null-terminated!
    }
}

// DON'T: Assume length matches
fn bad_strlen(s: &str) -> usize {
    unsafe {
        c_strlen(s.as_ptr())  // May read past buffer
    }
}

// DON'T: Use String in FFI signatures
extern "C" fn bad_callback(s: String) {  // Wrong!
    println!("{}", s);
}

Good Example

use std::ffi::{CString, CStr};
use std::os::raw::c_char;

extern "C" {
    fn c_print(s: *const c_char);
    fn c_strlen(s: *const c_char) -> usize;
    fn c_get_string() -> *const c_char;
}

// DO: Convert to CString for passing to C
fn good_print(s: &str) -> Result<(), std::ffi::NulError> {
    let c_string = CString::new(s)?;  // Adds null terminator, checks for interior nulls
    unsafe {
        c_print(c_string.as_ptr());
    }
    Ok(())
}

// DO: Use CStr for receiving C strings
fn good_receive() -> String {
    unsafe {
        let ptr = c_get_string();
        let c_str = CStr::from_ptr(ptr);
        c_str.to_string_lossy().into_owned()
    }
}

// DO: Handle interior null bytes
fn handle_nulls(s: &str) {
    match CString::new(s) {
        Ok(c_string) => unsafe { c_print(c_string.as_ptr()) },
        Err(e) => {
            // String contains interior null at position e.nul_position()
            eprintln!("String contains null byte at {}", e.nul_position());
        }
    }
}

// DO: Use proper types in callbacks
extern "C" fn good_callback(s: *const c_char) {
    if !s.is_null() {
        let c_str = unsafe { CStr::from_ptr(s) };
        if let Ok(rust_str) = c_str.to_str() {
            println!("{}", rust_str);
        }
    }
}

String Type Comparison

Type Null-terminated Encoding Use
String No UTF-8 Rust owned
&str No UTF-8 Rust borrowed
CString Yes Byte Rust-to-C owned
&CStr Yes Byte Rust-to-C borrowed
*const c_char Yes Byte FFI pointer
OsString Platform Platform Paths, env

Checklist

  • Am I passing Rust strings to C? → Use CString
  • Am I receiving C strings? → Use CStr
  • Does my string contain null bytes? → Handle NulError
  • Am I checking for null pointers from C?
  • ffi-02: Read documentation for std::ffi types
  • ffi-06: Ensure C-ABI string compatibility