| |

Rust 94 🦀 Accessing or Modifying Mutable Static State

⏱️ Estimated read: 11 min | |

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

MechanismSafetyUse Case
static mutunsafeLegacy; avoid
addr_of_mut!unsafe derefMigration path
Mutex<T>SafeGeneral read/write
OnceLock<T>SafeWrite once, read many
AtomicTSafeLock-free integer/bool
SyncUnsafeCell<T>unsafe derefFFI globals

2024 Edition Changes

Operation20212024
&STATIC_MUTWarningHard error (E0796)
&mut STATIC_MUTWarningHard error (E0796)
addr_of!(STATIC_MUT)AllowedAllowed
*addr_of_mut!(STATIC_MUT)unsafeunsafe

Static vs Const

ItemMemory LocationMutableInitializer
constInlinedNoCompile-time
staticFixed addressNoCompile-time
static mutFixed addressYes (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

PitfallWhy It HappensFix
E0796 in 2024 editionReference to static mutUse addr_of_mut! or interior mutability
Data raceConcurrent unsynchronized accessUse Mutex or Atomic
DeadlockRe-entrant Mutex lockUse try_lock or restructure
Stale read with RelaxedMissing synchronizationUse Acquire/Release
FFI global corruptedC writes while Rust readsDocument 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

ItemValue
static mut accessunsafe for read and write
2024 edition changeReferences are hard errors (E0796)
Raw pointer migrationaddr_of_mut! / addr_of!
General mutable stateMutex<T>
Write-once stateOnceLock<T>
Lock-free simple typesAtomicT
FFI migration targetSyncUnsafeCell<T>
Static lifetime'static, lives for entire program
Static dropDoes not run drop

Key takeaways:

  • static mut requires unsafe for every access. The compiler cannot verify that the access is free of data races or aliasing violations. The unsafe block is where the programmer accepts that responsibility .
  • The 2024 edition bans references to static mut. Taking &STATIC or &mut STATIC is a hard error (E0796), even if the reference is never used. The ban exists because the reference’s 'static lifetime makes the aliasing guarantee impossible to uphold .
  • addr_of_mut! is the migration path for code that must keep using static mut. It creates a raw pointer without creating a reference, and the dereference is the only unsafe operation. This localizes the safety reasoning to the dereference rather than the reference creation .
  • Interior mutability types are the recommended alternative. Mutex for general state, OnceLock for write-once initialization, and atomics for simple counters and flags. These types provide safe access without requiring the programmer to reason about global aliasing .
  • SyncUnsafeCell is the FFI-specific migration target. It allows a static to contain mutable data with unsafe access, and the unsafety is isolated to the get() 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.

Was this helpful?


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!