Skip to main content
Cookies
We use cookies for analytics, marketing and targeting. You can read the privacy policy here.
16.10.2024 | Technology

Unsafe Mode in Rust Programming Language

The popularity of the Rust programming language is constantly growing, manifesting in various courses and blog posts on the subject. However, there is one thing that always catches my attention: the complete misunderstanding of Rust's unsafe feature. So let's go through in this blog post what that scary-sounding keyword actually means and what use cases it has.

How Unsafe Works

If your knowledge of Rust is limited to its syntax, you might easily assume that unsafe removes all of the compiler's famous safety guarantees. Surprisingly, this same misconception can be found in various blog posts about Rust and sometimes even in university courses. However, a diligent beginner Rust developer who has read The Rust Programming Language book already knows that unsafe allows the developer to use five precisely defined operations and does not disable other compiler features. [1]

These 5 operations are:

  • Dereferencing a raw pointer
  • Calling an unsafe function or method
  • Accessing or modifying a mutable static variable
  • Implementing an unsafe trait
  • Accessing fields of a union

The developer must define an unsafe block in the code, within which these operations are possible. The restrictions of this state enable circumventing certain compiler constraints without becoming an easy escape hatch for a beginning Rust programmer.

fn main() {
    // Calling an unsafe function requires an unsafe block
    let x = unsafe { deref_raw_pointer() };
    println!("{x}");
}

// You can use unsafe features inside an unsafe function
unsafe fn deref_raw_pointer() -> i32 {
    let x = 5;
    let raw = &x as *const i32;

    *raw
}

This means that potential memory safety issues can only occur in highly restricted areas of the code. This makes finding these bugs significantly easier both during code review and later, even if the application is already in use. Additionally, "safe" Rust behaves the same way inside an unsafe block as it does outside of it.

fn main() {
    unsafe {
        let mut string1 = String::from("test");
        let copystring = &mut string;
        /*
        The borrow checker does not allow mutable and immutable
        references to the same variable even in unsafe mode.
        */
        println!("{string1} {copystring}");
    }
}

Simultaneous existence of mutable and immutable reference causes compilation errorFigure 1: Compilation error caused by simultaneous existence of mutable and immutable references.

However, the state exists for a reason, and by utilizing unsafe functions and raw pointers, it is possible to cause problems.

fn main() {
    let mut s = String::from("test");
    /*
    By using a separate unsafe function, it's possible to create a
    value pointing to the same memory location by utilizing raw pointers
    */
    let copystring =
        unsafe {
            String::from_raw_parts(s.as_mut_ptr(), s.len(), s.capacity())
        };
    println!("{s} {copystring}");
}
/*
    Both values are dropped and the program crashes because the values
    are at the same memory address
*/

Dropping the same value twice crashes the programFigure 2: Double-freeing a memory location crashes the program.

use std::ptr::null;

fn main() {
    // Creating a raw pointer is not an unsafe operation
    let a: *const i32 = null();

    // Dereferencing a raw pointer requires unsafe mode
    unsafe {
        // Dereferencing a null pointer causes the program to crash
        println!("{}", *a);
    }
}

A pointer with a null value crashes the programFigure 3: Dereferencing a null pointer crashes the program.

Use Cases for Unsafe

So what is unsafe mode useful for? It's important to remember that using unsafe mode is not inherently bad, as long as it's used for a good reason.

Low-Level Programming

In my own experience, the need has arisen in embedded systems, although the ecosystem of embedded rust has improved to a point where you can now easily use ready-made libraries without having to write unsafe code yourself. Unsafe mode is also sometimes necessary in other low-level operations when dealing with memory addresses. These include, for example, device drivers and operating systems. In these scenarios, situations often arise where the Rust compiler cannot guarantee the contents of memory addresses, so unsafe mode is needed to handle them.

Foreign Function Interface

Another use case is the so-called FFI (foreign function interface), which enables interoperability with other programming languages. The Rust compiler cannot guarantee the memory safety of code written in other languages, so they require the use of unsafe mode.

Performance Optimization

Unsafe mode can also be used to eliminate performance bottlenecks. In unsafe mode, it's possible to manually perform performance optimizations that would be difficult to use in "safe" Rust. Currently, for example, using SIMD (single instruction multiple data) instructions requires unsafe mode, as the standard library's SIMD abstractions are still in an experimental phase.

Using Unsafe in Libraries

A typical way to use unsafe mode in Rust is to build a safe API around unsafe blocks. This is especially used in libraries, or crates. Library users can freely use functions whose internal implementation utilizes unsafe mode without having to use unsafe mode in their own code. This is possible because in Rust, you can use unsafe mode in a function or method that is not itself unsafe. Many libraries in Rust's ecosystem, including Rust's standard library, do use unsafe code to some extent, which may not be visible to library users at all. [2].

fn main() {
    let x = raw_pointer_value(); // Does not require an unsafe block
    // Print the value obtained from the unsafe function through a safe API
    println!("{x}");
}

/// This function doesn't need to be unsafe
fn raw_pointer_value() -> i32 {
    let a = unsafe { deref_raw_pointer() }; // Requires an unsafe block

    a // function return value
}

unsafe fn deref_raw_pointer() -> i32 {
    let x = 5;
    let raw = &x as *const i32;

    *raw
}

Summary

Unsafe mode has its time and place, and should not be automatically treated as a bad thing. The strength of unsafe mode is precisely that it allows performing strictly limited operations in clearly marked areas without removing most of the compiler's safety mechanisms. This mode also must be used in certain use cases, such as low-level programming. However, it's good to remember that in most code, unsafe mode is not needed, and its excessive use is not desirable either. This is usually not a problem due to the restrictions of the mode. Automatically considering unsafe code as a bad thing and using it to scare people in various educational materials is not useful and only causes its purpose to be misunderstood.

References

1 Unsafe Rust - The Rust Programming Language --- doc.rust-lang.org. https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html

2 Rust Foundation --- foundation.rust-lang.org. https://foundation.rust-lang.org/news/unsafe-rust-in-the-wild-notes-on-the-current-state-of-unsafe-rust

3 Meet Safe and Unsafe - The Rustonomicon --- doc.rust-lang.org. https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html

Tuomas Rinne
Written by Tuomas Rinne

Did you like this article? Give it a clap!