ruststaticlifetime

How to assign mutable closure to a static mut variable


I have a static variable, to store callback for some system event: static mut CALLBACK: Option<Box<dyn FnMut()>> = None;

My goal is to be able to assign my closure to this CALLBACK (by "closure" i mean closure that really captures it's environment, and cannot be casted to 'static lifetime)

So made a struct CbGuard, that works as Guard for that CALLBACK, and guarantees it's valid state.

impl CbGuard
{
    fn new(user_cb: impl FnMut()) -> Self
    {
        unsafe{
            CALLBACK = Some(Box::new(user_cb));
        };
        CbGuard{}
    }
}
impl Drop for CbGuard
{
    fn drop(&mut self) {
        unsafe{
            CALLBACK = None;
        };
    }
}

but compiler says that user_cb must be 'static. As i understand - to match lifetimes of user_cb and CALLBACK. But that is what im trying to avoid. I want to use it like that:

let mut my_var:i32 = 0;
let guard = CbGuard::new(||{
    my_var = 5;
});

My thoughts:

  1. It might be reference counter as CALLBACK. But that i think it's too "heavy" for that exact problem + concept of "shared ownership" is not applicable here. Because real owner of CALLBACK is CbGuard. And he is only one at a time.
  2. If rust has a "smart" typesystem and lifetime checks - this problem must be solvable at the language level. But i can not really find it

What might be the solution?


Solution

  • What you think is sound is actually not. It's true that CbGuard is the "owner", metaphorically, of the callback, but CbGuard itself has a 'static lifetime, so it is possible to carry it around freely. With that, it is possible to use a non-'static closure and invoke it after it has expired.

    So the compiler is right: you need 'static. Alternatively, you can make CbGuard generic over the callback type, which will prevent it from being used after its lifetime has expired (and it is possible for it to carry only a lifetime, not a whole type).


    I'm attaching an example of exploit. Note that I'm ignoring thread safety here, which also is a severe problem.

    static mut CALLBACK: Option<Box<dyn FnMut()>> = None;
    
    struct CbGuard {}
    
    impl CbGuard {
        fn new<T: FnMut()>(cb: T) -> Self {
            unsafe {
                CALLBACK = Some(std::mem::transmute::<Box<dyn FnMut() + '_>, Box<dyn FnMut()>>(Box::new(cb)));
            };
            CbGuard {}
        }
        
        fn call(&mut self) {
            unsafe { CALLBACK.as_mut().unwrap()() }
        }
    }
    impl Drop for CbGuard {
        fn drop(&mut self) {
            unsafe {
                CALLBACK = None;
            };
        }
    }
    
    fn exploit() -> CbGuard {
        let s = "abc".to_owned();
        let callback = || println!("{s}");
        CbGuard::new(callback)
    }
    
    fn main() {
        exploit().call();
    }
    

    Playground.


    Example of a sound CbGuard, that also considers thread safety:

    use std::marker::PhantomData;
    use std::mem::transmute;
    use std::sync::atomic::{AtomicBool, Ordering};
    
    static CALLBACK_STORED: AtomicBool = AtomicBool::new(false);
    static mut CALLBACK: Option<Box<dyn FnMut() + Send>> = None;
    
    pub struct CbGuard<'a> {
        _marker: PhantomData<&'a ()>,
    }
    
    impl<'a> CbGuard<'a> {
        #[inline]
        pub fn new<T: FnMut() + Send + 'a>(cb: T) -> Result<Self, ()> {
            if CALLBACK_STORED.swap(true, Ordering::Acquire) {
                // Another callback already stored.
                return Err(());
            }
            // SAETY: We verify no other callback is stored here and locked the place, so nobody else could access it.
            unsafe {
                CALLBACK = Some(transmute::<
                    Box<dyn FnMut() + Send + '_>,
                    Box<dyn FnMut() + Send>,
                >(Box::new(cb)));
            };
            Ok(CbGuard {
                _marker: PhantomData,
            })
        }
    
        #[inline]
        pub fn call(&mut self) {
            // SAFETY: We have a callback stored and we have a mutable reference so nobody else is accessing.
            unsafe { CALLBACK.as_mut().unwrap_unchecked()() }
        }
    }
    impl Drop for CbGuard<'_> {
        #[inline]
        fn drop(&mut self) {
            // SAFETY: We have a mutable reference so nobody else can access.
            unsafe {
                CALLBACK = None;
            }
            CALLBACK_STORED.store(false, Ordering::Release);
        }
    }