General information on unsafe
Unsafe operations
Language capabilities can be extended using unsafe code. The full list of these features is given in the Rust reference. Notice the following ones.
- Dereference a raw pointer
- Read or write a mutable or extern static variable
- Read a field of an
union - Implement an
unsafetrait - Declare an
externblock
More examples can be found in nomicon.
These capabilities may be necessary for system programming but they cause the language to lose its safety properties and Undefined Behaviors may happen.
No Undefined Behavior is allowed.
A keyword with two usages
The unsafe keyword is used both for marking unsafety in an API and unlocking unsafety in the implementation.
unsafe marking
Marking with unsafe is a delegation of responsibility with respect to memory safety from the API author to the API user.
The use of this keyword in an API warns the API user about the potential harmful effects of using the API.
- In a function signature (r-unsafe.fn),
unsafemeans that the behavior of the function may lead to UB if the use of the function does not comply with its interface contract (informally described in its documentation). - In a trait declaration (r-unsafe.trait),
unsafemeans that an erroneous implementation of this trait may lead to UB if the implementation contract (preferably documented) is not respected.
unsafe unlocking
Unlocking with unsafe means taking responsibility for memory safety from the compiler to the developer.
Using an unsafe block in a function body or in a constant declaration is imposed by the compiler to prevent the inadvertent use of unsafe capabilities like
- using
unsafetagged functions - modifying static variables
- using extern functions
Similarly, the implementation of an unsafe trait requires unsafe for the developer to explicitly take into account the memory safety contracts. The keyword unsafe unlocks the implementation of unsafe traits.
Lastly, Since the 2024 edition, unsafe is also required to unlock the following:
externblocks, which contain declarations of foreign functions and variables, for FFI,- some attributes (for instance , no_mangle, cf. r-attributes.safety).
Limitations and precautions
Paraphrasing the Rustonomicon, the fundamental principle of Rust can be summed up as follows:
unsafe-free code cannot go wrong
The combined use of the type system and the ownership system enforces a high-level memory safety in Rust programs. This way, the language helps prevent memory overflows, null or invalid pointer constructions, and data races.
This promise is valid only if the code does not use unsafe features. When unsafe features are used, the compiler can no longer guarantee memory safety. The developer must then ensure that the code respects the invariants that guarantee memory safety.
That is why it is crucial to limit the use of unsafe features as much as possible:
In a secure Rust development, the unsafe blocks must be avoided. In the following,
we list the only cases where unsafe may be used, provided that they come
with a proper justification:
-
The Foreign Function Interface (FFI) of Rust allows for describing functions whose implementations are written in C, using the
extern "C"prefix. To use such a function, theunsafekeyword is required. “Safe” wrapper shall be defined to safely and seamlessly call C code. -
For embedded device programming, registers and various other resources are often accessed through a fixed memory address. In this case,
unsafeblocks are required to initialize and dereference those particular pointers in Rust. In order to minimize the number of unsafe accesses in the code and to allow easier identification of them by a programmer, a proper abstraction (data structure or module) shall be provided. -
A function can be marked unsafe globally (by prefixing its declaration with the
unsafekeyword) when it may exhibit unsafe behaviors based on its arguments, that are unavoidable. For instance, this happens when a function tries to dereference a pointer passed as an argument. -
When hitting a performance wall on a small portion of code (E.G: Zero-copy buffer modified in-place, Allocation overhead, etc.).
With the exception of these cases, #![forbid(unsafe_code)] must appear in the crate root (typically main.rs or lib.rs) to generate compilation errors if unsafe is used in the code base.
If the use of unsafe is necessary, it is the responsibility of the developer to:
- ensure that the use of
unsafeunlocking does not lead to UBs, - ensure that any
unsafemarkings are correctly and exhaustively documented so that no UB are possible if the usage conditions (invariants) are respected.
Aside from the unsafe code itself, it is also crucial to properly encapsulate the use of unsafe features in a component (crate or module) so as to restore the usual Rust memory safety guarantees:
In secure development of a Rust software component (crate or module), all unsafe code must be encapsulated in such a way that:
- either it exposes a safe behavior to the user, in which no safe interaction can result in UB (undefined behavior);
- or it exposes features marked as unsafe whose usage conditions (preconditions, sequencing, etc.) are exhaustively documented.
Thus, a function using unsafe operations can be safe if the unsafe
operations do not present any UB (undefined behavior) given the component's
invariants (typically the type invariant for a method). Conversely, a function
without an unsafe block must be marked as unsafe if it breaks these
invariants. The choice and knowledge of these invariants are therefore crucial
for secure development.
Example 1: Preserving a type invariant
The following code comes from the Rustonomicon.
It could be used to implement a custom Vec type.
#![allow(unused)] fn main() { use std::ptr; pub struct Vec<T> { ptr: *mut T, len: usize, cap: usize, } // Note this implementation does not correctly handle zero-sized types. impl<T> Vec<T> { pub fn push(&mut self, elem: T) { if self.len == self.cap { // reallocate new array with bigger capacity } unsafe { ptr::write(self.ptr.add(self.len), elem); self.len += 1; } } } }
Soundness and safety of this code rely on the fact that bytes from address self.ptr to self.ptr + self.cap * size_of<T>() are allocated.
This invariant can be broken with safe code. For instance
#![allow(unused)] fn main() { impl<T> Vec<T> { fn make_room(&mut self) { // grow the capacity self.cap += 1; } } }
This function may be necessary for internal use, but it should not be exposed in the API, or it should be marked with the unsafe keyword, because its use can lead to UB.
Example 2: Trust relationship between safe and unsafe
In the Rust paradigm:
unsafe-free code cannot go wrong
which means it cannot result in UB. This property is lost when developers use unsafe code, so they are responsible for not producing UB in any scenario. Consequently, even safe functions must be handled carefully in unsafe contexts.
Suppose one wants to propose an API to find an object of a given type in memory. This API could require implementing the following trait:
#![allow(unused)] fn main() { trait Locatable { /// Find object of type `Self` in the buffer `buf`. /// Returns the index of the first byte representing /// an object of type `Self` fn locate_instance_into(buf: &[u8]) -> Option<usize>; } fn find<T: Locatable>(buf: &[u8]) -> Option<T> { let start = T::locate_instance_into(buf)?; unsafe { let ptr: *const T = buf.as_ptr().add(start).cast(); Some(ptr.read_unaligned()) } } }
This trait can be implemented without using unsafe.
For instance, the bool type can implement this trait as follows:
impl Locatable for bool {
fn locate_instance_into(buf: &[u8]) -> Option<usize> {
buf.iter().position(|u| *u == 0 || *u == 1)
}
}
This API is harmful for two reasons:
- If the
Locatableimplementation does not give the index of an object of typeT, theread_unalignedmay produce UB. - If the
Locatableimplementation gives an out-of-bounds index or an index for which part of the object is out of bounds, the subsequent buffer overflow is UB.
For instance, the following Locatable implementation is incorrect, but it is the responsibility of the API author to take it into account.
#![allow(unused)] fn main() { impl Locatable for bool { fn locate_instance_into(buf: &[u8]) -> Option<usize> { buf.iter().position(|u| *u == 0 || *u == 1).map(|n| n + 100) } } }
The following program produces UB.
fn use_locatable() {
let buf = [4, 1, 99];
let located_bool: Option<bool> = find(&buf); // UB here!
println!("{:?}", located_bool)
}
The UB-detecting tool miri reports the following:
$ cargo +nightly miri r --bin overflow
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri runner target/miri/x86_64-unknown-linux-gnu/debug/overflow`
error: Undefined Behavior: in-bounds pointer arithmetic failed: attempting to offset pointer by 101 bytes, but got alloc249 which is only 3 bytes from the end of the allocation
--> src/overflow.rs:16:29
|
16 | let ptr: *const T = buf.as_ptr().add(start).cast();
| ^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
help: alloc249 was allocated here:
--> src/overflow.rs:22:9
|
22 | let buf = [4, 1, 99];
| ^^^
= note: BACKTRACE (of the first span):
= note: inside `find::<bool>` at src/overflow.rs:16:29: 16:52
note: inside `main`
--> src/overflow.rs:23:38
|
23 | let located_bool: Option<bool> = find(&buf); // UB here!
| ^^^^^^^^^^
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error
This example shows that developers using unsafe blocks
cannot assume that safe functions or traits they use are well implemented, and thus must prevent UB in case these safe functions have bad behavior.
If they cannot protect their function against poorly implemented safe functions or traits, they have two options:
- Mark the function they write as
unsafe: thus, it is the user's responsibility to provide correct arguments (by checking theunsafefunction's documentation). - Mark the traits they use as
unsafe: thus, it is the user's responsibility to implement the trait properly (again, by reading the trait documentation).
More examples can be found in rust-book (in the Unsafe Rust chapter) or the nomicon.
References
- The Rust Programming Language (rust-book)