What does it mean to write unsafe code in Rust, and what can you do (and not do) with the 'unsafe' keyword? The facts may surprise you.
Reasons abound for Rustโs growing popularity: itโs fast, memory-safe without needing garbage collection, and outfitted with world-class tooling. Rust also allows experienced programmers to selectively toggle off someโalthough not allโof its safeties for the sake of speed or direct low-level memory manipulation.
โUnsafeโ Rust is the general term for any Rust code that is contained in a block delineated by the unsafe keyword. Inside an unsafe block, you can bend (but not break) some (but not all) of Rustโs safety rules.
If youโve come from C or another low-level systems language, you may be tempted to reach for unsafe whenever you want to use some familiar pattern for manipulating things in a low-level way. In some cases, you may be right: there are a few things you canโt do in Rust except through unsafe code. But in many cases you donโt actually need unsafe. Rust already has you covered if you know where to look.
In this article, weโll explore what unsafe Rust is actually for, what it can and canโt do, and how to use it sensibly.
What you can do with โunsafeโ Rust
The unsafe keyword in Rust lets you delineate a block of code, or function, to enable a specific subset of features in the language. Letโs look at the features you can access inside Rustโs unsafe blocks.
Raw pointers
Rustโs raw pointers can refer to mutable or immutable values and are closer to Cโs idea of a pointer than Rustโs references. With them, you can ignore some of the rules for how borrowing works:
- Raw pointers can be null values.
- Multiple raw mutable pointers can point to the same space in memory.
- You can also use both immutable and mutable pointers to refer to the same memory.
- You donโt need to guarantee that a raw pointer points to a valid region of memory.
Raw pointers are useful if you need to do things like access hardware directly (e.g., for a device driver), or talk to an application written in another language by way of a raw region of memory.
External function calls
Another common use of unsafe is to make calls through a foreign function interface, or FFI. Thereโs no guarantee that what we get back from such a call will follow Rustโs rules, and thereโs also a chance we will need to supply things that donโt conform to those rules (such as a raw pointer).
Consider this example (from Rustโs documentation):
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
Any calls made to the functions exposed via the extern "C" block must be wrapped in unsafe, the better to ensure you take proper responsibility for what you send to it and get back from it.
Altering mutable static variables
Global or static variables in Rust can be set to mutable, since they occupy a fixed memory address. However, itโs only possible to modify a mutable static variable inside an unsafe block.
Data races are the biggest reason you need unsafe to alter mutable static variables. Youโd get unpredictable results if you allowed the same mutable static variable to be modified from different threads. So, while you can use unsafe to make such changes, any data race issues would be your responsibility, not Rustโs. In general, Rust cannot entirely prevent data races, but you need to be doubly cautious about that in unsafe blocks.
Creating unsafe methods and traits
Methods (functions) can be made unsafe with the declaration unsafe fn <function_name>(). Youโd use this to ensure that any call to such a method must also be performed inside an unsafe block.
For instance, if you had a function that required a raw pointer as an argument, youโd want to ensure the caller did their due diligence for how the call was performed in the first place. Any safeties donโt begin and end at the function-call boundary.
You can also declare traits unsafe, along with their implementations, using a similar syntax: unsafe trait <trait_name> for the trait, and unsafe impl <trait_name> for the implementation.
Unlike an unsafe method, though, an unsafe trait implementation does not have to be called inside an unsafe block. The burden of safety is on the one writing the implementation, not the one calling it.
Unions
Unions in Rust are essentially the same as unions in C: a struct that has multiple possible type definitions for its contents. This kind of loosey-goosey behavior is acceptable in C, but Rustโs sterner promises of correctness and safety donโt allow it by default.
However, sometimes you need to create a Rust structure that maps to a C union, such as when you call into a C library to work with a union. To do this, you need to use unsafe to access one particular field definition at a time.
Hereโs an example from the Comprehensive Rust guide:
#[repr(C)]
union MyUnion {
i: u8,
b: bool,
}
fn main() {
let u = MyUnion { i: 42 };
println!("int: {}", unsafe { u.i });
println!("bool: {}", unsafe { u.b }); // Undefined behavior!
}
For each access to the union, you have to use unsafe. The borrow checker also requires borrowing all the fields of a union even if you just want to access one of them at a time.
Note that writing to a union does not have the same restrictions: in Rustโs eyes youโre not writing to anything that needs tracking. Thatโs why you donโt need unsafe when defining the contents of the union with the let statement.
What you cannot do with Rust โunsafeโ
Outside of the four big points listed above, unsafe doesnโt give you any other special powers.
The single biggest thing you canโt do is use unsafe to circumvent the borrow checker. Borrows are still enforced on values in unsafe, same as anyplace else. One of Rustโs truly immutable principles is how borrowing and references work, and unsafe does not alter those rules.
For a good example of this, look at the way borrowing is still enforced in unions, as described in the previous section. See Steve Klabnikโs blog post for a more detailed discussion on this topic. Itโs better to think of unsafe as a superset of Rustโsomething that adds a few new features without taking away existing ones.
Best practices with unsafe code
unsafe is like any other language feature: it has its uses and limitations, so use it judiciously and with care. Here are some pointers.
Wrap as little code as possible in โunsafeโ
The smaller an unsafe block is, the better. In many cases, you donโt need to make unsafe blocks more than a couple of lines. Itโs worth thinking about how much of the code actually needs to be unsafe, and how to enforce boundaries and interfaces around that code. Itโs best to have safe interfaces to code thatโs not safe.
Sometimes the interfaces themselves have to be unsafe. I mentioned earlier that you can declare an entire function as unsafeโfor example, unsafe fn <function_name>(). If an argument to a function requires unsafe, any calls to such a function also must be unsafe.
On the whole, though, itโs best to start with individual unsafe blocks, and promote them to functions only if needed.
Be mindful of undefined behaviors
Undefined behaviors exist in Rust, and so they also exist in unsafe Rust. For instance, Rustโs basic safeties donโt guard against data races, or reading from uninitialized memory. Be extra careful of how you might be introducing undefined behavior in your unsafe blocks.
For examples of what to avoid, the Rust Reference has a long but not exhaustive list of undefined behavior.
Document why โunsafeโ is needed
Itโs been said that we comment code not to say what is being done, but why. That idea absolutely applies to unsafe blocks.
Whenever possible, document exactly why unsafe is needed at any given point. This gives others some idea of the rationale behind the use of unsafe, andโpotentiallyโfuture insight into how the code could be rewritten to not need unsafe.
Read the Rustonomicon
Rustโs documentation is second to none. That includes the Rustonomicon, an entire book-length document created to thoroughly document โall the awful details that you need to understand when writing Unsafe Rust programs.โ Donโt open it until you are comfortable with Rust generally, as it delves into great detail about how โunsafeโ and โregularโ Rust interoperate.
If youโre coming in from the C side of things, another useful document is Learn Rust The Dangerous Way. This collection of articles talks about Rust in a way that people used to low-level C programming can wrap their heads around, including best practices for using Rustโs unsafe feature.


