How to Access Mutable Statics in Rust

Access mutable statics in Rust by declaring them with `static mut` and wrapping all reads or writes in an `unsafe` block.

When globals bite back

You're building a high-performance logger. Every function in your codebase wants to increment a global hit counter. Passing a reference to that counter through twenty layers of abstraction turns your clean architecture into a dependency injection nightmare. You want a single variable that lives forever and is accessible from anywhere. In Python, you'd write global counter and move on. In Rust, you write static mut COUNTER: u32 = 0; and the compiler immediately blocks you.

Rust allows mutable statics, but it treats them like radioactive material. The language gives you the syntax, but it forces you to wrap every access in unsafe. This isn't bureaucracy. It's a hard boundary against data races and undefined behavior. Accessing a mutable static bypasses the borrow checker entirely. The compiler stops tracking who holds references and who owns data. You take full responsibility for correctness.

Treat static mut like a live grenade. You can hold it, but one slip and everyone gets hurt.

What a mutable static actually is

A static mut is a global variable with a fixed memory address that persists for the entire lifetime of the program. Unlike local variables, it doesn't live on the stack. It lives in the binary's data segment. The operating system allocates this memory when the program loads. The value stays there until the process exits.

The mut keyword makes the value writable. This is where the danger starts. Rust's safety guarantees rely on the borrow checker ensuring that mutable access is exclusive. If you have a mutable reference, no one else can read or write the data. static mut throws this rule out the window. Multiple parts of your code can access the static simultaneously. The compiler cannot prevent two threads from writing at the same time. It cannot prevent a read from happening while a write is in progress.

Think of a static mut as a whiteboard in a public hallway. Anyone can walk up and write on it. If two people write at the same time, the text gets garbled. If someone erases the board while another person is reading it, the message is lost. Rust usually locks the whiteboard and hands out keys. static mut removes the lock and throws away the keys.

You can store any type in a static mut, but complex types cause headaches. If you store a String or a Vec, who is responsible for dropping them? The static lives forever. The value never goes out of scope. If you replace the value, the old value leaks unless you manually drop it. Manual drop logic in globals is error-prone. The community convention is to keep static mut values simple: primitives, pointers, or types that implement Copy. If you need complex state, you're probably using the wrong tool.

Memory doesn't clean itself up. If you put a complex type in a static mut, you own the cleanup.

The minimal syntax

The syntax is straightforward, but the unsafe requirement is mandatory. You declare the static with static mut. You access it inside an unsafe block. The compiler enforces this boundary strictly.

static mut COUNTER: u32 = 0;

fn main() {
    unsafe {
        // SAFETY: This is the main thread. No other code runs concurrently.
        // We are the only accessor, so there is no data race.
        COUNTER += 1;
        println!("{COUNTER}");
    }
}

The unsafe block tells the compiler, "I have verified the safety invariants myself." The compiler trusts you. If you lie, the program exhibits undefined behavior. The // SAFETY: comment documents the proof. In this minimal case, the proof is simple: single-threaded execution means no concurrent access.

Convention aside: always write // SAFETY: comments for unsafe blocks. These comments are for future maintainers, including future you. They must state the invariants that make the block safe. If you can't write the comment, you don't have a safety proof.

What happens under the hood

When you compile code with a static mut, the compiler reserves memory in the BSS or data segment. The address is fixed. Every access to the static resolves to that address. There is no ownership transfer. There is no move semantics. The value sits there until the process dies.

If you try to access a static mut without an unsafe block, the compiler rejects you with E0133 (dereference of raw pointer requires unsafe). The compiler treats accessing a mutable static like dereferencing a raw pointer. Both operations can cause data races. Both require unsafe.

The compiler also makes assumptions about your code based on the absence of data races. If you have a data race in a static mut, the compiler might optimize away reads or reorder instructions in ways that break your logic. The compiler assumes that static mut accesses are properly synchronized. If they aren't, the generated machine code can do anything. It might crash. It might corrupt memory silently. It might appear to work in debug mode and fail in release mode.

Undefined behavior doesn't care about your intentions. It breaks reality.

Wrapping the danger

Using static mut directly everywhere is a bad pattern. It scatters unsafe blocks throughout your codebase. It makes reasoning about safety nearly impossible. The correct approach is to isolate the unsafe access in a small wrapper function. The wrapper exposes a safe API. Callers use the safe API and never see the unsafe.

This follows the "minimum unsafe surface" rule. You want the smallest possible amount of unsafe code. The rest of your codebase stays safe. If the wrapper is correct, the callers are safe.

use std::cell::Cell;

// Cell provides interior mutability for Copy types.
// The static is still unsafe to access, but Cell lets us
// mutate the value without borrowing.
static mut GLOBAL_STATS: Cell<u32> = Cell::new(0);

/// Returns the current global stats counter.
///
/// This function is safe because it accesses the static
/// in a way that respects single-threaded exclusivity.
fn get_stats() -> u32 {
    unsafe {
        // SAFETY:
        // 1. This function is only called from the main thread.
        // 2. No other code accesses GLOBAL_STATS concurrently.
        // 3. Cell::get does not create a reference, so no aliasing occurs.
        GLOBAL_STATS.get()
    }
}

/// Increments the global stats counter by one.
///
/// This function is safe because it accesses the static
/// in a way that respects single-threaded exclusivity.
fn bump_stats() {
    unsafe {
        // SAFETY:
        // 1. This function is only called from the main thread.
        // 2. No other code accesses GLOBAL_STATS concurrently.
        // 3. Cell::set does not create a reference, so no aliasing occurs.
        GLOBAL_STATS.set(GLOBAL_STATS.get() + 1);
    }
}

fn main() {
    bump_stats();
    println!("Stats: {}", get_stats());
}

The wrapper uses Cell to avoid creating references. Cell allows mutation without borrowing. This is crucial for globals. If you created a mutable reference to the static, you'd need to ensure that reference is exclusive. Cell sidesteps this by storing the value by copy. The get and set methods copy the value in and out. No references are created. No borrow checker rules are violated.

The // SAFETY: comments list the invariants. The invariants here are single-threaded access and no aliasing. If you call these functions from multiple threads, the invariants break. The code becomes undefined behavior. The wrapper doesn't magically make multi-threaded access safe.

Wrap the danger. Expose only the safe interface. If the wrapper leaks unsafe, you've failed.

Pitfalls and gotchas

Mutable statics are a minefield. The most common pitfall is data races. If two threads access a static mut and at least one writes, you have a data race. Data races are undefined behavior. The program can crash, corrupt memory, or produce wrong results. Debugging undefined behavior is notoriously difficult. The bug might only appear under heavy load. It might only appear in release builds.

Another pitfall is the Sync trait. static mut T is never Sync, even if T is Sync. This means you cannot share a static mut across threads safely, even if you wrap it in a Mutex. The access to the static itself is unsafe. You cannot create a shared reference to a static mut. This breaks many standard patterns. If you need thread-safe global state, use a static (immutable) with a Mutex or RwLock inside. The immutable static holds the synchronization primitive. The primitive protects the data. This pattern is safe and idiomatic.

Initialization order is another trap. Statics are initialized when the binary loads. If you have multiple statics that depend on each other, the order is not guaranteed. You might read a static before it's initialized. This leads to null pointer dereferences or garbage values. The compiler cannot catch this. You have to manage initialization order manually.

Convention aside: name static mut variables in UPPER_SNAKE_CASE. This matches the convention for constants and immutable statics. It signals to readers that this is a global. It also helps linters and formatters. cargo fmt formats everything consistently. Don't argue style; argue logic.

Data races don't just crash your program. They corrupt memory silently. Assume the worst.

When to use mutable statics

Mutable statics have a place in Rust, but that place is small. You should reach for them only when safe abstractions cannot solve the problem. The decision matrix below helps you choose the right tool.

Use static mut for FFI callbacks where C code writes to a global buffer you control. The C runtime doesn't understand Rust's ownership. You need a global address that C can write to. Wrap the access in unsafe and document the synchronization requirements.

Use static mut when you are implementing a low-level allocator and need a global pointer that the compiler cannot track. Allocators often need global state for heap management. The compiler's borrow checker cannot model the allocator's invariants. static mut lets you manage the state manually.

Use Mutex when multiple threads need to read and write shared state safely. Mutex provides synchronization. It prevents data races. It integrates with the borrow checker. You can wrap a Mutex in an immutable static. This is the standard pattern for thread-safe globals.

Use OnceLock when you need lazy initialization of a global value that is read-only after setup. OnceLock ensures the value is initialized exactly once. It is thread-safe. It is safe to use from multiple threads. It avoids the complexity of static mut.

Use AtomicU32 when you need a global counter updated by multiple threads without locking overhead. Atomic types provide lock-free operations. They are safe to share across threads. They are faster than Mutex for simple counters. They avoid the complexity of static mut.

If you can use OnceLock, use OnceLock. static mut is the escape hatch, not the door.

Where to go next