Add: windows mvp - transparent bugs not fixed
This commit is contained in:
122
skills/unsafe-checker/rules/ffi-01-no-string-direct.md
Normal file
122
skills/unsafe-checker/rules/ffi-01-no-string-direct.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user