Is `static mut` unsafe in a single-threaded context?

I know that accessing or writing to static mut is unsafe because it can lead to data races if multiple threads try to access the data. However, in a strictly single-threaded context, is there any possibility of undefined behavior?

Yes; since you can obtain &'static mut references from a static mut variable at any time, you can obtain aliasing mutable references, which results in undefined behavior. For example, this program will print 1 with optimizations enabled, since the compiler assumes that one reference cannot affect the other (Rust Playground):

static mut EXAMPLE: i32 = 0;

fn example(ref1: &mut i32, ref2: &mut i32) -> i32 {
    *ref1 = 1;
    *ref2 = 2;
    *ref1
}

fn main() {
    use std::hint::black_box;
    let ref1 = black_box(unsafe { &mut EXAMPLE });
    let ref2 = black_box(unsafe { &mut EXAMPLE });
    println!("{}", example(ref1, ref2));
}

With static mut variables, you must manually and unsafely uphold the rules for reference lifetimes.

7 Likes

A bit more extended explanation might be found here - The Problem With Single-threaded Shared Mutability - In Pursuit of Laziness.

5 Likes

Another example of UB:

6 Likes

The other examples talk about aliasing being UB, but there's a much simpler example that shows why it is wrong:

static mut VEC: Vec<i32> = Vec::new();

fn main() {
    unsafe {
        VEC.push(10);
        
        let ten_ref = &VEC[0];
        VEC.clear();
        println!("{}", *ten_ref);
    }
}

Here, you are accessing a value after destroying it with clear. That's not ok.

13 Likes

The interesting question is that if you didn't need unsafe to access a static mut, would it be safe in a single threaded program, assuming regular safety rules applied.

The answer is that regular rules can't apply! You would have to prove that you don't have illegally overlapping references statically, for example to require that you pass the lack of a lifetime around in calls... which is quite a strange concept.

Much easier to just initialize the value somewhere and pass a reference around, you can use Box::leak if you don't want to worry about lifetime parameters.

1 Like