Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-119 · Case file with fixtures · Case 91 of 694 · Compiler evidence

Why Rust 2024 Denies References to static mut

Rust 2024 makes static_mut_refs deny-by-default because creating an aliased reference can already violate Rust's rules. Replace globals with atomics or locks, or use raw pointers only behind a proven unsafe protocol.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets; atomic availability depends on target
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
A Rust reference immediately asserts aliasing guarantees that untracked global mutation makes difficult to prove, so static_mut_refs is deny-by-default.
First discriminating check
Expand implicit borrows and replace the global with an atomic, lock, OnceLock, or owned context matching its actual operations.

The failing program reads a static mut by first creating &REQUESTS. Under edition 2024, rustc denies this through the static_mut_refs lint even though the expression sits inside unsafe.

The unsafe block says the programmer accepts responsibility. It does not make the reference validity rules less strict.

Creating the reference is already the dangerous operation

The Rust 2024 Edition Guide explains that shared or mutable references to mutable statics are dangerous and that the lint is deny-by-default in the 2024 edition.

A shared reference promises that the referenced value will not be mutated incompatibly while that reference lives. A mutable reference promises exclusive access. A global static mut can be reached elsewhere without the compiler tracking ordinary borrowing, so these promises are very hard to establish.

Undefined behavior can begin when an invalid reference is created, not only when a later read observes a race. This is why “I only print it once” is not a sufficient proof.

The mutable static reference requires unsafe access and describes the synchronization responsibility. Edition 2024 makes one particularly risky access shape harder to write by accident.

Choose a primitive matching the operation

The repaired program uses AtomicU64 for a counter. It loads with Ordering::Relaxed because the fixture only observes the counter value and does not use it to publish other memory.

This ordering is not a universal replacement. If the counter establishes ordering for associated data, stronger semantics or a lock may be needed.

I choose based on the state transition:

  • atomics for small operations with a documented memory-ordering contract;
  • Mutex for exclusive multi-step updates;
  • RwLock for a measured read-heavy shared structure;
  • OnceLock for one-time initialization;
  • thread-local storage when the state need not be shared.

The wrapper is part of the design. static Mutex<State> tells readers how access is synchronized; static mut State leaves that protocol outside the type system.

Raw pointers are not a generic escape

The compiler may suggest &raw const to create a raw pointer without first creating a reference. This is important for low-level code where reference creation itself would be invalid.

A raw pointer does not synchronize access or guarantee alignment, initialization, aliasing, or lifetime. Dereferencing it later still needs an unsafe proof. I use raw pointers only when the surrounding protocol—hardware, FFI, interrupt masking, or a custom synchronization primitive—defines those facts.

Replacing &STATIC with &raw const STATIC and immediately turning it back into &*pointer recreates the same unproven reference.

Migration can reveal implicit references

References can be created by syntax that does not display & clearly. Formatting macros, method calls, indexing, and field operations may auto-borrow. The Edition Guide calls out these cases.

I let the migration lint identify locations, then reduce each one. Copying a Copy value with a raw read may fit a proven single-threaded bootstrap. Most application globals should move to safe synchronization instead.

Adding #[allow(static_mut_refs)] keeps risk and loses the migration's value. I reserve an allow for audited low-level code with a tracked reason and tests on its concurrency boundary.

Global mutable state needs a lifecycle

Synchronization prevents data races but does not define initialization, shutdown, or reset. Test suites are especially sensitive because globals persist across tests in the same process.

I document when the value becomes available, who updates it, whether reads can fail, and how tests isolate it. Often passing an owned context or Arc<State> into components is simpler than a global.

For libraries, hidden process-global state can make multiple consumers interfere. The 2024 error is an opportunity to reconsider scope, not only syntax.

My debugging sequence

When edition 2024 rejects a static-mut reference, I do this:

  1. Expand the expression to see where an implicit reference is created.
  2. Write down the concurrency and initialization protocol.
  3. Replace the global with an atomic, lock, OnceLock, or owned context when possible.
  4. Choose atomic ordering from the communicated memory facts, not habit.
  5. Use raw pointers only when references are genuinely inappropriate and the unsafe protocol is documented.
  6. Test races, initialization, shutdown, and repeated test execution.

The edition change makes an old hidden promise visible. The durable fix moves that promise into a synchronization type or a narrow unsafe boundary that can actually justify every access.