Security and Privacy
Rust Unsafe Boundaries: Review the Assumptions Safe Code Relies On
Review Rust unsafe boundaries by documenting invariants, checking FFI ownership, testing edge cases, and following compiler advisories without overstating safety.
In this article
Rust Unsafe Boundaries: Review the Assumptions Safe Code Relies On
Rust's safety model depends on compiler behavior and on the invariants upheld by unsafe code. An unsafe block is not automatically a vulnerability, but it creates an obligation to justify operations the compiler cannot fully verify. Review those obligations where safe callers cross into unsafe implementations or foreign code.
Reports of compiler soundness issues are a reason to examine evidence and affected configurations, not to declare that all Rust software is unsafe. Likewise, a crate that compiles successfully is not proof that every unsafe abstraction is sound. Separate the language's design, implementation defects, and application-specific misuse.
Start from the trust boundary
Inventory direct unsafe code, unsafe functions, unsafe traits, foreign-function interfaces, and dependencies that expose safe wrappers around unsafe internals. Include generated bindings and platform-specific implementations. The important question is which assumptions a safe caller depends on.
For each boundary, record memory ownership, lifetime, initialization, alignment, aliasing, thread access, and valid input conditions where relevant. Use the appropriate Rust reference and library documentation for exact language rules; avoid relying on an AI-generated explanation of a subtle invariant without review.
The Rust project's security policy describes the official reporting channel. Follow that channel for a suspected project security issue rather than publishing exploit details prematurely. A compiler issue needs a reproducible report and supported assessment, not a headline inferred from a discussion thread.
Put invariants next to the operation
An unsafe block should have a specific explanation of why the operation is valid at that point. “This is safe” is not a useful justification. State the source of the pointer, the allocation's lifetime, the accessible range, and the condition that prevents conflicting access when those are the relevant facts.
Consider a wrapper receiving a pointer and length from a foreign library. The review must establish that the pointer refers to initialized memory for the requested length and remains valid during use. Checking that the pointer is non-null does not prove those other conditions.
Separate caller obligations from implementation guarantees. If a function requires the caller to uphold an invariant, its API should make that requirement explicit. A safe public API must not silently depend on ordinary callers knowing an undocumented memory rule.
Review FFI ownership and error paths
Foreign libraries can allocate, retain, free, or mutate memory under rules different from Rust's normal ownership model. Document who frees each allocation, which allocator is required, and whether a callback can outlive the initiating call.
Test failure cases: null results, partial initialization, early returns, repeated cleanup, cancellation, and callback reentrancy. Bugs often appear when a normal-path ownership diagram is correct but an error path frees the same object twice or leaves a callback pointing at removed state.
Do not assume a foreign function is thread-safe because its Rust wrapper is used from several threads. Check the library's supported concurrency contract and ensure the wrapper preserves it. A mutex around one call may not protect hidden shared state used elsewhere.
Build tests around the invariant
Use tests that exercise empty input, boundary lengths, unusual alignment where appropriate, repeated creation and destruction, and concurrent operations where supported. Fuzzing and dynamic checking can reveal defects, but passing them is not a mathematical proof of soundness.
Keep the test environment controlled. Do not execute an unknown proof-of-concept from an untrusted issue against a production host. Review code, use disposable authorized environments, and avoid providing unrelated secrets or network privileges.
A useful regression test describes the violated invariant, expected behavior, and affected path. It should fail for the old implementation and pass for the corrected one. A test that simply avoids the crashing input can conceal the problem rather than fix it.
Respond carefully to a compiler advisory
Verify the official issue or advisory, affected versions, trigger conditions, and recommended update or workaround. Determine whether the application's compiler, target, features, and code pattern are in scope. Do not infer exposure merely from the programming language.
Rebuild and test with the corrected toolchain when available. Preserve the exact compiler version in build records so deployed artifacts can be assessed later. If a workaround changes semantics or performance, review that tradeoff explicitly instead of accepting it as a harmless syntax change.
For a dependency boundary, identify the fixed crate version and test the wrapper's callers. The related release artifact guide explains why knowing the source commit alone is not enough to identify the compiler used for a shipped build.
Keep claims proportional to evidence
Describe the reviewed boundary, tested conditions, remaining assumptions, and open questions. Avoid “memory-safe therefore unhackable” and its opposite, “one compiler bug invalidates every safety benefit.” Both skip the actual engineering assessment.
Unsafe Rust review is most effective when invariants are explicit, APIs preserve them, and tests target their failure modes. The result is a stronger abstraction that ordinary safe code can rely on—not a blanket promise that no defect can exist.