What's the "correct" way of doing static mut in 2024 Rust?

Hello!

The title of my question may look pretty general, but I actually have a question about a specific warning. I know that static mut globals are inherently unsafe, and potentially a really bad idea. I know that holding more than one mutable reference to something is undefined behavior. But hear me out.

I have a project for a very resource constrained AVR system. My firmware simply becomes too large if I do "proper" handling of globals using avr-device's Mutex and an interrupt-free section every time. Not all my globals are used inside interrupt handlers, and it's a single-threaded processor/program, so I should be good in most situations. With the latest nightly Rust, I'm starting to get the following warning.

Consider this minimal example program:

use core::mem::MaybeUninit;

struct Thing {
    inner: u32,
}

static mut GLOBAL: MaybeUninit<Thing> = MaybeUninit::uninit();

fn main() {
    unsafe {
        GLOBAL = MaybeUninit::new(Thing { inner: 42 });
    }

    unsafe {
        let mutable_reference = GLOBAL.assume_init_mut();
        mutable_reference.inner += 1295;

        println!("Thing.inner: {}", mutable_reference.inner);
    }
}

This is the warning:

warning: creating a mutable reference to mutable static is discouraged
  --> src/main.rs:15:33
   |
15 |         let mutable_reference = GLOBAL.assume_init_mut();
   |                                 ^^^^^^^^^^^^^^^^^^^^^^^^ mutable reference to mutable static
   |
   = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/static-mut-references.html>
   = note: mutable references to mutable statics are dangerous; it's undefined behavior if any other pointer to the static is used or if any other reference is created for the static while the mutable reference lives
   = note: `#[warn(static_mut_refs)]` on by default

warning: `global-thing` (bin "global-thing") generated 1 warning

But when I change it like so:

use core::mem::MaybeUninit;

struct Thing {
    inner: u32,
}

static mut GLOBAL: MaybeUninit<Thing> = MaybeUninit::uninit();

fn main() {
    unsafe {
        GLOBAL = MaybeUninit::new(Thing { inner: 42 });
    }

    unsafe {
        // let mutable_reference = GLOBAL.assume_init_mut();
        let mutable_reference = (*(&raw mut GLOBAL)).assume_init_mut();
        mutable_reference.inner += 1295;

        println!("Thing.inner: {}", mutable_reference.inner);
    }
}

The warning goes away. I don't really understand why the second thing is fundamentally different than the first example. Is doing it the second way actually safer somehow?

Thanks so much in advance :slight_smile:

Quick edit:

Looking at the edition guide for the upcoming Rust 2024 edition, the warning #[warn(static_mut_refs)] will become an error because it is instantaneous undefined behavior to create a mutable or immutable reference to a global mutable static variable which violates the mutable XOR aliasing reference rules. The recommendation is to instead use interior mutability, such as with a Arc<Mutex> around an immutable shared global.

If you really cannot afford a safe abstraction like Mutex, you can use UnsafeCell to unsafely assert your mutable non-aliasing accesses.

Hm yeah, that might just be the way. Thanks!

Still wondering why using a &raw mut operator silences the warning, though.

I would try to use atomics for global mutability, if possible, since these are in the core package. There should be little/no cost with relaxed ordering, which may be all you need.

Do you know what the difference is between static mut T and static UnsafeCell<T> in terms of undefined behavior? Why is UnsafeCell more "defined"? Will access to the latter be compiled any differently?

This won't be zero cost on AVR. So it will probably not work for OP.

IIRC there is no difference as long as you write the strictly equivalent thing. However, static mut is much easier to mess up because calling methods on static muts will implicitly create &/&mut references, whereas with UnsafeCell you need to cast the *mut pointer as &T.

Well, it's just a warning—just a rustc lint. Not a safety mechanism. Lints in general are never meant to deal with adversarial code. So if you obfuscate the referencing by taking a pointer and immediately undoing, There is no reason why any linter should warn you.

You missed an important clause in the guide: “in violation of Rust's mutability XOR aliasing requirement”. Taking references is not UB; taking a mutable/exclusive reference in a way which doesn’t keep it exclusive is UB. static mut is hazardous because the borrow checker can’t, and silently doesn’t, check this for you like it does for normal variables.

That makes sense, thanks. So I won't feel safe just because the warning is gone then :slight_smile:

I suppose the best thing to do is to make sure that globals used in interrupt handlers are always accessed in an interrupt-free context, and otherwise use UnsafeCell if I can guarantee that mutable references are exclusive. Thanks very much for everyone's replies :slightly_smiling_face:

An aliased &mut _ is instant UB (including for example two independently created &mut _ on the stack in a single-threaded program), whereas with UnsafeCell you can just worry about reads and writes.

See this issue, the first 10 or 15 comments in particular.

"Only" if the normal &mut _ rules are violated (the wording in the tracking issue isn't stellar). It will be deny by default because that's extremely difficult to achieve with static mut, and there are more alternatives in std now.

I feel like this really is the correct solution, so I marked it as such. Thanks again!

If you just want to initialize the static, making a const fn init function may be a possibility.

Oh, I see. It's an 8 bit processor so all larger atomic ops have to disable interrupts.

The statement apples to static mut (variable as in "mutable"), not just any static binding (variable as in "binding") as I initially understood this summary.

I should have gathered from context, but initially had a huge doubletake and checked the edition guide to be sure.

It isn't. You've just confused the compiler. The risks are the same.

The correct way to deal with static mut is to only work through raw pointers. This means you create a pointer via &raw mut GLOBAL, and then use pointer methods](std::ptr - Rust) to work with it. So no *ptr = val;, instead use ptr.write(val). Similarly, reads are done via ptr.read(). This way you never create any references, and thus not at risk of violating aliasing rules (although see this discussion).

static GLOBAL: UnsafeCell<T> is functionally the same. You just can't accidentally create a &mut to its contents via a method call. Surprisingly, GLOBAL += 1 or GLOBAL.add_assign(1) don't trigger the static_mut_refs warning.

Thanks for the correction. I've updated my original response to clarify UB only occurs when the mutable aliasing rules are violated, but the warning will become an error because reasoning about this is particularly difficult.

Because that gives you a pointer without ever making the intermediate reference.

So long as you stay in pointer-land, you just need to avoid problems during reads and writes, which is generally what people are thinking about anyway. Doing that avoids the "exactly when are the references considered used that you must uphold the reference rules" issues that are much, much harder to get right.

Is this a safety requirement? AFAIK the only difference between those two statements (assuming that ptr is a raw mutable pointer), is that *ptr = val calls drop on pointed-to value, so it introduces UB when ptr points to uninitialized memory. In other words safety requirements of *ptr = val are the same as of following code:

ptr.drop_in_place();
ptr.write(val);

So using pointer methods is preferable, because it is harder to introduce UB with them, but if careful, dereferencing raw pointers should be fine.

Am I right, or does using *prt = val indeed inttroduces UB?