Rust 94 🦀 Accessing or Modifying Mutable Static State
A static mut is a global variable with a fixed memory address that can be modified at runtime. The Rust compiler requires an unsafe block for any read or write because it cannot guarantee that the access is free of data races or aliasing violations . The 2024 edition goes further: taking a reference to a static mut—even without dereferencing it—is a hard error, because the mere existence of a reference that could outlive a mutation is undefined behavior . The alternatives are interior mutability types like Mutex, OnceLock, or atomics, which provide safe access without requiring the programmer to reason about global aliasing.
Key point: In the 2024 edition, &STATIC_MUT and &mut STATIC_MUT are compile errors. The only way to access the value is through a raw pointer obtained via addr_of! or addr_of_mut!, or by migrating to SyncUnsafeCell or a synchronization primitive.
Why mutable statics are dangerous
The aliasing problem. Rust’s borrow checker enforces that at any time, a value has either one mutable reference or any number of shared references. A static mut violates this because any code in the program can access it at any time, including from multiple threads. Taking a reference to it creates a 'static lifetime reference that the compiler cannot constrain, and if the value is mutated while the reference is alive, the reference becomes invalid. This is undefined behavior even if the reference is never read .
The data race problem. Two threads can read and write a static mut concurrently without synchronization. The compiler cannot insert synchronization because the access is marked unsafe. The result is a data race, which is undefined behavior in Rust regardless of the target architecture .
The global reasoning problem. Upholding the aliasing rules for a static mut requires reasoning about every access to the variable across the entire program. In a large codebase with reentrancy, threads, and signal handlers, this reasoning is intractable. The 2024 edition’s ban on references exists because the alternative—allowing references but hoping the programmer reasons correctly—has proven to be unsound in practice .
a. The 2024 Edition: References Are Hard Errors
Before the 2024 edition, taking a reference to a static mut produced a warning (static_mut_refs) but compiled. The 2024 edition makes it a hard error (E0796) that cannot be disabled .
static mut COUNTER: i32 = 0;
fn main() {
// ❌ E0796 in 2024 edition: creating a shared reference to a mutable static
// let c = unsafe { &COUNTER };
// ❌ E0796: creating a mutable reference to a mutable static
// let c = unsafe { &mut COUNTER };
}
The error applies even if the reference is immediately discarded. The mere act of creating a reference asserts an aliasing guarantee that the compiler cannot verify, and that guarantee is violated the moment any other code accesses the static .
The migration path is addr_of! and addr_of_mut!, which create raw pointers without creating intermediate references. The raw pointer can be dereferenced inside the unsafe block, and the safety reasoning is localized to the dereference rather than spread across every reference creation .
use std::ptr::addr_of_mut;
static mut COUNTER: i32 = 0;
fn increment() {
// ✅ Create raw pointer, then dereference
let ptr = unsafe { addr_of_mut!(COUNTER) };
// SAFETY: ptr is valid, and this is the only access at this moment.
unsafe { *ptr += 1; }
}
b. The Standard Alternatives: Mutex, OnceLock, and Atomics
The Rust documentation recommends replacing static mut with an immutable static containing an interior mutability type whenever possible . The choice depends on the access pattern.
Mutex provides mutual exclusion. It is the general-purpose choice when the value can be read and written from multiple threads and the access needs to be serialized. The Mutex::new() constructor is const, so it can be used in a static initializer.
use std::sync::Mutex;
static COUNTER: Mutex<i32> = Mutex::new(0);
fn increment() {
let mut guard = COUNTER.lock().unwrap();
*guard += 1;
}
The lock is a runtime cost on every access. If the value is written only during initialization, OnceLock is more efficient because it avoids the lock after the first write .
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
fn get_config() -> &'static Config {
CONFIG.get_or_init(|| Config::load())
}
For simple types like integers, booleans, and pointers, atomics provide lock-free access with explicit memory ordering. An AtomicUsize can be incremented from any thread without a mutex.
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::Relaxed);
}
The Relaxed ordering is sufficient when the only requirement is that the increment is atomic, not that it synchronizes with other memory operations. For flags that gate access to other data, Acquire and Release are required.
c. SyncUnsafeCell: The Migration Target for FFI
The RFC that deprecates static mut proposes SyncUnsafeCell<T> as the equivalent type for cases where interior mutability with unsafe access is genuinely required, such as FFI globals . SyncUnsafeCell is UnsafeCell with a Sync implementation, allowing it to be placed in an immutable static. Accessing the value requires unsafe, but creating the pointer is safe, which isolates the unsafe operation to the dereference.
use std::cell::SyncUnsafeCell;
static GLOBAL: SyncUnsafeCell<i32> = SyncUnsafeCell::new(0);
fn set_global(value: i32) {
// SAFETY: caller ensures no concurrent access.
unsafe { *GLOBAL.get() = value; }
}
The distinction is that SyncUnsafeCell makes the static itself immutable. The unsafety is moved from the declaration to the access, and the access is a single dereference rather than a reference creation. This is the pattern the RFC recommends for code that cannot migrate to Mutex or OnceLock .
Complete Example Session
// ============================================
// PART 1: STATIC MUT — THE PROBLEM
// ============================================
static mut COUNTER: i32 = 0;
fn increment_unsafe() {
// SAFETY: single-threaded, no concurrent access.
unsafe {
COUNTER += 1;
}
}
// ============================================
// PART 2: 2024 EDITION ERROR — REFERENCES
// ============================================
// ❌ Compile error in 2024 edition
// unsafe { let c = &COUNTER; }
// ============================================
// PART 3: RAW POINTER WORKAROUND
// ============================================
use std::ptr::addr_of_mut;
fn increment_raw() {
let ptr = unsafe { addr_of_mut!(COUNTER) };
// SAFETY: ptr is valid and this is the only access.
unsafe { *ptr += 1; }
}
// ============================================
// PART 4: MUTEX — GENERAL PURPOSE
// ============================================
use std::sync::Mutex;
static COUNTER_MUTEX: Mutex<i32> = Mutex::new(0);
fn increment_mutex() {
let mut guard = COUNTER_MUTEX.lock().unwrap();
*guard += 1;
}
// ============================================
// PART 5: ONCELOCK — WRITE ONCE
// ============================================
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
fn config() -> &'static String {
CONFIG.get_or_init(|| "default".to_string())
}
// ============================================
// PART 6: ATOMIC — LOCK-FREE
// ============================================
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNT: AtomicUsize = AtomicUsize::new(0);
fn increment_atomic() {
COUNT.fetch_add(1, Ordering::Relaxed);
}
// ============================================
// PART 7: SYNCUNSAFECELL — FFI MIGRATION
// ============================================
use std::cell::SyncUnsafeCell;
static GLOBAL: SyncUnsafeCell<i32> = SyncUnsafeCell::new(0);
fn write_global(v: i32) {
// SAFETY: external synchronization guarantees exclusive access.
unsafe { *GLOBAL.get() = v; }
}
// ============================================
// PART 8: FFI STATIC MUT
// ============================================
unsafe extern "C" {
static mut ERROR_CODE: i32;
}
fn read_error() -> i32 {
// SAFETY: the C library initializes ERROR_CODE before use.
unsafe { ERROR_CODE }
}
// ============================================
// PART 9: ATOMIC BOOL FLAG
// ============================================
use std::sync::atomic::AtomicBool;
static READY: AtomicBool = AtomicBool::new(false);
fn set_ready() {
READY.store(true, Ordering::Release);
}
fn is_ready() -> bool {
READY.load(Ordering::Acquire)
}
// ============================================
// PART 10: MIGRATION CHECKLIST
// ============================================
// 1. Can the value be immutable? → static, no mut
// 2. Written once at init? → OnceLock
// 3. Simple integer/bool? → Atomic
// 4. Read/write with contention? → Mutex
// 5. FFI global with unsafe access? → SyncUnsafeCell
// 6. None of the above? → addr_of_mut! with raw pointer
The ten parts trace the problem, the 2024 edition error, and the six migration targets. Part 3 shows the raw pointer workaround. Parts 4 through 6 show the safe alternatives. Part 7 shows the FFI-specific migration target.
Quick Reference
Access Mechanisms
| Mechanism | Safety | Use Case |
|---|---|---|
static mut | unsafe | Legacy; avoid |
addr_of_mut! | unsafe deref | Migration path |
Mutex<T> | Safe | General read/write |
OnceLock<T> | Safe | Write once, read many |
AtomicT | Safe | Lock-free integer/bool |
SyncUnsafeCell<T> | unsafe deref | FFI globals |
2024 Edition Changes
| Operation | 2021 | 2024 |
|---|---|---|
&STATIC_MUT | Warning | Hard error (E0796) |
&mut STATIC_MUT | Warning | Hard error (E0796) |
addr_of!(STATIC_MUT) | Allowed | Allowed |
*addr_of_mut!(STATIC_MUT) | unsafe | unsafe |
Static vs Const
| Item | Memory Location | Mutable | Initializer |
|---|---|---|---|
const | Inlined | No | Compile-time |
static | Fixed address | No | Compile-time |
static mut | Fixed address | Yes (unsafe) | Compile-time |
Best Practices
✅ Do This:
// Use Mutex for general mutable global state
static STATE: Mutex<State> = Mutex::new(State::new()); // ✅
// Use OnceLock for write-once initialization
static CONFIG: OnceLock<Config> = OnceLock::new(); // ✅
// Use Atomic for simple counters and flags
static COUNT: AtomicUsize = AtomicUsize::new(0); // ✅
// Use addr_of_mut! for the migration from static mut
let ptr = unsafe { addr_of_mut!(COUNTER) }; // ✅
❌ Don’t Do This:
// Take a reference to a static mut in 2024 edition
let c = unsafe { &COUNTER }; // E0796 // ❌
// Access static mut from multiple threads without synchronization
unsafe { COUNTER += 1; } // data race // ❌
// Assume unsafe makes static mut safe
// It only makes it compile; correctness is still your responsibility // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| E0796 in 2024 edition | Reference to static mut | Use addr_of_mut! or interior mutability |
| Data race | Concurrent unsynchronized access | Use Mutex or Atomic |
| Deadlock | Re-entrant Mutex lock | Use try_lock or restructure |
| Stale read with Relaxed | Missing synchronization | Use Acquire/Release |
| FFI global corrupted | C writes while Rust reads | Document the contract; use atomics if possible |
Real-World Examples
1. Atomic Counter
static COUNT: AtomicUsize = AtomicUsize::new(0);
COUNT.fetch_add(1, Ordering::Relaxed);
2. Mutex State
static STATE: Mutex<Vec<String>> = Mutex::new(Vec::new());
let mut v = STATE.lock().unwrap();
3. OnceLock Config
static CONFIG: OnceLock<String> = OnceLock::new();
CONFIG.get_or_init(|| load());
4. SyncUnsafeCell FFI
static GLOBAL: SyncUnsafeCell<i32> = SyncUnsafeCell::new(0);
unsafe { *GLOBAL.get() = 1; }
5. Raw Pointer Migration
let ptr = unsafe { addr_of_mut!(COUNTER) };
unsafe { *ptr += 1; }
6. Atomic Bool Flag
static READY: AtomicBool = AtomicBool::new(false);
READY.store(true, Ordering::Release);
7. FFI Static Mut
unsafe extern "C" { static mut ERRNO: i32; }
8. Relaxed Ordering
COUNTER.fetch_add(1, Ordering::Relaxed);
9. Acquire/Release Pair
READY.store(true, Ordering::Release);
if READY.load(Ordering::Acquire) { ... }
10. Thread-Local Alternative
thread_local! { static LOCAL: Cell<i32> = Cell::new(0); }
Visual
The Static Mut Problem
┌──────────────────────────────────────────────────────────────┐
│ static mut COUNTER: i32 = 0; │
│ │
│ Thread 1 Thread 2 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ &COUNTER │ │ &COUNTER │ │
│ │ (reference) │ │ (reference) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Both references exist simultaneously│ │
│ │ One writes → the other is invalid │ │
│ │ → Undefined Behavior │ │
│ └─────────────────────────────────────┘ │
│ │
│ The compiler cannot prevent this. │
│ The 2024 edition bans references entirely. │
│ │
└──────────────────────────────────────────────────────────────┘
The Interior Mutability Solution
┌──────────────────────────────────────────────────────────────┐
│ static COUNTER: Mutex<i32> = Mutex::new(0); │
│ │
│ Thread 1 Thread 2 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ lock() │ │ lock() │ │
│ │ (acquire) │ │ (blocks) │ │
│ └──────┬──────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Exclusive access guaranteed │ │
│ │ Write is safe │ │
│ │ Unlock → next thread proceeds │ │
│ └─────────────────────────────────────┘ │
│ │
│ The lock enforces the aliasing rule at runtime. │
│ │
└──────────────────────────────────────────────────────────────┘
Migration Decision
┌──────────────────────────────────────────────────────────────┐
│ NEED MUTABLE GLOBAL STATE? │
│ │
│ ┌─────────────────┐ │
│ │ Written once? │────► OnceLock │
│ └────────┬────────┘ │
│ │ NO │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Integer/bool? │────► Atomic │
│ └────────┬────────┘ │
│ │ NO │
│ ▼ │
│ ┌─────────────────┐ │
│ │ General state? │────► Mutex │
│ └────────┬────────┘ │
│ │ NO (FFI) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ FFI global? │────► SyncUnsafeCell or addr_of_mut! │
│ └─────────────────┘ │
│ │
│ static mut is the last resort, not the first choice. │
│ │
└──────────────────────────────────────────────────────────────┘
2024 Edition Transition
┌──────────────────────────────────────────────────────────────┐
│ 2021 EDITION 2024 EDITION │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ &COUNTER │ │ &COUNTER │ │
│ │ (warning) │ │ (hard error) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ addr_of_mut! │ │ addr_of_mut! │ │
│ │ (allowed) │ │ (allowed) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ *ptr │ │ *ptr │ │
│ │ (unsafe) │ │ (unsafe) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ References are banned; raw pointers are the escape hatch. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
static mut access | unsafe for read and write |
| 2024 edition change | References are hard errors (E0796) |
| Raw pointer migration | addr_of_mut! / addr_of! |
| General mutable state | Mutex<T> |
| Write-once state | OnceLock<T> |
| Lock-free simple types | AtomicT |
| FFI migration target | SyncUnsafeCell<T> |
| Static lifetime | 'static, lives for entire program |
| Static drop | Does not run drop |
Key takeaways:
static mutrequiresunsafefor every access. The compiler cannot verify that the access is free of data races or aliasing violations. Theunsafeblock is where the programmer accepts that responsibility .- The 2024 edition bans references to
static mut. Taking&STATICor&mut STATICis a hard error (E0796), even if the reference is never used. The ban exists because the reference’s'staticlifetime makes the aliasing guarantee impossible to uphold . addr_of_mut!is the migration path for code that must keep usingstatic mut. It creates a raw pointer without creating a reference, and the dereference is the onlyunsafeoperation. This localizes the safety reasoning to the dereference rather than the reference creation .- Interior mutability types are the recommended alternative.
Mutexfor general state,OnceLockfor write-once initialization, and atomics for simple counters and flags. These types provide safe access without requiring the programmer to reason about global aliasing . SyncUnsafeCellis the FFI-specific migration target. It allows astaticto contain mutable data withunsafeaccess, and the unsafety is isolated to theget()dereference rather than spread across the declaration .
Remember: static mut is a legacy feature that Rust is actively moving away from. The 2024 edition’s ban on references is the first step; the RFC proposes deprecating the syntax entirely in favor of SyncUnsafeCell . The reason is not that static mut is inherently broken, but that the safety reasoning it requires is global and intractable in large codebases. A Mutex enforces the aliasing rule at runtime. An Atomic enforces it at the hardware level. A SyncUnsafeCell isolates the unsafety to a single dereference. A static mut reference spreads the unsafety across every line of code that touches the variable. The alternatives are not just safer; they are easier to audit.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!