Extending lifetime of stack variable to 'static

The concrete question would be, if you have a variable on the stack of a function that never returns, is it safe to extend its lifetime to 'static using transmute?

fn main() -> ! {
    let x = 42;

    // Safety: This function never returns, therefore the reference is valid
    // until the end of the program
    let x = unsafe { make_static(&x) };

    loop {
        take_static_ref(x);
    }
}

fn take_static_ref(x: &'static i32) {
    println!("{x}");
}


unsafe fn make_static<T>(t: &T) -> &'static T {
    core::mem::transmute(t)
}

It's been debated, not sure if it has been resolved.

https://www.reddit.com/r/rust/comments/z2jk23/a_better_way_to_borrow_in_rust_stack_tokens/?rdt=44776

1 Like

It's not enough for the function to never return, it also need to never unwind (i.e. never panic), otherwise stack variables will be dropped during the unwind and the static reference will become invalid.

In my opinion your example is much better solved by leaking a Box.

4 Likes

I'm working on firmware that has no allocator available, so this is not an option in my case.

1 Like

It is unsound because you can mutate the value behind the shared reference, which is UB. You can check this with MIRI on the playground.

fn main() -> ! {
    let mut a = 42;

    let x = unsafe { make_static(&a) };
    dbg!(x);
    a = 43;
    let y = unsafe { make_static(&a) };

    dbg!(x);
    dbg!(y);
    
    loop {}
}

unsafe fn make_static<T>(t: &T) -> &'static T {
    core::mem::transmute(t)
}
3 Likes

Then I guess you won't call this function more than once. In that case have you considered using a static, potentially in combination with an OnceCell for initialization?

4 Likes

It seems this problem goes away if you remove the mut from a. Would that make the code sound?

Then I'd add this variable is immutable to the safety comments too.

No, it could have interior mutability, like Cell.

1 Like

Why does interior mutability affect the soundness of the code? If there was unsoundness in the code with interior mutability, the code would be unsound with or without the transmute, wouldn't it? The transmute is just extending the lifetime of the variable, nothing else.

This is probably sound; I wonder if a crate for this exists:

fn with_static<T: 'static>(
    mut x: T,
    f: impl FnOnce(&'static mut T) -> std::convert::Infallible, // <- type `!` not allowed here on stable Rust
) -> ! {
    let _guard = Guard;
    struct Guard;
    impl Drop for Guard {
        fn drop(&mut self) {
            // must not make it beyond this point!
            std::process::abort();
            // might need a no_std alternative if you don't have std
            // e.g. double-panic should abort reliably, too
        }
    }
    // sound, because `_guard` ensures this call can never be left, even with unwinding
    let static_ref: &'static mut T = unsafe { &mut *(&mut x as *mut T) };
    #[allow(unreachable_code)]
    match f(static_ref) {}
}

usage:

fn take_static_ref(x: &'static i32) {
    println!("{x}");
}

fn main() -> ! {
    with_static(42, |x| {
        loop {
            take_static_ref(x);
        }
    })
}

Edit: Glancing at the mk_static API discussed in the article linked above, this seems different and avoids the soundness conflicts AFAICT, because we’re taking the value that’s supposed to be made to “live forever” by-value, not by-reference.

1 Like

I think it might be possible to wrap everything in a struct and make it all static. I'll see if it is possible and I can get rid of those unsafe calls.

The static reference destroys the relationship between a RefMut and it's RefCell. No let mut a needed!.

use std::cell::RefMut;
use std::cell::RefCell;

fn main() -> ! {
    let a = RefCell::new(42);
    
    let x: &'static RefCell<i32> = unsafe { make_static(&a) };
    dbg!(x);
    
    let y: &'static RefMut<'static, i32> = unsafe { make_static(&x.borrow_mut()) };
    drop(a);

    dbg!(y);
    
    loop {}
}

unsafe fn make_static<T>(t: &T) -> &'static T {
    core::mem::transmute(t)
}
2 Likes

I see, I think I understand. Thank you for your answers :slight_smile:

As he's doing embedded (no-std and even no alloc) work, there's a good chance he's also doing panic = 'abort' in his Cargo.toml and can therefore ignore unwinding concerns.