unsafe, raw pointers and undefined behaviour
Safe Rust checks every access at compile time, and a few operations cannot be checked that way: reading through a raw pointer, skipping a bounds check, building a slice from parts. Those operations live behind unsafe, which is a promise from the programmer that the rules the compiler cannot verify still hold.
Coming from C++? Undefined behaviour returns the address of a local in both languages and shows the warning g++ prints, the refusal rustc gives and the unsafe block that compiles anyway.
This lesson covers the three operations that need the block, the contract each one carries, and the error for using one outside it. The unsafe block does not turn off the borrow checker or the type system; it only unlocks the operations that need a proof the programmer supplies.
A raw pointer
*const T is a raw pointer: it may be null, dangling or unaligned, and dereferencing it is unsafe. It is created from a reference with &value and read inside the block.
The program creates one from a local integer and prints the value behind it. The block is small and the pointer never leaves it, which is the safe way to use the operation.
fn main() {
let value = 42;
let pointer: *const i32 = &value;
unsafe {
println!("{}", *pointer);
}
}
rustc --edition 2024 raw.rs
./raw
42
The reference lives longer than the block, so the pointer is valid for the read. The lesson on borrowing explains the lifetime that makes it valid.
A mutable pointer
*mut T writes through the pointer, and the aliasing rules still apply: the block promises that no other reference touches the value meanwhile. The compiler does not check that part.
The program increments a value through a raw pointer and prints the result. The output is two.
fn main() {
let mut value = 1;
let pointer: *mut i32 = &mut value;
unsafe {
*pointer += 1;
}
println!("{value}");
}
rustc --edition 2024 mutate.rs
./mutate
2
A raw pointer can be copied freely, unlike a reference, which is why it is used in data structures that share nodes. The lesson on Box shows the safe ownership those structures usually build on.
An unchecked access
get_unchecked reads an element without the bounds check. It is unsafe because the caller must know the index is in range; when it is not, the read is undefined behaviour rather than a panic.
The program reads index one of a three-element array and prints twenty. The index is a constant the reader can check by eye, which is the only kind of index this operation should receive.
fn main() {
let values = [10, 20, 30];
let index = 1;
let value = unsafe { *values.get_unchecked(index) };
println!("{value}");
}
rustc --edition 2024 unchecked.rs
./unchecked
20
The checked form, values[index], panics on a bad index and costs one comparison. The lesson on arrays shows the panic that the unchecked call replaces with undefined behaviour.
A slice from its parts
from_raw_parts builds a slice from a pointer and a length. The caller promises that the memory is valid for the length, aligned and not aliased by a mutable reference at the same time.
The program builds a mutable slice over an array, writes the second element and prints the array. The output is the changed array.
fn main() {
let mut values = [1, 2, 3];
let slice = unsafe { std::slice::from_raw_parts_mut(values.as_mut_ptr(), values.len()) };
slice[1] = 20;
println!("{values:?}");
}
rustc --edition 2024 from_parts.rs
./from_parts
[1, 20, 3]
The slice borrows the array for as long as the binding lives, which the type system still tracks. The unsafe part is only the construction, which is why the block is as short as possible.
The contract of unsafe
The block does not disable the checks; it moves the proof from the compiler to the programmer. Every unsafe operation documents what it requires, and breaking the requirement is undefined behaviour, which means the optimiser may do anything at all.
A sound wrapper keeps the block private and exposes a safe function whose signature cannot be misused. The standard library is built that way: Vec and String contain unsafe code behind interfaces that are safe to call.
The last section is the compiler refusing a dereference outside the block. The error is E0133, and it is the one place where the language asks for the keyword.
Without the block
The same dereference outside an unsafe block is E0133, with a note that lists what a raw pointer may be. The fix is the block, not a cast, and the note is the contract in one sentence.
The program below prints through a raw pointer in safe code. The compiler stops at the dereference.
fn main() {
let value = 42;
let pointer: *const i32 = &value;
println!("{}", *pointer);
}
rustc --edition 2024 unsafe_err.rs
error[E0133]: dereference of raw pointer is unsafe and requires unsafe block
--> unsafe_err.rs:4:20
|
4 | println!("{}", *pointer);
| ^^^^^^^^ dereference of raw pointer
|
= note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0133`.
The note names the three ways a raw pointer can be invalid and the aliasing rule it can break. The lesson on pointer arithmetic continues with the operations that move a pointer.
Exercises
- Create a raw pointer to an array element and read it without the block; read the error.
- Write a function that takes a slice and returns its second element through get_unchecked, with a comment that states the requirement.
- Build a slice from a Vec's pointer and length and print its values.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does an unsafe block change?
Answer
It allows operations the compiler cannot check
Only the listed operations are unlocked; the rest of the checks still run.
-
What is a raw pointer allowed to be?
Answer
Null, dangling or unaligned
The type carries no validity guarantee, which is why the dereference is unsafe.
-
What does get_unchecked skip?
Answer
The bounds check
The caller promises the index is in range, and a wrong index is undefined behaviour.
-
What does undefined behaviour mean for the optimiser?
Answer
It may assume the situation never happens
The optimiser is allowed to do anything once the program breaks the contract.
-
What does this program print?
fn main() { let value = 42; let pointer: *const i32 = &value; unsafe { println!("{}", *pointer); } }Answer
42
The pointer points at a live local, and the unsafe block allows the read.
-
What does this program print?
fn main() { let mut value = 1; let pointer: *mut i32 = &mut value; unsafe { *pointer += 1; } println!("{value}"); }Answer
2
The write goes through the raw pointer into the local variable.
-
What does this program print?
fn main() { let values = [10, 20, 30]; let index = 1; let value = unsafe { *values.get_unchecked(index) }; println!("{value}"); }Answer
20
Index one is in range, and the unchecked read returns the second element.
-
What does this program print?
fn main() { let mut values = [1, 2, 3]; let slice = unsafe { std::slice::from_raw_parts_mut(values.as_mut_ptr(), values.len()) }; slice[1] = 20; println!("{values:?}"); }Answer
[1, 20, 3]
The slice writes the second element, and the array shows the change.
-
What is the first line of the compiler output for this program?
fn main() { let value = 42; let pointer: *const i32 = &value; println!("{}", *pointer); }Answer
error[E0133]: dereference of raw pointer is unsafe and requires unsafe block
The dereference is one of the operations that need the block, and the note explains why.
-
How is a sound wrapper around unsafe code written?
Answer
A safe function whose contract makes misuse impossible
The block stays private and the public signature prevents an invalid call.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev