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

146 lines
3.6 KiB
Markdown

---
id: ffi-04
original_id: P.UNS.FFI.04
level: P
impact: CRITICAL
clippy: panic_in_result_fn
---
# Handle Panics When Crossing FFI Boundaries
## Summary
Panics must not unwind across FFI boundaries. Use `catch_unwind` or mark functions as `extern "C-unwind"`.
## Rationale
- Unwinding across C code is undefined behavior
- C has no concept of Rust panics
- Can corrupt C stack frames and cause crashes
- Even with `panic=abort`, still UB to attempt unwinding in `extern "C"`
## Bad Example
```rust
// DON'T: Allow panics to escape to C
#[no_mangle]
pub extern "C" fn callback(data: *const u8, len: usize) -> i32 {
let slice = unsafe { std::slice::from_raw_parts(data, len) };
// If this panics, UB occurs!
let sum: i32 = slice.iter().map(|&x| x as i32).sum();
// If this panics due to overflow in debug, UB!
process(sum)
}
// DON'T: Unwrap in extern functions
#[no_mangle]
pub extern "C" fn parse_config(path: *const c_char) -> i32 {
let path = unsafe { CStr::from_ptr(path) };
let config = std::fs::read_to_string(path.to_str().unwrap()).unwrap(); // Can panic!
0
}
```
## Good Example
```rust
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ffi::CStr;
use std::os::raw::{c_char, c_int};
// DO: Catch panics at FFI boundary
#[no_mangle]
pub extern "C" fn safe_callback(data: *const u8, len: usize) -> c_int {
let result = catch_unwind(AssertUnwindSafe(|| {
if data.is_null() || len == 0 {
return -1;
}
let slice = unsafe { std::slice::from_raw_parts(data, len) };
let sum: i32 = slice.iter().map(|&x| x as i32).sum();
sum
}));
match result {
Ok(value) => value,
Err(_) => {
// Log error, return error code
eprintln!("Panic caught at FFI boundary");
-1
}
}
}
// DO: Use Result-based API internally
#[no_mangle]
pub extern "C" fn parse_config(path: *const c_char) -> c_int {
let result = catch_unwind(AssertUnwindSafe(|| -> Result<(), Box<dyn std::error::Error>> {
let path = unsafe { CStr::from_ptr(path) }.to_str()?;
let _config = std::fs::read_to_string(path)?;
Ok(())
}));
match result {
Ok(Ok(())) => 0,
Ok(Err(e)) => {
eprintln!("Error: {}", e);
-1
}
Err(_) => {
eprintln!("Panic in parse_config");
-2
}
}
}
// DO: For Rust-calling-Rust across C, use "C-unwind"
#[no_mangle]
pub extern "C-unwind" fn rust_callback_can_unwind() {
// This is OK to panic if called from Rust through C
// The "C-unwind" ABI allows unwinding
panic!("This is allowed");
}
```
## FFI Error Handling Pattern
```rust
// Define error codes
const SUCCESS: c_int = 0;
const ERR_NULL_PTR: c_int = -1;
const ERR_INVALID_UTF8: c_int = -2;
const ERR_IO: c_int = -3;
const ERR_PANIC: c_int = -99;
// Thread-local for detailed error
thread_local! {
static LAST_ERROR: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
}
fn set_error(msg: String) {
LAST_ERROR.with(|e| *e.borrow_mut() = Some(msg));
}
#[no_mangle]
pub extern "C" fn get_last_error() -> *const c_char {
LAST_ERROR.with(|e| {
e.borrow().as_ref().map(|s| s.as_ptr() as *const c_char)
.unwrap_or(std::ptr::null())
})
}
```
## Checklist
- [ ] Does my extern "C" function use catch_unwind?
- [ ] Am I avoiding unwrap/expect in FFI functions?
- [ ] Do I return error codes for error conditions?
- [ ] Have I considered using "C-unwind" for Rust-to-Rust through C?
## Related Rules
- `ffi-08`: Handle errors properly in FFI
- `safety-01`: Panic safety