Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-555 · Case file with fixtures · Case 527 of 694 · Compiler evidence

A Rust const with Interior Mutability Has No Single Address to Borrow

Interior mutation needs an identifiable allocation whose state all users share. Use static for synchronized global storage and const for repeatable values.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Interior mutation exposes allocation identity, but a const does not promise one unique address shared by all uses.
First discriminating check
Use a Sync static or explicit owner for shared mutable identity, then review atomic ordering or lock invariants separately.

const and static both create names available throughout a program, but they promise different things. A const names a value that may be copied or promoted at use sites. A static names one storage location. Interior mutation needs the second identity.

The failing fixture declares a const AtomicUsize and tries to keep a 'static reference to it. Rust emits E0492.

A const is substituted as a value expression

Conceptually, using a const is closer to writing its initializer at the use site than loading from one mandatory allocation. The compiler may inline, duplicate, or promote values according to language rules and optimisation.

For an integer this distinction is normally invisible. For an atomic or cell, identity is the whole point: mutation must affect the same storage that another read observes.

The Reference's constant item section notes that constants do not represent unique memory locations and that references to values with interior mutability have restrictions.

A static provides stable storage identity

The repaired fixture declares static REQUESTS: AtomicUsize. REQUESTS_REF refers to that one allocation, and increments through either path affect the same counter.

The official E0492 page recommends a static whose type is Sync. The Reference describes static items as allocated objects with stable addresses.

This is the semantic repair: shared mutation now has shared identity.

Sync protects cross-thread shared access

Immutable statics must be safe to access from multiple threads, so their types need Sync. Atomics provide synchronized interior mutability and implement the required traits.

Cell and RefCell are for single-threaded interior mutability and are not Sync. Wrapping them in an unsafe manual Sync implementation without real synchronization would be unsound.

I choose an atomic only when the state and ordering fit atomic operations. Multi-field invariants may require a mutex or another owner instead.

Atomic ordering remains a separate contract

The repaired counter uses Ordering::Relaxed because its assertion needs atomicity but no cross-variable happens-before relationship. A readiness flag guarding other data could need Acquire and Release or stronger ordering.

Changing const to static fixes address identity, not memory-order correctness. I document which state transition an atomic synchronizes and test concurrency assumptions separately.

This keeps E0492 from becoming a reason to paste SeqCst everywhere without understanding.

Lazy initialization solves non-const constructors

Some global state cannot be constructed in a const expression. LazyLock, OnceLock, or an application-owned initialization phase can create it at runtime while retaining stable storage.

I prefer dependency injection or explicit context ownership for most services. A static is appropriate for process-wide constants with identity, counters, caches with clear lifecycle, and other truly global state.

The need for a stable address does not automatically make global mutation good architecture.

References to ordinary const values can be promoted

Rust may promote certain immutable values so references live long enough. Interior mutability blocks the assumption that duplicated or promoted instances are observationally equivalent.

That explains why &42 can often become effectively static while &AtomicUsize::new(0) cannot be treated as one shared counter. The latter's future state exposes which allocation was chosen.

Address identity should be tested through behaviour

For a shared counter, I test that mutation through one exported handle is observed through another, not merely that the code compiles. For concurrency-sensitive uses, I add multi-threaded tests around the actual invariant rather than asserting an address equality and assuming synchronization follows. A stable address answers where state lives; atomic operations and their orderings answer how access is coordinated. Keeping those proofs separate makes it harder to mistake static storage for a complete concurrency design.

My E0492 checklist

  • Does the const value contain Cell, an atomic, or other interior mutability?
  • Does the program require one shared address and evolving state?
  • Should the item be a static instead?
  • Is its type safely Sync for global shared access?
  • What atomic ordering or lock invariant does mutation require?
  • Does runtime construction need OnceLock or LazyLock?
  • Is process-wide state really the right ownership boundary?
  • Would a passed context make tests and lifecycle clearer?

The core principle is that mutation is observable identity. I use const for repeatable values and static or explicit owners when every observer must reach the same changing storage.