Are Mutable Statics Only Unsound When Accessing Over Multiple Threads?

I've got a single-threaded program here I would like to have essentially a static !Sync, OnceCell to emulate a global variable ( assume I've already justified the use-case for a global variable ), but Rust requires all statics to be Sync unless they are mut static which is unsafe to access. Is it sound to access a mut static if I never spawn a thread in my program?

It's sound if you never take a mutable reference to it that overlaps with other references.

OK, then if I put an RC in it, I never have to take a mutable reference at all and the RC will enforce the borrowing rules so I should be good, then, right?

It'll be difficult to put a Rc into a mutable static because Rc allocates. If you mean RefCell, then that would be okay, except you can't put it in a static because RefCell doesn't implement sync.

Oh, yeah, I meant I could do this:

use once_cell::unsync::Lazy; // 1.7.2
use std::sync::Mutex;

static mut VAR: Lazy<Mutex<i32>> = Lazy::new(|| Default::default());

fn main() {
    // All this is sound because there is not access from different threads
    unsafe {
        *VAR.lock().unwrap() += 1;
    }
    
    unsafe {
        assert_eq!(*VAR.lock().unwrap(), 1);
    }
}

It is essentially a !Sync, lazy_static!.

Do you actually want to increment integers? If so you can use atomic integers here and avoid unsafe code altogether.

No, that' was just my example. I actually need to access a data struct.

To do this without unsafe, you can use once_cell::sync::Lazy (or it's std implementation, if you are okay with using nightly).

This provides safe immutable access to the static. To mutate the contained values you'd need to wrap it in a Mutex as well; i.e. static once_cell::sync::Lazy<Mutex<???>>

Let me ask another question:

assuming a way (other than static mut!) to circumvent the Sync requirement for static variables, would it be sound to use it if I never spawn a thread in my program?

And the answer is: if there is only one thread at play, then it is indeed sound!

  • (Hint of) proof? thread-local statics. Be it through the thread_local! macro & .with API (needed because the whole API drops the statics, there, but assuming they didn't, and assuming threads never died, a simple &-getter would be possible), or be it through the unstable #[thread_local] attribute.

From this point stem two questions:

  • Is it possible to learn this power? How does one circumvent the Sync requirement for statics using unsafe?

  • Would using a static mut be equivalent, or does a static mut grant more powers and thus require more responsibilities (thus making it more error-prone / dangerous / unsafe!).

To answer the first question: simply use a newtype (with a private constructor) and unsafe impl Sync on it. Then within the module with access to the private construction, expose a public static of that type, and impl Deref on it for convenience.

  • Macro PoC, unsync_static!:
    macro_rules! unsync_static {(
        $unsafe:ident {/* forward `unsafe` contract from caller */
            $( #[doc = $doc:expr] )* /* Only allow docstring attrs */
            $pub:vis
            static ref $VAR:ident : $T:ty = $value:expr;
        }
    ) => (
        $pub use $VAR::$VAR;
    
        #[allow(nonstandard_style)]
        mod $VAR {
            use super::*;
    
            $( #[doc = $doc] )*
            pub static $VAR: __ = __($value);
    
            use __private__::__;
            mod __private__ {
                use super::*;
                pub struct __(pub(in super) $T);
            }
            
            impl ::core::ops::Deref for __ {
                type Target = $T;
                
                #[inline]
                fn deref (self: &'_ __)
                  -> &'_ $T
                {
                    let &__(ref inner) = self;
                    inner
                }
            }
            
            $unsafe
            impl ::core::marker::Sync for __ {}
        }
    )}
    use unsync_static;
    


And now, the answer to the second question, where we are back to the OP: yes, static mut grants more powers than a !Sync static, which is the reason static muts are to be deprecated: anything a static mut can do a static UnsafeCell can also do it, but in a less dangerous / error-prone manner (in the same fashion that MaybeUninit can do everything mem::uninitialized() can do, but in a less dangerous / error-prone manner). The following, for instance, is UB:

static mut ARR: [u8; 2] = [42, 27];
let mut refs: Vec<&mut u8> =
    (0 .. 2)
        .map(|i| unsafe {
            let arr: &mut [u8; 2] = &mut ARR; // assumes a unique ref to the *whole* `ARR`
            &mut arr[i] // reduce the span of the ref, but too late.
        })
        .collect()
;
*refs[0] += *refs[1];
  • and it not very clear to me whether directly doing &mut ARR[i] would lead to UB or not (I suspect it's still UB), which is not a good place to be!

    • Yes, something as "inocuous" as doing [&mut ARR[0], &mut ARR[1]] (and then using that first element) is likely to be UB!
  • to see why, replace static mut with let mut and notice how the borrow checker starts complaining.

Whereas the following showcases well-defined behavior :slightly_smiling_face::

unsync_static! {
    unsafe {
        // Safety: no threads spawned whatsoever
        static ref ARR: UnsafeCell<[u8; 2]> = UnsafeCell::new([u8; 2]);
    }
}

let mut refs: Vec<&mut u8> =
    (0 .. 2)
        .map(|i| unsafe {
            let arr: *mut [u8; 2] = ARR.get(); // does not assume uniqueness yet!
            &mut *arr.cast::<u8>().add(i) // assert uniqueness over the i-th field only
        })
        .collect()
;
*refs[0] += *refs[1]; // OK.
  • Playground (feel free to run with MIRI, as well as the commented out version).
2 Likes

The issue with using once_cell::sync::Lazy is that the object I need to put into the Lazy is !Sync.

Mutex<T> is Sync even if T: !Sync. And you can .lock() it on just static, not static mut. If you don't like the Lazy part you can use parking_lot crate which have its own Mutex type with const initializer.

Consider static mut like the goto. Never use it on the hand-written code.

4 Likes

OK, cool! I'll digest this more thorougly later, but it sounds like you guys have answered my question. :slight_smile:

Follow-up: an alternative API to the macro-based one would be the following type:

mod privacy_boundary {
    #[repr(transparent)]
    pub
    struct UnsafeMakeInstanceSync<T : ?Sized> /* = */ (
        T,
    );

    impl<T> UnsafeMakeInstanceSync<T> {
        /// Safety: a shared reference to this instance of type
        /// `Self` must never be produced in thread different from
        /// where this instance lives.
        pub
        unsafe
        fn new (value: T)
          -> Self
        {
             Self(value)
        }
    }

     impl<T : ?Sized> ::core::ops::Deref
        for UnsafeMakeInstanceSync<T>
    {
        type Target = T;

        #[inline]
        fn deref (self: &'_ Self)
          -> &'_ T
        {
            &self.0
        }
    }

    unsafe // Safety: no public non-`unsafe` constructors
    impl<T : ?Sized> Sync for UnsafeMakeInstanceSync<T>
    {}
}

And then you'd be able to declare your static as:

static ARR: UnsafeMakeInstanceSync<[Cell<u8>; 2]> = unsafe {
    // Safety: single-threaded execution.
    UnsafeMakeInstanceSync::new([Cell::new(42), Cell::new(27)])
};

Finally, note that all I am saying with this still unsafe API, is that it is, at the very least, safer than a static mut! This is to answer to the OP's question.

If we XY your problem, then I suspect indeed that there be non-unsafe ways to tackle your problem with, at worst, a negligible runtime cost :slightly_smiling_face:

1 Like

Personally, I'd just roll my own minimalistic Mutex, if I only wanted it for single-threaded access and do the minimum necessary to provide a safe API around it:

use std::cell::UnsafeCell;
use std::ops::Deref;
use std::ops::DerefMut;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering::Acquire;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::atomic::Ordering::Release;

#[derive(Default)]
pub struct Mutex<T: ?Sized> {
    locked: AtomicBool,
    value: UnsafeCell<T>,
}

unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}

unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}

impl<T> Mutex<T> {
    pub const fn new(value: T) -> Self {
        Self {
            locked: AtomicBool::new(false),
            value: UnsafeCell::new(value),
        }
    }

    pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
        if self
            .locked
            .compare_exchange(false, true, Acquire, Relaxed)
            .is_ok()
        {
            Some(MutexGuard { mutex: self })
        } else {
            None
        }
    }
}

#[must_use = "if unused the Mutex will immediately unlock"]
pub struct MutexGuard<'a, T> {
    mutex: &'a Mutex<T>,
}

impl<T> Drop for MutexGuard<'_, T> {
    fn drop(&mut self) {
        self.mutex.locked.store(false, Release);
    }
}

impl<T> Deref for MutexGuard<'_, T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { &*(self.mutex.value.get() as *const T) }
    }
}

impl<T> DerefMut for MutexGuard<'_, T> {
    fn deref_mut(&mut self) -> &mut T {
        unsafe { &mut *self.mutex.value.get() }
    }
}

I think it offers a good opportunity for people wanting to dive into writing safe wrappers around unsafe internals. It features interior mutability, atomic operations, as well as unsafe implementations of Send and Sync. Most importantly, though, it's an API, that is simple enough to digest and reason about for a beginner of the unsafe realm.

The spatial overhead is that of an AtomicBool and additional padding dependent on the stored type's alignment [EDIT: …, as well as the reference to the Mutex stored in MutexGuard]. The temporal overhead is the cost of performing a compare-exchange with Acquire/Relaxed ordering when acquiring the lock and a store with Release ordering when releasing the lock.

The ergonomic overhead is the existence of the wrapped reference in form of MutexGuard, partially mitigated by implementing Deref(Mut) and the potential panic when calling MUTEX.try_lock().unwrap() while a MutexGuard to the same Mutex still exists. The latter shouldn't happen in single-threaded code, but if it ever does, it saved you from undefined behavior.

2 Likes