Unsafe Rust

Accessing Mutable Static Variables

A static variable is a value stored at a fixed memory address in the binary's data segment for the entire 'static lifetime of the program. While immutable static items (whose types implement Sync) are safe to read from any thread, a static mut variable is globally mutable without built-in synchronization—making every read or write a potential data race or aliasing violation that requires an unsafe block.

Why static mut Is Unsafe and Discouraged

  1. Multi-threaded data races: If two threads read and write a static mut variable concurrently without external synchronization, the program executes immediate undefined behavior.
  2. Single-threaded aliasing violations: Even in a single-threaded program, taking a shared or mutable reference (&STATIC_VAR or &mut STATIC_VAR) while a nested function call also accesses STATIC_VAR creates two overlapping references—violating Rust's aliasing rules and causing undefined behavior!

Rust 2024 Edition Rule: static_mut_refs Is Denied by Default

In the Rust 2024 edition, creating a shared or mutable reference directly to a static mut item (&COUNTER or &mut COUNTER, including implicit auto-referencing via println!("{}", COUNTER) or COUNTER.push(...)) is rejected by default via the static_mut_refs lint:

// ❌ does not compile
static mut COUNTER: u64 = 0;

fn main() {
    unsafe {
        COUNTER += 1;
        // Taking `&COUNTER` (implicitly done by `println!`) is denied in Rust 2024:
        println!("counter = {}", COUNTER);
    }
}
error: creating a shared reference to mutable static is discouraged
 --> src/main.rs:8:34
  |
8 |         println!("counter = {}", COUNTER);
  |                                  ^^^^^^^ shared reference to mutable static
  |
  = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/static-mut-references.html>
  = note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives
  = note: `#[deny(static_mut_refs)]` on by default

If low-level FFI or bare-metal code must touch a static mut, use the raw borrow operators &raw const and &raw mut or copy the value into a local variable first without creating a reference:

static mut RAW_METRIC: u64 = 0;

fn main() {
    // SAFETY: Single-threaded execution in `main`; we never create `&RAW_METRIC`
    // or `&mut RAW_METRIC` references, using only raw pointers and by-value copy.
    unsafe {
        let ptr: *mut u64 = &raw mut RAW_METRIC;
        ptr.write(ptr.read() + 10);

        let snapshot: u64 = *(&raw const RAW_METRIC);
        println!("RAW_METRIC = {}", snapshot);
    }
}
RAW_METRIC = 10

Safe Alternatives That Replace static mut Completely

In modern Rust, static mut is almost never needed. Use an immutable static holding a Sync interior-mutability type instead:

Use Case Safe static Replacement unsafe Required?
Global integer / boolean counter or flag static COUNT: AtomicU64 = AtomicU64::new(0); No (100% safe)
Write-once global configuration static CFG: OnceLock<Config> = OnceLock::new(); No (100% safe)
Lazily initialized global collection static MAP: LazyLock<HashMap<K, V>> = LazyLock::new(|| ...); No (100% safe)
Mutable global struct or Vec<T> static LIST: Mutex<Vec<String>> = Mutex::new(Vec::new()); No (100% safe)
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};

static REQUEST_COUNT: AtomicUsize = AtomicUsize::new(0);
static AUDIT_TRAIL: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());

fn handle(endpoint: &'static str) {
    REQUEST_COUNT.fetch_add(1, Ordering::Relaxed);
    AUDIT_TRAIL.lock().unwrap().push(endpoint);
}

fn main() {
    handle("/login");
    handle("/profile");

    println!("requests: {}", REQUEST_COUNT.load(Ordering::Relaxed));
    println!("trail:    {:?}", *AUDIT_TRAIL.lock().unwrap());
}
requests: 2
trail:    ["/login", "/profile"]