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

3.5 KiB

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

Handle Errors Properly in FFI

Summary

FFI functions must use C-compatible error handling (return codes, errno, out parameters). Rust's Result/Option don't cross FFI boundaries.

Rationale

  • C doesn't have Result or Option
  • Exceptions don't exist in C
  • Must use patterns C code understands

Bad Example

// DON'T: Return Result across FFI
#[no_mangle]
pub extern "C" fn bad_open(path: *const c_char) -> Result<Handle, Error> {
    // Result is not C-compatible!
    unimplemented!()
}

// DON'T: Return Option across FFI
#[no_mangle]
pub extern "C" fn bad_find(id: i32) -> Option<*mut Data> {
    // Option<*mut T> might work but is confusing
    unimplemented!()
}

Good Example

use std::os::raw::{c_char, c_int};

// Error codes
const SUCCESS: c_int = 0;
const ERR_NULL_PTR: c_int = 1;
const ERR_INVALID_PATH: c_int = 2;
const ERR_FILE_NOT_FOUND: c_int = 3;
const ERR_PERMISSION: c_int = 4;
const ERR_UNKNOWN: c_int = -1;

// DO: Return error code, output via pointer
#[no_mangle]
pub extern "C" fn open_file(
    path: *const c_char,
    out_handle: *mut *mut Handle
) -> c_int {
    if path.is_null() || out_handle.is_null() {
        return ERR_NULL_PTR;
    }

    let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
        Ok(s) => s,
        Err(_) => return ERR_INVALID_PATH,
    };

    match File::open(path_str) {
        Ok(file) => {
            let handle = Box::into_raw(Box::new(Handle { file }));
            unsafe { *out_handle = handle; }
            SUCCESS
        }
        Err(e) => {
            match e.kind() {
                std::io::ErrorKind::NotFound => ERR_FILE_NOT_FOUND,
                std::io::ErrorKind::PermissionDenied => ERR_PERMISSION,
                _ => ERR_UNKNOWN,
            }
        }
    }
}

// DO: Use errno for POSIX-style APIs
#[cfg(unix)]
#[no_mangle]
pub extern "C" fn posix_style_read(
    fd: c_int,
    buf: *mut u8,
    count: usize
) -> isize {
    if buf.is_null() {
        unsafe { *libc::__errno_location() = libc::EINVAL; }
        return -1;
    }

    // ... do read ...
    // On error:
    // unsafe { *libc::__errno_location() = error_code; }
    // return -1;

    count as isize
}

// DO: Provide error message function
thread_local! {
    static LAST_ERROR: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
}

#[no_mangle]
pub extern "C" fn get_error_message(buf: *mut c_char, len: usize) -> c_int {
    LAST_ERROR.with(|e| {
        if let Some(msg) = e.borrow().as_ref() {
            let bytes = msg.as_bytes();
            let copy_len = std::cmp::min(bytes.len(), len.saturating_sub(1));
            unsafe {
                std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
                *buf.add(copy_len) = 0;
            }
            SUCCESS
        } else {
            ERR_UNKNOWN
        }
    })
}

Error Handling Patterns

Pattern Usage
Return code Simple success/failure
Return code + out param Return value on success
errno POSIX-style APIs
Error message function Detailed error info
Last-error thread-local Windows-style APIs

Checklist

  • Am I returning C-compatible error indicators?
  • Are output parameters used for return values?
  • Is there a way to get detailed error info?
  • Am I documenting all possible error codes?
  • ffi-04: Handle panics at FFI boundary
  • safety-10: Document safety requirements