Rust Unsafe Code and FFI
Rust's safety guarantees — no data races, no use-after-free, no out-of-bounds access in safe code — come from the compiler checking ownership and borrowing. Some code cannot be checked that way: calling a C library, implementing a lock-free data structure, talking to hardware, or squeezing out the last bounds check. For those cases Rust provides unsafe, which lets you perform a small set of operations the compiler cannot verify, on the promise that you have verified them yourself.
The goal is not to avoid unsafe at all costs, but to contain it: keep unsafe blocks small, document the invariants they rely on, wrap them in safe APIs that cannot be misused from safe code, and test them with tools that detect undefined behavior. FFI (foreign function interface) — calling C from Rust and Rust from C — is where most teams first need unsafe, and where ownership, lifetimes, strings, and panics need careful handling at the boundary.
TL;DR
unsafepermits five extra operations: dereferencing raw pointers, calling unsafe functions, accessing mutable statics, implementing unsafe traits, and accessing union fields.- It does not disable the borrow checker or other checks in safe code.
- Every unsafe block needs a
// SAFETY:comment explaining why its requirements hold. - Build safe wrappers: the public API must make undefined behavior impossible from safe callers (soundness).
- For FFI, use
#[repr(C)]types,CString/CStr, clear ownership rules, andextern "C-unwind"when panics may cross the boundary. - Generate bindings with bindgen or cbindgen, and test unsafe code with Miri, sanitizers, and fuzzing.
Quick Example
Calling a C compression function through a safe wrapper (Rust 2024 edition):
Exposing a Rust function to C with a C-compatible type:
Checking for undefined behavior in tests:
Core Concepts
What Unsafe Means
Inside an unsafe block or function you may do five things the compiler cannot check. Everything else — borrow checking, type checking, bounds checks on slices — still applies. Undefined behavior (UB) in unsafe code, such as creating a dangling reference, reading uninitialized memory, data races, or violating aliasing rules, can corrupt the whole program, not only the unsafe block, because the optimizer assumes UB never happens.
An unsafe function declares preconditions callers must uphold; an unsafe block discharges them. The // SAFETY: comment records the reasoning, and Clippy's undocumented_unsafe_blocks lint can require it.
Soundness and Safe Abstractions
A safe function is sound if no safe caller can trigger UB through it, whatever arguments they pass. Vec, String, and Mutex are built on unsafe code but expose sound APIs. When writing your own:
- Validate inputs at the boundary (lengths, alignment, null pointers, UTF-8).
- Encode invariants in types — lifetimes tie borrowed data to its owner,
PhantomDatamarks ownership,NonNulldocuments non-null pointers. - Keep the unsafe surface private, so all access goes through checked methods.
- Think about
SendandSync: implementing them unsafely for a type that wraps thread-unsafe C state is a common soundness bug.
Rust 2024 Edition Changes
The 2024 edition makes unsafety more explicit: extern blocks must be written unsafe extern (items inside may be marked safe when calling them is always sound), attributes with safety implications are written #[unsafe(no_mangle)] and #[unsafe(export_name = "...")], and the unsafe_op_in_unsafe_fn lint warns by default, so unsafe operations inside an unsafe function need their own unsafe block.
FFI Essentials
When returning Rust-allocated data to C, expose a matching free function (for example built on Box::from_raw or CString::from_raw) instead of letting C call free().
Tooling
- bindgen generates Rust declarations from C headers, usually in a
build.rsof a*-syscrate. - cbindgen generates C headers from Rust code for libraries consumed by C or C++.
- cc compiles bundled C sources in
build.rs; pkg-config finds system libraries. - The convention is a
foo-syscrate with raw bindings and afoocrate with the safe API. See Cargo.
Testing Unsafe Code
Run Miri on the pure-Rust parts in CI, and sanitizers and fuzzing on the FFI boundary.
Best Practices
Minimize and Isolate Unsafe
Keep unsafe blocks as small as possible, inside a module whose public API is entirely safe.
Document Every Invariant
Write // SAFETY: comments for blocks and # Safety sections for unsafe functions, and review them like security-sensitive code.
Make Ownership Explicit at the Boundary
Name functions and document whether each pointer is borrowed, transferred, or must be freed — and by which side.
Test With UB Detectors
Run Miri, sanitizers, and fuzzers in CI; ordinary tests rarely expose undefined behavior.
Common Mistakes
Dangling Pointer From a Temporary CString
Freeing With the Wrong Allocator
Unsound "Safe" Wrappers
FAQ
Does unsafe Rust turn off the borrow checker?
No. Unsafe allows five additional operations, such as dereferencing raw pointers and calling unsafe functions. Borrow checking and other compile-time checks still apply to references and safe code.
What is soundness in Rust?
A safe API is sound when no safe code using it can cause undefined behavior, regardless of the arguments it passes. Unsound safe APIs are bugs, even if no current caller misuses them.
How do I call a C library from Rust?
Declare the functions in an unsafe extern "C" block (or generate declarations with bindgen), link the library through build.rs, and wrap the calls in safe Rust functions that convert types, check results, and enforce ownership.
What happens if a Rust panic crosses into C?
Unwinding out of an extern "C" function aborts the process. Catch panics at the boundary with std::panic::catch_unwind, or use the "C-unwind" ABI when unwinding through foreign frames is intended and supported.
What is Miri?
An interpreter for Rust's mid-level intermediate representation that detects many kinds of undefined behavior in unsafe code during tests. It cannot execute foreign C code, so FFI calls need sanitizers or other tools.
Related Topics
- Rust — The language and its guarantees
- Rust Ownership and Borrowing — The rules unsafe code must uphold
- C — The most common FFI partner
- Memory Management — Allocation, lifetimes, and leaks
- Cargo — Build scripts and sys crates
- Systems Programming — Low-level programming in context
- Zig Allocators, Error Unions, and C Interoperability — C interop and ownership from Zig