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

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:

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

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

References