ffi-code-review
Installation
SKILL.md
FFI Code Review
Review Workflow
- Check Cargo.toml -- Note Rust edition (2024 has breaking changes to extern blocks and unsafe attributes),
build-dependencies(bindgen, cc, pkg-config),crate-type(cdylib,staticlib), andlinkskey - Check build.rs -- Verify link directives (
cargo:rustc-link-lib,cargo:rustc-link-search), bindgen configuration, and C source compilation - Check extern blocks -- Verify calling conventions, symbol declarations, and safety annotations
- Check type layout -- Every type crossing FFI must be
#[repr(C)]or a primitive FFI type - Check string and pointer handling -- CStr/CString usage, null checks, ownership transfers
- Check callbacks --
extern "C" fnpointers, panic safety across FFI boundary - Verify before reporting -- Load
beagle-rust:review-verification-protocolbefore submitting findings
Output Format
Report findings as:
[FILE:LINE] ISSUE_TITLE
Severity: Critical | Major | Minor | Informational
Description of the issue and why it matters.
Quick Reference
| Issue Type | Reference |
|---|---|
| C-to-Rust type mapping, repr(C) layout, enums, opaque types | references/type-mapping.md |
| Safe wrappers, ownership transfer, callbacks, build.rs, testing | references/safety-patterns.md |
Review Checklist
extern Blocks and Calling Conventions
- Foreign function declarations use
extern "C"(explicit, not bareextern) - Edition 2024:
extern "C" {}blocks written asunsafe extern "C" {} - Functions exposed to C use
extern "C" fn(not default Rust calling convention) - Calling convention matches the foreign library (
"C","system"for Win32 API) -
#[link(name = "...")]specifies the correct library name -
#[link(name = "...", kind = "static")]used when statically linking
Symbol Management
- Exported functions use
#[no_mangle]to preserve symbol names - Edition 2024:
#[no_mangle]written as#[unsafe(no_mangle)] - Edition 2024:
#[export_name = "..."]written as#[unsafe(export_name = "...")] -
#[link_name = "..."]used when Rust name differs from C symbol - Exported items are
pub(only public#[no_mangle]symbols appear in library output)
Type Layout
- Every struct/union crossing FFI has
#[repr(C)]-- Rust's default layout is undefined - Primitive types use
std::ffi/std::os::rawequivalents (c_int,c_char,c_void) - No bare
i32where C usesint-- usec_int(width varies by platform) - Quirky C types like
__be32use byte arrays ([u8; 4]), not Rust integers - Enums crossing FFI use
#[repr(C)]or#[repr(u8)]/#[repr(u32)]with explicit discriminants - C-style bitflag enums use a newtype around an integer (or
bitflagscrate), not a Rust enum -
#[non_exhaustive]on enums representing C enumerations that may gain new values
String Handling
- C strings use
CStr(borrowed) orCString(owned), never&strorString -
CString::new()result is checked for interior null bytes (returnsErron\0) -
CStringoutlives any*const c_charpointer derived from it via.as_ptr() - Incoming
*const c_charvalidated withCStr::from_ptr()insideunsafe - No assumption that C strings are valid UTF-8 -- use
to_str()which returnsResult - OS paths use
OsStr/OsStringandCStr, not&str
Ownership and Allocation
- Clear ownership contract: who allocates, who frees
- Rust-allocated memory freed by Rust (
Box::from_raw), C-allocated freed by C -
Box::into_raw/Box::from_rawpaired correctly for heap transfers -
Vec::into_raw_partsused when passing arrays to C (pointer + length + capacity) - Destructor functions exposed for every opaque Rust type given to C
- No
Droprunning on C-allocated memory (and vice versa)
Callbacks
- Callback types are
extern "C" fn(...), not closures orfn(...) - Callbacks use
std::panic::catch_unwindto prevent panics from unwinding across FFI - Callback context passed as
*mut c_voidwith safe reconstruction at call site -
Option<extern "C" fn(...)>used for nullable function pointers (niche optimization)
Bindgen and Build Scripts
- Bindgen output reviewed for correctness (auto-generated types may need adjustment)
-
-syscrate pattern used for raw bindings, separate crate for safe wrappers -
build.rsusescargo:rustc-link-libandcargo:rustc-link-searchcorrectly -
linkskey inCargo.tomlprevents duplicate linking of the same native library - Platform-specific bindings generated per-build (not checked in for a single platform)
Safety Documentation
- Every
unsafeblock has a// SAFETY:comment explaining invariants - Every public FFI wrapper function documents safety requirements
- Edition 2024:
unsafe fnbodies use explicitunsafe {}blocks around unsafe ops
Severity Calibration
Critical (Block Merge)
- Missing
#[repr(C)]on types crossing FFI boundary (undefined memory layout) - Wrong string handling:
&str/StringwhereCStr/CStringrequired - Ownership confusion: freeing C-allocated memory with Rust's allocator (or vice versa)
- Panic unwinding across FFI boundary without
catch_unwind - Using Rust enum for C bitflags (invalid discriminant = undefined behavior)
- Passing closure where
extern "C" fnpointer required
Major (Should Fix)
- Missing safety documentation on
unsafeblocks or public FFI functions - No null pointer check on incoming
*const T/*mut Tbefore dereferencing CStringdropped before its pointer is used by C (dangling pointer)- Missing
#[link(name = "...")]causing link failures on some platforms - Edition 2024:
externblock not markedunsafe extern - Edition 2024:
#[no_mangle]not wrapped in#[unsafe(...)]
Minor (Consider Fixing)
- Using
i32instead ofc_intfor Cint(correct on most platforms but not portable) - Missing
#[non_exhaustive]on enums mapping to extensible C enumerations - Verbose manual bindings where bindgen would be more maintainable
- Checked-in bindings without platform guards
Informational
- Suggestions to split raw bindings into a
-syscrate - Suggestions to add opaque wrapper types for distinct
*mut c_voidpointers - Suggestions to use
Option<NonNull<T>>for nullable pointers
Valid Patterns (Do NOT Flag)
unsafe extern "C" {}in edition 2024 -- correct form for foreign declarations#[unsafe(no_mangle)]in edition 2024 -- correct form for symbol exportOption<extern "C" fn(...)>for nullable callbacks -- niche optimization guaranteedOption<NonNull<T>>for nullable pointers -- zero-cost nullable pointer pattern*mut c_voidfor opaque C types -- standard when internal layout is irrelevant- Distinct empty structs wrapping
c_voidfor type-safe opaque pointers -- prevents pointer confusion CStr::from_bytes_with_nul_uncheckedwith compile-time literal -- safe when literal is known null-terminatedextern "C-unwind"for controlled unwinding -- valid per RFC 2945include!(concat!(env!("OUT_DIR"), "/bindings.rs"))in bindgen crates -- standard patternBox::into_raw/Box::from_rawpairs for ownership transfer -- correct pattern when paired
Before Submitting Findings
Load and follow beagle-rust:review-verification-protocol before reporting any issue.
Weekly Installs
13
Repository
existential-birds/beagleGitHub Stars
56
First Seen
10 days ago
Security Audits