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

123 lines
2.9 KiB
Markdown

---
id: ffi-01
original_id: P.UNS.FFI.01
level: P
impact: 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
```rust
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
```rust
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?
## Related Rules
- `ffi-02`: Read documentation for std::ffi types
- `ffi-06`: Ensure C-ABI string compatibility